/**
 * Connector Registry — load, install, uninstall, list connectors.
 *
 * Manifests live in `<dataDir>/connectors/<id>/manifest.yaml`, fetched
 * by lib/connectors/sync.ts from the remote forge-connectors repo.
 * There are no built-in connector manifests — a fresh install has zero
 * connectors until the first registry sync completes.
 *
 * Per-instance config (settings filled in by the user — host URL,
 * PAT, etc.) lives in `<dataDir>/connector-configs.json`. Secret
 * fields are encrypted at rest with AES-256-GCM via lib/crypto.ts.
 */

import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, writeFileSync, statSync } from 'node:fs';
import { join } from 'node:path';
import YAML from 'yaml';
import { getDataDir } from '../dirs';
import { encryptSecret, decryptSecret, isEncrypted } from '../crypto';
import type {
  ConnectorDefinition,
  ConnectorEntry,
  ConnectorFieldSchema,
} from './types';

// ─── Paths ────────────────────────────────────────────────

function connectorsDir(): string {
  return join(getDataDir(), 'connectors');
}

function manifestPath(id: string): string {
  return join(connectorsDir(), id, 'manifest.yaml');
}

function configsFile(): string {
  return join(getDataDir(), 'connector-configs.json');
}

function ensureDir(p: string): void {
  if (!existsSync(p)) mkdirSync(p, { recursive: true, mode: 0o700 });
}

// ─── Manifest loading ─────────────────────────────────────

function parseManifest(raw: string): ConnectorDefinition | null {
  try {
    const def = YAML.parse(raw) as ConnectorDefinition;
    if (!def?.id || !def?.name) return null;
    if (!def.tools && !def.connectors?.length) return null;
    if (!def.version) def.version = '0.0.0';
    if (!def.icon) def.icon = '🔌';
    return def;
  } catch {
    return null;
  }
}

/** Read the manifest for a single connector id; returns null if missing. */
export function getConnector(id: string): ConnectorDefinition | null {
  const p = manifestPath(id);
  if (!existsSync(p)) return null;
  try {
    return parseManifest(readFileSync(p, 'utf-8'));
  } catch {
    return null;
  }
}

/** List installed connector ids (presence of `<dir>/manifest.yaml`). */
export function listConnectorIds(): string[] {
  const dir = connectorsDir();
  if (!existsSync(dir)) return [];
  const out: string[] = [];
  for (const name of readdirSync(dir)) {
    try {
      const sub = join(dir, name);
      if (!statSync(sub).isDirectory()) continue;
      if (existsSync(join(sub, 'manifest.yaml'))) out.push(name);
    } catch {}
  }
  return out;
}

/**
 * Normalize a connector to its entry list.
 *   1:1 shape (top-level `tools`) → synthesizes one entry with id === connector id.
 *   1:N shape (`connectors[]`) → returned as-is.
 */
export function getConnectorEntries(def: ConnectorDefinition): ConnectorEntry[] {
  if (def.connectors?.length) return def.connectors;
  if (def.tools) {
    return [
      {
        id: def.id,
        tools: def.tools,
        settings: def.settings,
        host_match: def.host_match,
        login_redirect: def.login_redirect,
        runner: def.runner,
      },
    ];
  }
  return [];
}

// ─── Config store ─────────────────────────────────────────

/**
 * Wire shape of `<dataDir>/connector-configs.json`:
 *   { "<id>": { config, installed_version, enabled } }
 */
interface ConnectorConfigRow {
  config: Record<string, unknown>;
  installed_version: string;
  enabled?: boolean;
}

type ConfigStore = Record<string, ConnectorConfigRow>;

function getSecretFieldNames(def: ConnectorDefinition | null): string[] {
  if (!def) return [];
  const fields: Array<[string, ConnectorFieldSchema]> = [];
  if (def.settings) fields.push(...Object.entries(def.settings));
  for (const entry of def.connectors || []) {
    if (entry.settings) fields.push(...Object.entries(entry.settings));
  }
  return fields
    .filter(([, schema]) => {
      const t = String((schema as any)?.type || '');
      return t === 'secret' || t === 'password';
    })
    .map(([name]) => name);
}

function loadStoreRaw(): ConfigStore {
  const p = configsFile();
  if (!existsSync(p)) return {};
  try {
    return JSON.parse(readFileSync(p, 'utf-8')) as ConfigStore;
  } catch {
    return {};
  }
}

function loadStore(): ConfigStore {
  const store = loadStoreRaw();
  // Decrypt secret fields lazily — readers expect plaintext.
  for (const [id, row] of Object.entries(store)) {
    if (!row?.config) continue;
    const def = getConnector(id);
    const secrets = getSecretFieldNames(def);
    if (!secrets.length) continue;
    for (const key of secrets) {
      const v = row.config[key];
      if (typeof v === 'string' && isEncrypted(v)) {
        try { row.config[key] = decryptSecret(v); }
        catch (err) {
          console.warn(`[connectors] failed to decrypt ${id}.${key}`, err);
        }
      }
    }
  }
  return store;
}

