/**
 * Plugin loader — discovers built-in and user plugins, resolves inheritance.
 *
 * Built-in plugins are statically imported. User plugins are loaded from
 * ~/.mcpctl/proxymodels/ as .js files (default export must be a ProxyModelFactory).
 *
 * Inheritance:
 *   extends: ['gate', 'content-pipeline'] merges parent hooks.
 *   If two parents define the same hook and the child doesn't override it,
 *   that's a conflict → error at load time.
 */
import type { ProxyModelPlugin, ProxyModelFactory, PluginHookName } from './plugin.js';
import { PLUGIN_HOOK_NAMES } from './plugin.js';

export interface PluginRegistryEntry {
  name: string;
  plugin: ProxyModelPlugin;
  source: 'built-in' | 'local';
}

/**
 * Immutable registry of resolved plugins.
 */
export class PluginRegistry {
  private plugins = new Map<string, PluginRegistryEntry>();

  /** Register a plugin entry. */
  register(entry: PluginRegistryEntry): void {
    this.plugins.set(entry.name, entry);
  }

  /** Get a plugin by name. Returns null if not found. */
  get(name: string): PluginRegistryEntry | null {
    return this.plugins.get(name) ?? null;
  }

  /** Resolve a plugin by name. Returns the plugin or null. */
  resolve(name: string): ProxyModelPlugin | null {
    return this.plugins.get(name)?.plugin ?? null;
  }

  /** List all registered plugins. */
  list(): PluginRegistryEntry[] {
    return [...this.plugins.values()];
  }

  /** Check if a plugin name is registered. */
  has(name: string): boolean {
    return this.plugins.has(name);
  }
}

/**
 * Resolve inheritance for a plugin by merging parent hooks.
 *
 * Rules:
 * - If two parents define the same hook and the child doesn't override, → error.
 * - Child hooks always override parent hooks.
 * - Lifecycle hooks (onSessionCreate/Destroy) chain sequentially (all parents + child run).
 */
export function resolveInheritance(
  plugin: ProxyModelPlugin,
  registry: PluginRegistry,
  visited = new Set<string>(),
): ProxyModelPlugin {
  if (!plugin.extends || plugin.extends.length === 0) {
    return plugin;
  }

  // Circular dependency check
  if (visited.has(plugin.name)) {
    throw new Error(`Circular plugin inheritance detected: ${[...visited, plugin.name].join(' → ')}`);
  }
  visited.add(plugin.name);

  // Resolve all parents recursively
  const resolvedParents: ProxyModelPlugin[] = [];
  for (const parentName of plugin.extends) {
    const parent = registry.resolve(parentName);
    if (!parent) {
      throw new Error(`Plugin '${plugin.name}' extends unknown plugin '${parentName}'`);
    }
    resolvedParents.push(resolveInheritance(parent, registry, new Set(visited)));
  }

  // Helper to access hooks by name
  const getHook = (p: ProxyModelPlugin, name: PluginHookName) =>
    (p as unknown as Record<string, unknown>)[name];
  const hasHook = (p: ProxyModelPlugin, name: PluginHookName) =>
    typeof getHook(p, name) === 'function';

  // Detect hook conflicts: two parents define the same hook, child doesn't override
  const mergedHooks = new Map<string, unknown>();

  for (const hookName of PLUGIN_HOOK_NAMES) {
    if (hasHook(plugin, hookName)) {
      // Child overrides — no conflict possible
      continue;
    }

    // Check which parents define this hook
    const parentsWithHook = resolvedParents.filter((p) => hasHook(p, hookName));

    if (parentsWithHook.length > 1) {
      if (isChainableHook(hookName)) {
        // Chainable hooks (lifecycle) — chain all parents
        mergedHooks.set(hookName, chainHooks(hookName, parentsWithHook));
      } else {
        // Non-chainable hooks — conflict
        const parentNames = parentsWithHook.map((p) => p.name).join(', ');
        throw new Error(
          `Plugin '${plugin.name}': hook '${hookName}' is defined by multiple parents (${parentNames}) ` +
          `and '${plugin.name}' does not override it. Add '${hookName}' to '${plugin.name}' to resolve the conflict.`,
        );
      }
    } else if (parentsWithHook.length === 1) {
      // Single parent defines it — inherit directly
      mergedHooks.set(hookName, getHook(parentsWithHook[0]!, hookName));
    }
  }

  // Build resolved plugin: start with name/description/extends, then layer hooks
  const resolved: Record<string, unknown> = {
    name: plugin.name,
  };
  if (plugin.description !== undefined) resolved['description'] = plugin.description;
  if (plugin.extends !== undefined) resolved['extends'] = plugin.extends;

  // Copy merged parent hooks first
  for (const [hookName, hook] of mergedHooks) {
    resolved[hookName] = hook;
  }

  // Then overlay child hooks (child always wins)
  for (const hookName of PLUGIN_HOOK_NAMES) {
    const childHook = getHook(plugin, hookName);
    if (typeof childHook === 'function') {
      resolved[hookName] = childHook;
    }
  }

  return resolved as unknown as ProxyModelPlugin;
}

