/**
 * Plugin Registry — load, install, uninstall, list plugins.
 *
 * Plugins are stored as YAML files:
 * - Built-in: lib/builtin-plugins/*.yaml
 * - User-installed: ~/.forge/plugins/<id>/plugin.yaml
 *
 * Installed plugin configs are stored in:
 * - ~/.forge/data/plugin-configs.json
 */

import { existsSync, readdirSync, readFileSync, writeFileSync, mkdirSync, rmSync } from 'node:fs';
import { join, dirname } from 'node:path';
import { homedir } from 'node:os';
import { fileURLToPath } from 'node:url';
import YAML from 'yaml';
import type { PluginDefinition, InstalledPlugin, PluginSource } from './types';
import { encryptSecret, decryptSecret, isEncrypted } from '../crypto';

const _filename = typeof __filename !== 'undefined' ? __filename : fileURLToPath(import.meta.url);
const _dirname = typeof __dirname !== 'undefined' ? __dirname : dirname(_filename);

const BUILTIN_DIR_COMPILED = join(_dirname, '..', 'builtin-plugins');
const BUILTIN_DIR_SOURCE = join(process.cwd(), 'lib', 'builtin-plugins');
const BUILTIN_DIR = existsSync(BUILTIN_DIR_COMPILED) ? BUILTIN_DIR_COMPILED : BUILTIN_DIR_SOURCE;
const USER_PLUGINS_DIR = join(homedir(), '.forge', 'plugins');
const CONFIGS_FILE = join(homedir(), '.forge', 'data', 'plugin-configs.json');

// ─── Load Plugin Definition ──────────────────────────────

function loadPluginYaml(filePath: string): PluginDefinition | null {
  try {
    const raw = readFileSync(filePath, 'utf-8');
    const def = YAML.parse(raw) as PluginDefinition;
    if (!def.id || !def.name) return null;
    if (!def.actions) return null;
    if (!def.config) def.config = {};
    if (!def.params) def.params = {};
    if (!def.icon) def.icon = '🔌';
    if (!def.version) def.version = '0.0.1';
    if (!def.category) def.category = 'pipeline-node';
    if (!def.mode) def.mode = 'server-side';
    return def;
  } catch {
    return null;
  }
}

// ─── Config Storage ──────────────────────────────────────

/**
 * Return field names declared as `type: secret` on the plugin's
 * top-level `config` schema. Used to decide which fields are
 * encrypted on disk.
 */
export function getSecretFieldNames(def: PluginDefinition | null): string[] {
  if (!def?.config) return [];
  return Object.entries(def.config)
    .filter(([, schema]) => {
      const t = String((schema as any)?.type || '');
      return t === 'secret' || t === 'password';
    })
    .map(([name]) => name);
}

/** Resolve a config row's plugin def, accounting for instance `source`. */
function defForConfigRow(id: string, row: any): PluginDefinition | null {
  const sourceId = (row?.source as string) || id;
  // getPlugin scans YAML files directly — no loadConfigs call, so no
  // cycle even though we're called from loadConfigs.
  try { return getPlugin(sourceId); } catch { return null; }
}

function loadConfigs(): Record<string, { config: Record<string, any>; installedAt: string; enabled: boolean; source?: string; name?: string }> {
  try {
    if (!existsSync(CONFIGS_FILE)) return {};
    const raw = JSON.parse(readFileSync(CONFIGS_FILE, 'utf-8')) as Record<string, any>;
    // Decrypt any encrypted secret fields on read. Plaintext values pass
    // through unchanged so a user-edited file still works (will be
    // encrypted next save by migratePluginSecrets / installPlugin).
    for (const [id, row] of Object.entries(raw)) {
      const def = defForConfigRow(id, row);
      const secrets = getSecretFieldNames(def);
      const cfg = (row as any)?.config;
      if (!cfg || !secrets.length) continue;
      for (const k of secrets) {
        if (typeof cfg[k] === 'string' && isEncrypted(cfg[k])) {
          try { cfg[k] = decryptSecret(cfg[k]); }
          catch (e) { console.warn(`[plugins] decrypt failed for ${id}.${k}:`, (e as Error).message); }
        }
      }
    }
    return raw;
  } catch (e) {
    console.warn('[plugins] loadConfigs failed:', (e as Error).message);
    return {};
  }
}