function saveStore(store: ConfigStore): void {
  ensureDir(getDataDir());
  // Deep-copy so we can encrypt without mutating caller-visible values.
  const out: ConfigStore = {};
  for (const [id, row] of Object.entries(store)) {
    const def = getConnector(id);
    const secrets = new Set(getSecretFieldNames(def));
    const encryptedConfig: Record<string, unknown> = {};
    for (const [k, v] of Object.entries(row.config || {})) {
      if (secrets.has(k) && typeof v === 'string' && v.length > 0 && !isEncrypted(v)) {
        try { encryptedConfig[k] = encryptSecret(v); }
        catch { encryptedConfig[k] = v; }
      } else {
        encryptedConfig[k] = v;
      }
    }
    out[id] = {
      config: encryptedConfig,
      installed_version: row.installed_version,
      enabled: row.enabled !== false,
    };
  }
  writeFileSync(configsFile(), JSON.stringify(out, null, 2), { mode: 0o600 });
}

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

export interface InstalledConnector {
  definition: ConnectorDefinition;
  config: Record<string, unknown>;
  installed_version: string;
  enabled: boolean;
}

export function isInstalled(id: string): boolean {
  return existsSync(manifestPath(id)) && !!loadStoreRaw()[id];
}

/**
 * IDs that have a config row but no manifest on disk yet. Happens
 * during the migration window after upgrading from pre-v0.9 Forge —
 * the row is moved across, but the manifest can only be fetched
 * from the remote registry. Sync uses this to know what to pull.
 */
export function listConfigOnlyIds(): string[] {
  const store = loadStoreRaw();
  const out: string[] = [];
  for (const id of Object.keys(store)) {
    if (!existsSync(manifestPath(id))) out.push(id);
  }
  return out;
}

export function listInstalledConnectors(): InstalledConnector[] {
  const store = loadStore();
  const out: InstalledConnector[] = [];
  for (const id of listConnectorIds()) {
    const def = getConnector(id);
    if (!def) continue;
    const row = store[id];
    if (!row) continue; // manifest on disk but no config row → not installed
    out.push({
      definition: def,
      config: row.config || {},
      installed_version: row.installed_version || def.version,
      enabled: row.enabled !== false,
    });
  }
  return out;
}

export function getInstalledConnector(id: string): InstalledConnector | null {
  const def = getConnector(id);
  if (!def) return null;
  const row = loadStore()[id];
  if (!row) return null;
  return {
    definition: def,
    config: row.config || {},
    installed_version: row.installed_version || def.version,
    enabled: row.enabled !== false,
  };
}

/**
 * Install (or upgrade) a connector by writing its manifest YAML and
 * recording the version in the config store. Existing user config is
 * preserved across upgrades.
 */
export function installConnector(id: string, manifestYaml: string): boolean {
  const def = parseManifest(manifestYaml);
  if (!def || def.id !== id) return false;
  const dir = join(connectorsDir(), id);
  ensureDir(dir);
  writeFileSync(join(dir, 'manifest.yaml'), manifestYaml, { mode: 0o600 });

  const store = loadStore();
  const prev = store[id]?.config || {};
  store[id] = {
    config: prev,
    installed_version: def.version,
    enabled: true,
  };
  saveStore(store);
  return true;
}

/**
 * Uninstall a connector.
 *   keepConfig=true (default): drop the manifest but retain settings/PAT
 *     so re-installing later restores them.
 *   keepConfig=false: also delete the config row.
 */
export function uninstallConnector(id: string, keepConfig = true): boolean {
  const dir = join(connectorsDir(), id);
  if (existsSync(dir)) {
    try { rmSync(dir, { recursive: true, force: true }); }
    catch { return false; }
  }
  if (!keepConfig) {
    const store = loadStore();
    delete store[id];
    saveStore(store);
  } else {
    // Mark disabled so the runtime doesn't try to use a now-missing manifest.
    const store = loadStore();
    if (store[id]) {
      store[id].enabled = false;
      saveStore(store);
    }
  }
  return true;
}

export function setConnectorConfig(
  id: string,
  config: Record<string, unknown>,
): boolean {
  const store = loadStore();
  const row = store[id];
  if (!row) {
    // Allow setting config before install if a manifest already exists
    // — supports auto-install-then-configure flows in the UI.
    const def = getConnector(id);
    if (!def) return false;
    store[id] = { config, installed_version: def.version, enabled: true };
  } else {
    store[id] = { ...row, config };
  }
  saveStore(store);
  return true;
}

export function setConnectorEnabled(id: string, enabled: boolean): boolean {
  const store = loadStore();
  if (!store[id]) return false;
  store[id].enabled = enabled;
  saveStore(store);
  return true;
}

// Re-export for callers (matches plugin-registry surface so it's familiar).
export { getSecretFieldNames };