/** Hooks that can be chained (all run sequentially) rather than conflicting. */
function isChainableHook(hookName: PluginHookName): boolean {
  return hookName === 'onSessionCreate' || hookName === 'onSessionDestroy';
}

/** Chain multiple lifecycle hooks so all parents run. */
function chainHooks(hookName: PluginHookName, parents: ProxyModelPlugin[]): unknown {
  if (hookName === 'onSessionCreate') {
    return async (ctx: unknown) => {
      for (const parent of parents) {
        if (parent.onSessionCreate) {
          await parent.onSessionCreate(ctx as Parameters<NonNullable<ProxyModelPlugin['onSessionCreate']>>[0]);
        }
      }
    };
  }
  if (hookName === 'onSessionDestroy') {
    return async (ctx: unknown) => {
      for (const parent of parents) {
        if (parent.onSessionDestroy) {
          await parent.onSessionDestroy(ctx as Parameters<NonNullable<ProxyModelPlugin['onSessionDestroy']>>[0]);
        }
      }
    };
  }
  return undefined;
}

/**
 * Load all plugins: built-in first, then user .js files from disk.
 * Resolves inheritance after all plugins are registered.
 */
export async function loadPlugins(
  builtInPlugins: ProxyModelPlugin[],
  userDir?: string,
): Promise<PluginRegistry> {
  const registry = new PluginRegistry();

  // Register built-ins
  for (const plugin of builtInPlugins) {
    registry.register({ name: plugin.name, plugin, source: 'built-in' });
  }

  // Load user plugins from disk
  if (userDir) {
    await loadUserPlugins(registry, userDir);
  } else {
    const defaultDir = `${process.env.HOME ?? '/tmp'}/.mcpctl/proxymodels`;
    await loadUserPlugins(registry, defaultDir);
  }

  // Resolve inheritance for all plugins
  const resolved = new PluginRegistry();
  for (const entry of registry.list()) {
    const resolvedPlugin = resolveInheritance(entry.plugin, registry);
    resolved.register({ name: entry.name, plugin: resolvedPlugin, source: entry.source });
  }

  return resolved;
}

/** Load user plugins from a directory (*.js files with default ProxyModelFactory export). */
async function loadUserPlugins(registry: PluginRegistry, dir: string): Promise<void> {
  const { readdir } = await import('node:fs/promises');
  const { join } = await import('node:path');
  const { pathToFileURL } = await import('node:url');

  try {
    const files = await readdir(dir);
    for (const file of files) {
      if (!file.endsWith('.js')) continue;
      try {
        const mod = await import(pathToFileURL(join(dir, file)).href) as { default?: ProxyModelFactory };
        if (typeof mod.default !== 'function') {
          console.warn(`[plugin-loader] ${file} does not export a default ProxyModelFactory, skipping`);
          continue;
        }
        const plugin = mod.default();
        registry.register({ name: plugin.name, plugin, source: 'local' });
      } catch (err) {
        console.warn(`[plugin-loader] Failed to load ${file}: ${(err as Error).message}`);
      }
    }
  } catch {
    // Directory doesn't exist — no user plugins
  }
}
