/**
 * Plugin runtime registries (ticket #43, F1, carnet §6/§7) — the in-memory,
 * per-session home for whatever a loaded plugin bundle registers via its
 * `activate(ctx)` call. Rebuilt from scratch on every app boot (carnet §1.3):
 * nothing here is persisted, the domain only persists declarative contribution
 * metadata and (for layouts) opaque cell state.
 *
 * Each registry enforces the "declared ids only" rule (carnet §6): a plugin can
 * only register a command/menu item/layout whose id was declared in its own
 * manifest `contributes`. This is the runtime half of that contract; the other
 * half (manifest validation) lives in the backend.
 */

import type { ComponentType } from "react";

import type {
  PluginContributionDto,
  PluginLayoutContribution,
  PluginMenuItemContribution,
  PluginTopLevelMenuContribution,
} from "@/domain";
import type {
  AgentGateway,
  FocusedProjectGateway,
  GitGateway,
  PluginConfigGateway,
  PluginEventGateway,
  PluginStorageGateway,
  PluginTaskGateway,
  PluginToolchainGateway,
  PluginWorkspaceGateway,
  ProjectGateway,
  SystemGateway,
  TerminalGateway,
  WindowGateway,
  WorkStateGateway,
} from "@/ports";

/** Internal gateways used to build the public plugin service facade. */
export interface PluginGatewaySet {
  project: ProjectGateway;
  git: GitGateway;
  terminal: TerminalGateway;
  agents: AgentGateway;
  system: SystemGateway;
  window: WindowGateway;
  workState: WorkStateGateway;
  focusedProject: FocusedProjectGateway;
  pluginWorkspace: PluginWorkspaceGateway;
  pluginTask: PluginTaskGateway;
  pluginToolchain: PluginToolchainGateway;
  pluginEvents: PluginEventGateway;
  pluginConfig: PluginConfigGateway;
  pluginStorage: PluginStorageGateway;
}

/** A disposable handle returned by every `register*` call. */
export interface Disposable {
  dispose(): void;
}

export type PluginCommandHandler = (...args: unknown[]) => void | Promise<void>;

/** Commands a plugin registers in `activate(ctx)`, dispatched by menu items. */
export class PluginCommandRegistry {
  private handlers = new Map<string, PluginCommandHandler>();

  constructor(
    private readonly pluginId: string,
    private readonly declaredCommandIds: ReadonlySet<string>,
  ) {}

  register(commandId: string, handler: PluginCommandHandler): Disposable {
    if (!this.declaredCommandIds.has(commandId)) {
      throw new Error(
        `plugin "${this.pluginId}" tried to register command "${commandId}" ` +
          "which is not declared by any menu item in its manifest",
      );
    }
    this.handlers.set(commandId, handler);
    return {
      dispose: () => {
        if (this.handlers.get(commandId) === handler) this.handlers.delete(commandId);
      },
    };
  }

  /** Runs a registered command; a no-op (never throws) if none is registered. */
  async run(commandId: string, ...args: unknown[]): Promise<void> {
    const handler = this.handlers.get(commandId);
    if (!handler) return;
    try {
      await handler(...args);
    } catch (e) {
      console.error(
        `[plugin:${this.pluginId}] command "${commandId}" failed`,
        e,
      );
    }
  }

  has(commandId: string): boolean {
    return this.handlers.has(commandId);
  }
}

export interface PluginLayoutProps {
  projectId: string;
  nodeId: string;
  layoutType: string;
  state: unknown;
  setState(next: unknown): void;
  availability: "available";
}

export interface PluginLayoutDefinition {
  type: string;
  component: ComponentType<PluginLayoutProps>;
}

/** Custom React layout components a plugin registers, keyed by declared `type`. */
export class PluginLayoutRegistry {
  private components = new Map<string, ComponentType<PluginLayoutProps>>();

  constructor(
    private readonly pluginId: string,
    private readonly declaredLayoutTypes: ReadonlySet<string>,
  ) {}

  register(def: PluginLayoutDefinition): Disposable {
    if (!this.declaredLayoutTypes.has(def.type)) {
      throw new Error(
        `plugin "${this.pluginId}" tried to register layout type "${def.type}" ` +
          "which is not declared in its manifest",
      );
    }
    this.components.set(def.type, def.component);
    return {
      dispose: () => {
        if (this.components.get(def.type) === def.component) {
          this.components.delete(def.type);
        }
      },
    };
  }

