/**
 * Plugin menu contribution resolution (ticket #43, F3, carnet §7.1/§7.2).
 *
 * Pure functions over a {@link PluginRuntimeRegistry} snapshot — no React, so
 * they're trivially unit-testable. `ProjectsView` (the menu bar owner) calls
 * these to build the extra `MenuBarMenu`/`MenuBarItem` entries it splices in
 * between/into the native menus.
 */

import type { MenuTargetId, ResolvedPluginMenuItem } from "@/domain";
import { evaluateWhen, type PluginRuntimeRegistry, type WhenContext } from "@/plugins/runtime";

export interface ResolvedTopLevelMenu {
  id: `plugin:${string}`;
  pluginId: string;
  label: string;
  icon?: string;
  order: number;
}

function nonEmptyString(value: unknown): string | undefined {
  return typeof value === "string" && value.trim().length > 0 ? value : undefined;
}

function objectOrNull(value: unknown): Record<string, unknown> | null {
  return value !== null && typeof value === "object" ? (value as Record<string, unknown>) : null;
}

function finiteOrder(value: unknown): number {
  return typeof value === "number" && Number.isFinite(value) ? value : 0;
}

function compareText(a: unknown, b: unknown): number {
  return String(a ?? "").localeCompare(String(b ?? ""));
}

function targetMatches(actual: unknown, expected: MenuTargetId): actual is MenuTargetId {
  if (actual === expected) return true;
  if (typeof actual !== "string") return false;
  return expected.startsWith("plugin:") && actual === expected.slice("plugin:".length);
}

/**
 * Top-level plugin menus, sorted by `order` then plugin display name then
 * label (carnet §7.1) — rendered between `Panneaux` and `Paramètres`.
 */
export function resolveTopLevelMenus(registry: PluginRuntimeRegistry): ResolvedTopLevelMenu[] {
  return registry
    .topLevelMenus()
    .flatMap(({ pluginId, pluginDisplayName, menu }) => {
      const menuObject = objectOrNull(menu);
      if (!menuObject) return [];
      const id = nonEmptyString(menuObject.id);
      const label = nonEmptyString(menuObject.label);
      if (!id || !label) return [];
      return [
        {
          id: `plugin:${id}` as const,
          pluginId,
          pluginDisplayName,
          label,
          icon: nonEmptyString(menuObject.icon),
          order: finiteOrder(menuObject.order),
        },
      ];
    })
    .sort(
      (a, b) =>
        a.order - b.order ||
        compareText(a.pluginDisplayName, b.pluginDisplayName) ||
        compareText(a.label, b.label),
    );
}

/**
 * Menu items contributed into one target menu (native `panels`/`settings` or a
 * plugin's own top-level menu, `plugin:<menuId>`), `when`-evaluated and sorted
 * by `order` then plugin display name then label (carnet §7.2). A disabled
 * plugin contributes nothing (its entries are simply absent from the
 * registry, carnet §1.3) — this function has nothing extra to filter for that.
 */
export function resolveMenuItems(
  registry: PluginRuntimeRegistry,
  targetMenuId: MenuTargetId,
  whenCtx: WhenContext,
): ResolvedPluginMenuItem[] {
  return registry
    .menuItems()
    .flatMap(({ pluginId, pluginDisplayName, item }) => {
      const itemObject = objectOrNull(item);
      if (!itemObject || !targetMatches(itemObject.targetMenuId, targetMenuId)) return [];
      const id = nonEmptyString(itemObject.id);
      const label = nonEmptyString(itemObject.label);
      const command = nonEmptyString(itemObject.command);
      if (!id || !label || !command) return [];
      const result = evaluateWhen(nonEmptyString(itemObject.when), whenCtx);
      return [
        {
          id,
          pluginId,
          pluginDisplayName: nonEmptyString(pluginDisplayName) ?? pluginId,
          targetMenuId,
          label,
          command,
          enabled: result.ok ? result.value : false,
          disabledReason: result.ok ? undefined : result.reason,
          groupLabel: nonEmptyString(pluginDisplayName) ?? pluginId,
          order: finiteOrder(itemObject.order),
          iconUrl: nonEmptyString(itemObject.icon),
        } satisfies ResolvedPluginMenuItem,
      ];
    })
    .sort(
      (a, b) =>
        a.order - b.order ||
        compareText(a.pluginDisplayName, b.pluginDisplayName) ||
        compareText(a.label, b.label),
    );
}
