/**
 * `PluginRuntimeProvider` — bootstraps the plugin runtime once per app session
 * (ticket #43, F1, carnet §1.3): fetches the runtime catalog (already filtered
 * to `enabled && !pendingUninstall` by the backend) and loads every bundle via
 * {@link loadPlugins}, then exposes the resulting {@link PluginRuntimeRegistry}
 * to the rest of the tree (menus in F3, layouts in F4).
 *
 * Mounted once near the app root, inside `<DIProvider>` (it reads gateways via
 * `useGateways()`). Never re-runs within a session — carnet §1.3: disable/
 * uninstall only mask contributions in-session; the actual reload happens on
 * the next app start (a fresh mount of this provider).
 */

import { createContext, useContext, useEffect, useState, type ReactNode } from "react";

import {
  loadPlugins,
  PluginRuntimeRegistry,
  type PluginLoadFailure,
  type PluginLoadPending,
} from "@/plugins/runtime";
import { useGateways } from "@/app/di";
import type { PluginRuntimePlugin, Unsubscribe } from "@/domain";

export interface PluginRuntimeContextValue {
  registry: PluginRuntimeRegistry;
  failures: PluginLoadFailure[];
  pending: PluginLoadPending[];
  /** True until the initial catalog fetch + bundle loads have settled. */
  loading: boolean;
}

/**
 * Default value used outside a `<PluginRuntimeProvider>` (e.g. `ProjectsView`
 * rendered directly in tests/Storybook without the full app shell): an empty,
 * already-settled registry rather than a hard requirement to wrap every call
 * site. Plugin contributions are strictly additive, so their absence must
 * never be a reason a surface fails to render.
 */
const EMPTY_PLUGIN_RUNTIME: PluginRuntimeContextValue = {
  registry: new PluginRuntimeRegistry(),
  failures: [],
  pending: [],
  loading: false,
};

const PluginRuntimeContext = createContext<PluginRuntimeContextValue>(EMPTY_PLUGIN_RUNTIME);

function describeError(e: unknown): string {
  if (e && typeof e === "object" && "message" in e) {
    return String((e as { message: unknown }).message);
  }
  return String(e);
}

interface PluginRuntimeProviderProps {
  children: ReactNode;
  /** Test/Storybook escape hatch — skips the gateway fetch and uses this value as-is. */
  value?: PluginRuntimeContextValue;
}

export function PluginRuntimeProvider({ children, value: injected }: PluginRuntimeProviderProps) {
  const gateways = useGateways();
  const [value, setValue] = useState<PluginRuntimeContextValue>(
    injected ?? {
      registry: new PluginRuntimeRegistry(),
      failures: [],
      pending: [],
      loading: true,
    },
  );

  useEffect(() => {
    if (injected) return;
    let cancelled = false;
    const pluginGateways = {
      project: gateways.project,
      git: gateways.git,
      terminal: gateways.terminal,
      agents: gateways.agent,
      system: gateways.system,
      window: gateways.window,
      workState: gateways.workState,
      focusedProject: gateways.focusedProject,
      pluginWorkspace: gateways.pluginWorkspace,
      pluginTask: gateways.pluginTask,
      pluginToolchain: gateways.pluginToolchain,
      pluginEvents: gateways.pluginEvents,
      pluginConfig: gateways.pluginConfig,
      pluginStorage: gateways.pluginStorage,
    };
    let activatedProjectScoped = false;
    let pendingProjectPlugins: PluginRuntimePlugin[] = [];
    let unsubscribeFocus: Unsubscribe | undefined;

    void gateways.focusedProject
      .onFocusedProjectChanged(async (project) => {
        if (!project || activatedProjectScoped || pendingProjectPlugins.length === 0) return;
        activatedProjectScoped = true;
        const projectPlugins = pendingProjectPlugins;
        pendingProjectPlugins = [];
        try {
          const result = await loadPlugins(projectPlugins, pluginGateways);
          if (cancelled) return;
          setValue((prev) => {
            for (const plugin of result.registry.list()) prev.registry.add(plugin);
            return {
              registry: prev.registry,
              failures: [...prev.failures, ...result.failures],
              pending: prev.pending.filter(
                (p) => !projectPlugins.some((entry) => entry.id === p.pluginId),
              ),
              loading: false,
            };
          });
        } catch (e: unknown) {
          if (cancelled) return;
          setValue((prev) => ({
            ...prev,
            failures: [
              ...prev.failures,
              ...projectPlugins.map((plugin) => ({
                pluginId: plugin.id,
                reason: describeError(e),
              })),
            ],
            pending: prev.pending.filter(
              (p) => !projectPlugins.some((entry) => entry.id === p.pluginId),
            ),
            loading: false,
          }));
        }
      })
      .then((unsubscribe) => {
        if (cancelled) unsubscribe();
        else unsubscribeFocus = unsubscribe;
      });

    gateways.plugin
      .listRuntimeContributions()
      .then(async (catalog) => {
        const result = await loadPlugins(catalog.plugins, pluginGateways);
        pendingProjectPlugins = catalog.plugins.filter((entry) =>
          result.pending.some((p) => p.pluginId === entry.id),
        );
        return result;
      })
      .then((result) => {
        if (cancelled) return;
        setValue({
          registry: result.registry,
          failures: result.failures,
          pending: result.pending,
          loading: false,
        });
      })
      .catch((e: unknown) => {
        // No plugin gateway / catalog fetch failed: run with zero plugins
        // rather than blocking the app (full-trust plugins are additive).
        if (!cancelled) {
          setValue((prev) => ({
            ...prev,
            failures: [
              ...prev.failures,
              { pluginId: "<runtime-catalog>", reason: describeError(e) },
            ],
            pending: [],
            loading: false,
          }));
        }
      });
    return () => {
      cancelled = true;
      unsubscribeFocus?.();
    };
    // Gateways are a stable singleton for the app session (from `useGateways`);
    // re-running on every render would reload every plugin bundle.
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []);

  return (
    <PluginRuntimeContext.Provider value={value}>{children}</PluginRuntimeContext.Provider>
  );
}

/**
 * Reads the loaded plugin runtime registry. Outside a `<PluginRuntimeProvider>`
 * this is the empty registry (see {@link EMPTY_PLUGIN_RUNTIME}), never a throw.
 */
export function usePluginRuntime(): PluginRuntimeContextValue {
  return useContext(PluginRuntimeContext);
}