  get(layoutType: string): ComponentType<PluginLayoutProps> | undefined {
    return this.components.get(layoutType);
  }
}

/**
 * Menu registration surface handed to `activate(ctx)`. V1 scope: a plugin
 * declares its menus/items in the manifest (carnet §7.1/§7.2); nothing dynamic
 * is registered here at runtime beyond commands, so this is intentionally a
 * thin marker object today — kept as its own class so a future dynamic-menu
 * capability doesn't change the `IdeaPluginContext` shape.
 */
export class PluginMenuRegistry {
  constructor(private readonly pluginId: string) {}

  /** Present for parity with the carnet's `ctx.menu` — no dynamic ops in V1. */
  get ownerPluginId(): string {
    return this.pluginId;
  }
}

/** One loaded plugin's registries + the manifest contribution it was scoped to. */
export interface LoadedPlugin {
  pluginId: string;
  displayName: string;
  contributes: PluginContributionDto;
  commands: PluginCommandRegistry;
  layouts: PluginLayoutRegistry;
  menu: PluginMenuRegistry;
  dispose(): Promise<void>;
}

function arrayOrEmpty<T>(value: unknown): T[] {
  return Array.isArray(value) ? (value as T[]) : [];
}

/**
 * Aggregate, session-scoped registry every loaded plugin's contributions land
 * in. `PluginRuntimeRegistry` itself never imports bundles (see `loader.ts`);
 * it just holds what has already been loaded and offers lookup/removal.
 */
export class PluginRuntimeRegistry {
  private loaded = new Map<string, LoadedPlugin>();

  add(plugin: LoadedPlugin): void {
    this.loaded.set(plugin.pluginId, plugin);
  }

  /** Best-effort: calls `dispose()` then removes the plugin from the registry. */
  async remove(pluginId: string): Promise<void> {
    const plugin = this.loaded.get(pluginId);
    if (!plugin) return;
    try {
      await plugin.dispose();
    } finally {
      this.loaded.delete(pluginId);
    }
  }

  get(pluginId: string): LoadedPlugin | undefined {
    return this.loaded.get(pluginId);
  }

  list(): LoadedPlugin[] {
    return [...this.loaded.values()];
  }

  /** All top-level menu contributions across every loaded plugin. */
  topLevelMenus(): Array<{ pluginId: string; pluginDisplayName: string; menu: PluginTopLevelMenuContribution }> {
    return this.list().flatMap((p) =>
      arrayOrEmpty<PluginTopLevelMenuContribution>(p.contributes.menus).map((menu) => ({
        pluginId: p.pluginId,
        pluginDisplayName: p.displayName,
        menu,
      })),
    );
  }

  /** All menu-item contributions across every loaded plugin. */
  menuItems(): Array<{ pluginId: string; pluginDisplayName: string; item: PluginMenuItemContribution }> {
    return this.list().flatMap((p) =>
      arrayOrEmpty<PluginMenuItemContribution>(p.contributes.menuItems).map((item) => ({
        pluginId: p.pluginId,
        pluginDisplayName: p.displayName,
        item,
      })),
    );
  }

  /** Layout component for a given `(pluginId, layoutType)` pair, if loaded. */
  layoutComponent(
    pluginId: string,
    layoutType: string,
  ): ComponentType<PluginLayoutProps> | undefined {
    return this.loaded.get(pluginId)?.layouts.get(layoutType);
  }

  /** All layout contributions across every loaded plugin (for the layout selector). */
  layoutContributions(): Array<{
    pluginId: string;
    pluginDisplayName: string;
    layout: PluginLayoutContribution;
  }> {
    return this.list().flatMap((p) =>
      arrayOrEmpty<PluginLayoutContribution>(p.contributes.layouts).map((layout) => ({
        pluginId: p.pluginId,
        pluginDisplayName: p.displayName,
        layout,
      })),
    );
  }

  async runCommand(pluginId: string, commandId: string, ...args: unknown[]): Promise<void> {
    await this.loaded.get(pluginId)?.commands.run(commandId, ...args);
  }
}