function saveConfigs(configs: Record<string, any>): void {
  const dir = dirname(CONFIGS_FILE);
  mkdirSync(dir, { recursive: true });
  // Encrypt any secret fields before persisting. Operate on a deep-ish
  // copy so the in-memory object the caller still holds remains decrypted.
  const out: Record<string, any> = {};
  for (const [id, row] of Object.entries(configs)) {
    const def = defForConfigRow(id, row);
    const secrets = getSecretFieldNames(def);
    const copy = { ...(row as any), config: { ...((row as any).config || {}) } };
    for (const k of secrets) {
      const v = copy.config[k];
      if (typeof v === 'string' && v && !isEncrypted(v)) {
        try { copy.config[k] = encryptSecret(v); }
        catch (e) { console.warn(`[plugins] encrypt failed for ${id}.${k}:`, (e as Error).message); }
      }
    }
    out[id] = copy;
  }
  writeFileSync(CONFIGS_FILE, JSON.stringify(out, null, 2), { mode: 0o600 });
}

/**
 * One-shot migration: re-write plugin-configs.json so any plaintext
 * secret fields get encrypted in place. Idempotent — calling it
 * repeatedly is cheap once everything is already encrypted.
 */
export function migratePluginSecrets(): void {
  if (!existsSync(CONFIGS_FILE)) return;
  let raw: Record<string, any>;
  try { raw = JSON.parse(readFileSync(CONFIGS_FILE, 'utf-8')); }
  catch { return; }
  let dirty = 0;
  for (const [id, row] of Object.entries(raw)) {
    const def = defForConfigRow(id, row);
    const secrets = getSecretFieldNames(def);
    const cfg = row?.config;
    if (!cfg || !secrets.length) continue;
    for (const k of secrets) {
      if (typeof cfg[k] === 'string' && cfg[k] && !isEncrypted(cfg[k])) {
        cfg[k] = encryptSecret(cfg[k]);
        dirty++;
      }
    }
  }
  if (dirty > 0) {
    writeFileSync(CONFIGS_FILE, JSON.stringify(raw, null, 2), { mode: 0o600 });
    console.log(`[plugins] Migrated ${dirty} plaintext secret(s) to encrypted storage`);
  }
}

// ─── Public API ──────────────────────────────────────────

/** List all available plugins (built-in + user-installed) */
export function listPlugins(): PluginSource[] {
  const sources: PluginSource[] = [];
  const configs = loadConfigs();

  // Check if any config entry references this plugin (base install or instance)
  const isInstalledOrHasInstance = (id: string) =>
    Object.entries(configs).some(([key, cfg]) => key === id || (cfg as any).source === id);

  // Built-in plugins
  if (existsSync(BUILTIN_DIR)) {
    for (const file of readdirSync(BUILTIN_DIR).filter(f => f.endsWith('.yaml') || f.endsWith('.yml'))) {
      const def = loadPluginYaml(join(BUILTIN_DIR, file));
      if (def) {
        sources.push({
          id: def.id,
          name: def.name,
          icon: def.icon,
          version: def.version,
          author: def.author || 'forge',
          description: def.description || '',
          source: 'builtin',
          installed: isInstalledOrHasInstance(def.id),
          configCount: Object.keys(def.config).length,
          category: def.category,
          mode: def.mode,
        });
      }
    }
  }

  // User-installed plugins
  mkdirSync(USER_PLUGINS_DIR, { recursive: true });
  for (const dir of readdirSync(USER_PLUGINS_DIR, { withFileTypes: true })) {
    if (!dir.isDirectory()) continue;
    const yamlPath = join(USER_PLUGINS_DIR, dir.name, 'plugin.yaml');
    const ymlPath = join(USER_PLUGINS_DIR, dir.name, 'plugin.yml');
    const filePath = existsSync(yamlPath) ? yamlPath : existsSync(ymlPath) ? ymlPath : null;
    if (!filePath) continue;
    const def = loadPluginYaml(filePath);
    if (def) {
      // Don't duplicate if also built-in
      if (sources.some(s => s.id === def.id)) continue;
      sources.push({
        id: def.id,
        name: def.name,
        icon: def.icon,
        version: def.version,
        author: def.author || 'local',
        description: def.description || '',
        source: 'local',
        installed: isInstalledOrHasInstance(def.id),
        configCount: Object.keys(def.config).length,
        category: def.category,
        mode: def.mode,
      });
    }
  }

  return sources;
}

