/**
 * Plugin bootstrap loader (ticket #43, F1, carnet §1.3/§6).
 *
 * At UI bootstrap, the app calls {@link loadPlugins} once with the catalog
 * from `PluginGateway.listRuntimeContributions()` (already filtered by the
 * backend to `enabled && !pendingUninstall`, carnet §1.3) and the stable
 * gateways the plugin context exposes. For each entry it dynamically imports
 * the bundle URL, validates the module shape, and calls `activate(ctx)`,
 * scoping the command/layout registries to exactly the ids declared in that
 * plugin's manifest (enforced by {@link PluginCommandRegistry}/
 * {@link PluginLayoutRegistry} themselves).
 *
 * A single plugin failing to load/activate must not break the rest of the
 * app or the other plugins — each is loaded independently and failures are
 * collected, never thrown past `loadPlugins`.
 */

import type { PluginRuntimePlugin } from "@/domain";
import {
  PluginCommandRegistry,
  PluginLayoutRegistry,
  PluginMenuRegistry,
  PluginRuntimeRegistry,
  type LoadedPlugin,
  type PluginGatewaySet,
} from "./registry";

export type { PluginGatewaySet } from "./registry";

export interface IdeaPluginContext extends PluginGatewaySet {
  pluginId: string;
  pluginDisplayName: string;
  version: string;
  commands: PluginCommandRegistry;
  layouts: PluginLayoutRegistry;
  menu: PluginMenuRegistry;
}

export interface PluginActivation {
  dispose?: () => void | Promise<void>;
}

export interface IdeaPluginModule {
  activate(ctx: IdeaPluginContext): PluginActivation | Promise<PluginActivation>;
}

export interface PluginLoadFailure {
  pluginId: string;
  reason: string;
}

export interface PluginLoadResult {
  registry: PluginRuntimeRegistry;
  failures: PluginLoadFailure[];
}

function isIdeaPluginModule(mod: unknown): mod is IdeaPluginModule {
  return (
    typeof mod === "object" &&
    mod !== null &&
    "activate" in mod &&
    typeof (mod as { activate: unknown }).activate === "function"
  );
}

async function loadOne(
  entry: PluginRuntimePlugin,
  gateways: PluginGatewaySet,
): Promise<{ plugin: LoadedPlugin } | { failure: PluginLoadFailure }> {
  try {
    // The bundle URL is a plugin-scoped, content-hashed local protocol URL
    // served by the backend (carnet §1.3) — never a disk path or arbitrary
    // remote URL, and the content hash busts the module cache after updates.
    const mod: unknown = await import(/* @vite-ignore */ entry.bundleUrl);
    if (!isIdeaPluginModule(mod)) {
      return {
        failure: {
          pluginId: entry.id,
          reason: `bundle does not export an "activate(ctx)" function`,
        },
      };
    }

    const declaredCommandIds = new Set(entry.contributes.menuItems.map((item) => item.command));
    const declaredLayoutTypes = new Set(entry.contributes.layouts.map((layout) => layout.type));
    const commands = new PluginCommandRegistry(entry.id, declaredCommandIds);
    const layouts = new PluginLayoutRegistry(entry.id, declaredLayoutTypes);
    const menu = new PluginMenuRegistry(entry.id);

    const ctx: IdeaPluginContext = {
      pluginId: entry.id,
      pluginDisplayName: entry.displayName,
      version: entry.version,
      commands,
      layouts,
      menu,
      ...gateways,
    };

    const activation = await mod.activate(ctx);

    const plugin: LoadedPlugin = {
      pluginId: entry.id,
      displayName: entry.displayName,
      contributes: entry.contributes,
      commands,
      layouts,
      menu,
      dispose: async () => {
        // Best-effort, full-trust (carnet §1.3) — a broken `dispose()` must not
        // prevent removing the plugin from the runtime registry.
        try {
          await activation.dispose?.();
        } catch {
          /* best-effort */
        }
      },
    };
    return { plugin };
  } catch (e) {
    return {
      failure: {
        pluginId: entry.id,
        reason: e instanceof Error ? e.message : String(e),
      },
    };
  }
}

/**
 * Loads every plugin in the catalog into a fresh {@link PluginRuntimeRegistry}.
 * Called once at app bootstrap (and never again in the same session — carnet
 * §1.3: disable/uninstall only masks contributions in-session, the actual
 * unload happens on next restart via a fresh `loadPlugins` call).
 */
export async function loadPlugins(
  catalogPlugins: PluginRuntimePlugin[],
  gateways: PluginGatewaySet,
): Promise<PluginLoadResult> {
  const registry = new PluginRuntimeRegistry();
  const failures: PluginLoadFailure[] = [];

  const results = await Promise.all(catalogPlugins.map((entry) => loadOne(entry, gateways)));
  for (const result of results) {
    if ("failure" in result) failures.push(result.failure);
    else registry.add(result.plugin);
  }

  return { registry, failures };
}