/** Get a plugin definition by ID */
export function getPlugin(id: string): PluginDefinition | null {
  // Check built-in first
  if (existsSync(BUILTIN_DIR)) {
    for (const file of readdirSync(BUILTIN_DIR).filter(f => f.endsWith('.yaml') || f.endsWith('.yml'))) {
      const def = loadPluginYaml(join(BUILTIN_DIR, file));
      if (def?.id === id) return def;
    }
  }

  // Check user plugins
  const yamlPath = join(USER_PLUGINS_DIR, id, 'plugin.yaml');
  const ymlPath = join(USER_PLUGINS_DIR, id, 'plugin.yml');
  if (existsSync(yamlPath)) return loadPluginYaml(yamlPath);
  if (existsSync(ymlPath)) return loadPluginYaml(ymlPath);

  return null;
}

/** Get an installed plugin with its config */
export function getInstalledPlugin(id: string): InstalledPlugin | null {
  const configs = loadConfigs();
  const cfg = configs[id] as any;
  if (!cfg) return null;
  // Resolve definition: use source field for instances, fallback to id
  const sourceId = cfg.source || id;
  const def = getPlugin(sourceId);
  if (!def) return null;
  return {
    id,
    definition: def,
    config: cfg.config || {},
    installedAt: cfg.installedAt || new Date().toISOString(),
    enabled: cfg.enabled !== false,
    instanceName: cfg.name,
    source: sourceId !== id ? sourceId : undefined,
  };
}

/** List all installed plugins (including instances) */
export function listInstalledPlugins(): InstalledPlugin[] {
  const configs = loadConfigs();
  const installed: InstalledPlugin[] = [];
  for (const [id, cfg] of Object.entries(configs)) {
    const sourceId = (cfg as any).source || id;
    const def = getPlugin(sourceId);
    if (def) {
      installed.push({
        id,
        definition: def,
        config: (cfg as any).config || {},
        installedAt: (cfg as any).installedAt || '',
        enabled: (cfg as any).enabled !== false,
        instanceName: (cfg as any).name,
        source: sourceId !== id ? sourceId : undefined,
      });
    }
  }
  return installed;
}

/** Install a plugin or create an instance */
export function installPlugin(id: string, config: Record<string, any>, opts?: { source?: string; name?: string }): boolean {
  const sourceId = opts?.source || id;
  const def = getPlugin(sourceId);
  if (!def) return false;
  const configs = loadConfigs();
  configs[id] = {
    config,
    installedAt: new Date().toISOString(),
    enabled: true,
    ...(opts?.source ? { source: opts.source } : {}),
    ...(opts?.name ? { name: opts.name } : {}),
  };
  saveConfigs(configs);
  console.log(`[plugins] Installed: ${opts?.name || def.name} (${id}${opts?.source ? ' ← ' + opts.source : ''})`);
  return true;
}

/** Uninstall a plugin or instance */
export function uninstallPlugin(id: string): boolean {
  const configs = loadConfigs();
  if (!configs[id]) return false;
  delete configs[id];
  saveConfigs(configs);
  console.log(`[plugins] Uninstalled: ${id}`);
  return true;
}

/** Update plugin config */
export function updatePluginConfig(id: string, config: Record<string, any>): boolean {
  const configs = loadConfigs();
  if (!configs[id]) return false;
  (configs[id] as any).config = config;
  saveConfigs(configs);
  return true;
}

/** Enable/disable a plugin */
export function setPluginEnabled(id: string, enabled: boolean): boolean {
  const configs = loadConfigs();
  if (!configs[id]) return false;
  (configs[id] as any).enabled = enabled;
  saveConfigs(configs);
  return true;
}
