/**
 * Agent Registry — manages available agents and provides adapters.
 * Agents coexist (not mutually exclusive). Each entry point can select any agent.
 */

import { execFileSync, execSync } from 'node:child_process';
import { loadSettings } from '../settings';

// Cache absolute-path resolution per command — `which <cmd>` is cheap
// but resolveTerminalLaunch is on the hot path for every chat tool call.
const _whichCache = new Map<string, string>();
function resolveAbsoluteBin(cmd: string): string {
  if (!cmd || cmd.startsWith('/')) return cmd;
  const cached = _whichCache.get(cmd);
  if (cached !== undefined) return cached;
  try {
    const out = execSync(`which ${cmd}`, { encoding: 'utf-8', timeout: 3000, stdio: ['pipe', 'pipe', 'pipe'] }).trim();
    if (out) {
      _whichCache.set(cmd, out);
      return out;
    }
    // `which` exited 0 but printed nothing — don't poison cache, retry next call.
    return cmd;
  } catch (e) {
    // Restart-window contention can blow past the 3s timeout. Caching the
    // failure here would pin bare `cmd` for the rest of the process — let
    // the next call retry instead.
    console.warn(`[agents] which ${cmd} failed:`, (e as Error).message);
    return cmd;
  }
}
import type { AgentAdapter, AgentConfig, AgentId } from './types';
import { createClaudeAdapter, detectClaude } from './claude-adapter';
import { createGenericAdapter, detectAgent } from './generic-adapter';

export type { AgentAdapter, AgentConfig, AgentId } from './types';

// Module-level cache
const adapterCache = new Map<AgentId, AgentAdapter>();

/** Probe the installed codex CLI's `--help` and return whichever
 *  "skip all approvals / sandbox" flag it actually accepts.
 *
 *  - Modern codex: `--dangerously-bypass-approvals-and-sandbox`
 *  - Legacy codex: `--full-auto`
 *
 *  If neither shows up, default to the modern flag so users can still override
 *  the value in settings.yaml if a future CLI renames it again.
 */
const codexSkipFlagCache = new Map<string, string>();
export function probeCodexSkipFlag(binary = 'codex'): string {
  const cached = codexSkipFlagCache.get(binary);
  if (cached) return cached;

  let help = '';
  try {
    help = execFileSync(binary, ['--help'], { encoding: 'utf-8', timeout: 3000, stdio: ['pipe', 'pipe', 'pipe'] });
  } catch {
    // `--help` might exit non-zero on some versions — also try without args.
    try { help = execFileSync(binary, [], { encoding: 'utf-8', timeout: 3000, stdio: ['pipe', 'pipe', 'pipe'] }); } catch {}
  }

  const flag = help.includes('--dangerously-bypass-approvals-and-sandbox')
    ? '--dangerously-bypass-approvals-and-sandbox'
    : help.includes('--full-auto')
      ? '--full-auto'
      : '--dangerously-bypass-approvals-and-sandbox';

  codexSkipFlagCache.set(binary, flag);
  return flag;
}

/** Get all configured agents */
export function listAgents(): AgentConfig[] {
  const settings = loadSettings();
  const agents: AgentConfig[] = [];

  // Claude (always check — primary agent)
  const claudeConfig = settings.agents?.claude;
  const claude = detectClaude(claudeConfig?.path || settings.claudePath);
  if (claude) {
    agents.push({ ...claude, enabled: claudeConfig?.enabled !== false, detected: true, skipPermissionsFlag: claudeConfig?.skipPermissionsFlag || '--dangerously-skip-permissions', cliType: 'claude-code' } as any);
  }

  // Codex
  const codexConfig = settings.agents?.codex;
  const codex = detectAgent('codex', 'OpenAI Codex', codexConfig?.path || 'codex', ['exec']);
  if (codex) {
    codex.capabilities.requiresTTY = false; // exec subcommand is non-interactive
    agents.push({ ...codex, enabled: codexConfig?.enabled !== false, detected: true, skipPermissionsFlag: codexConfig?.skipPermissionsFlag || probeCodexSkipFlag(codexConfig?.path || 'codex'), cliType: 'codex' } as any);
  }

  // Aider
  const aiderConfig = settings.agents?.aider;
  const aider = detectAgent('aider', 'Aider', aiderConfig?.path || 'aider', ['--message']);
  if (aider) {
    agents.push({ ...aider, enabled: aiderConfig?.enabled !== false, detected: true, skipPermissionsFlag: aiderConfig?.skipPermissionsFlag || '--yes', cliType: 'aider' } as any);
  }

  // User-named CLI agents from settings (post-migration shape: tool field)
  if (settings.agents) {
    for (const [id, cfg] of Object.entries(settings.agents)) {
      if (['claude', 'codex', 'aider'].includes(id)) continue;  // builtins handled above

      // Post-migration: tool field identifies which CLI to wrap.
      // Find the matching builtin we already detected and clone its
      // adapter shape, overriding model / env / etc. from this entry.
      if (cfg.tool) {
        const baseAgent = agents.find(a => a.id === cfg.tool);
        agents.push({
          ...(baseAgent || { type: 'generic' as const, capabilities: { supportsResume: false, supportsStreamJson: false, supportsModel: false, supportsSkipPermissions: false, hasSessionFiles: false, requiresTTY: false } }),
          id,
          name: cfg.name || id,
          path: baseAgent?.path || '',
          enabled: cfg.enabled !== false,
          isProfile: true,
          backendType: 'cli',
          model: cfg.models?.task,
          skipPermissionsFlag: cfg.skipPermissionsFlag || baseAgent?.skipPermissionsFlag,
          env: cfg.env,
          cliType: (baseAgent as any)?.cliType || 'generic',
        } as any);
        continue;
      }

      // === Legacy fallback (pre-migration / un-migrated) ===
      // type: 'api' — already moved to settings.apiProfiles by migration,
      // but keep this branch for the migration window in case someone hits
      // it before migrateAgentsFlatten runs. After 1-2 versions, remove.
      if (cfg.type === 'api') continue;   // silently skip — apiProfiles is canonical now

      // CLI profile via base (legacy) — same path as tool branch above
      if (cfg.base) {
        const baseAgent = agents.find(a => a.id === cfg.base);
        agents.push({
          ...(baseAgent || { type: 'generic' as const, capabilities: { supportsResume: false, supportsStreamJson: false, supportsModel: false, supportsSkipPermissions: false, hasSessionFiles: false, requiresTTY: false } }),
          id,
          name: cfg.name || id,
          path: baseAgent?.path || '',
          enabled: cfg.enabled !== false,
          base: cfg.base,
          isProfile: true,
          backendType: 'cli',
          model: cfg.models?.task,
          skipPermissionsFlag: cfg.skipPermissionsFlag || baseAgent?.skipPermissionsFlag,
          env: cfg.env,
          cliType: cfg.cliType || (baseAgent as any)?.cliType || 'generic',
        } as any);
        continue;
      }

      // Custom agent — explicit path
      if (!cfg.path) continue;
      const flags = cfg.taskFlags ? cfg.taskFlags.split(/\s+/).filter(Boolean) : cfg.flags;
      const detected = detectAgent(id, cfg.name || id, cfg.path, flags);
      agents.push({
        ...(detected || {
          id, name: cfg.name || id, path: cfg.path, type: 'generic' as const, flags,
          capabilities: { supportsResume: false, supportsStreamJson: false, supportsModel: false, supportsSkipPermissions: false, hasSessionFiles: false, requiresTTY: !!cfg.requiresTTY },
        }),
        flags,
        enabled: cfg.enabled !== false,
        detected: !!detected,
        cliType: cfg.cliType,
      } as any);
    }
  }

  return agents;
}

/** Get the default agent ID */
export function getDefaultAgentId(): AgentId {
  const settings = loadSettings();
  return settings.defaultAgent || 'claude';
}

/** Get an agent adapter by ID (falls back to default). For profiles, returns base agent's adapter. */
export function getAgent(id?: AgentId): AgentAdapter {
  const agentId = id || getDefaultAgentId();

  // Return cached adapter
  if (adapterCache.has(agentId)) return adapterCache.get(agentId)!;

  const agents = listAgents();
  const config = agents.find(a => a.id === agentId && a.enabled);

  // Profile with base → get base agent's adapter
  if (config?.base) {
    const baseAdapter = getAgent(config.base);
    // Wrap adapter with profile's model override
    const profileAdapter: AgentAdapter = {
      ...baseAdapter,
      id: agentId,
      config: { ...baseAdapter.config, ...config, id: agentId },
    };
    adapterCache.set(agentId, profileAdapter);
    return profileAdapter;
  }

  if (!config) {
    // If specifically requested agent not found, only fallback for 'claude' (default)
    if (agentId === 'claude' || agentId === getDefaultAgentId()) {
      const fallback = detectClaude() || {
        id: 'claude', name: 'Claude Code', path: 'claude', enabled: true,
        type: 'claude-code' as const,
        capabilities: { supportsResume: true, supportsStreamJson: true, supportsModel: true, supportsSkipPermissions: true, hasSessionFiles: true, requiresTTY: false },
      };
      const adapter = createClaudeAdapter(fallback);
      adapterCache.set(agentId, adapter);
      return adapter;
    }
    // Non-default agent not found — create generic with the ID as path (will fail if not installed)
    const notFound: AgentConfig = {
      id: agentId, name: agentId, path: agentId, enabled: true, type: 'generic',
      capabilities: { supportsResume: false, supportsStreamJson: false, supportsModel: false, supportsSkipPermissions: false, hasSessionFiles: false, requiresTTY: false },
    };
    const adapter = createGenericAdapter(notFound);
    adapterCache.set(agentId, adapter);
    return adapter;
  }

  const adapter = config.type === 'claude-code'
    ? createClaudeAdapter(config)
    : createGenericAdapter(config);

  adapterCache.set(agentId, adapter);
  return adapter;
}

/** Clear adapter cache (call after settings change) */
export function clearAgentCache(): void {
  adapterCache.clear();
  // Also drop the `which <bin>` resolution cache. Without this, a failed
  // resolution during the restart window stayed in cache and every later
  // terminal-launch returned bare `claude`.
  _whichCache.clear();
}

/** Auto-detect all available agents (called on startup) */
export function autoDetectAgents(): AgentConfig[] {
  const detected: AgentConfig[] = [];

  const claude = detectClaude();
  if (claude) detected.push(claude);

  const codex = detectAgent('codex', 'OpenAI Codex', 'codex');
  if (codex) detected.push(codex);

  const aider = detectAgent('aider', 'Aider', 'aider', ['--message']);
  if (aider) detected.push(aider);

  if (detected.length > 0) {
    console.log(`[agents] Detected: ${detected.map(a => a.name).join(', ')}`);
  }

  return detected;
}

/** Resolve terminal launch info for an agent — used by both VibeCoding and Workspace */
export interface TerminalLaunchInfo {
  cliCmd: string;              // actual binary: claude, codex, aider
  cliType: string;             // claude-code, codex, aider, generic
  supportsSession: boolean;    // has session files to resume
  resumeFlag: string;          // -c, --resume, etc.
  skipPermissionsFlag?: string; // --dangerously-skip-permissions (claude), version-detected for codex, --yes (aider)
  env?: Record<string, string>; // profile env vars to export
  model?: string;              // profile model override (--model flag)
}

export function resolveTerminalLaunch(agentId?: string, scene: 'terminal' | 'task' | 'telegram' | 'help' | 'mobile' = 'terminal'): TerminalLaunchInfo {
  const settings = loadSettings();
  const agentCfg = settings.agents?.[agentId || 'claude'] || {};
  // Resolve cliType: own cliType → base agent's cliType → base agent name guessing → agentId name guessing.
  // Wizard-generated agents use `tool: claude` instead of `base: claude` —
  // listAgents handles both, but the old resolveTerminalLaunch only looked
  // at `base`, so wizard-installed forti-k2 / forti-coder were missing the
  // inherited path and falling back to bare `claude`.
  const baseId = agentCfg.base || (agentCfg.tool && agentCfg.tool !== agentId ? agentCfg.tool : undefined);
  const baseCfg = baseId ? (settings.agents?.[baseId] || {}) : {};
  const cliType = agentCfg.cliType || baseCfg.cliType
    || (baseId === 'codex' ? 'codex' : baseId === 'aider' ? 'aider' : undefined)
    || (agentId === 'codex' ? 'codex' : agentId === 'aider' ? 'aider' : 'claude-code');

  // Determine CLI command and capabilities from cliType
  const cliMap: Record<string, { cmd: string; session: boolean; resume: string; skip: string }> = {
    'claude-code': { cmd: 'claude', session: true, resume: '--resume', skip: '--dangerously-skip-permissions' },
    'codex': { cmd: 'codex', session: false, resume: '', skip: '' },
    'aider': { cmd: 'aider', session: false, resume: '', skip: '--yes' },
    'generic': { cmd: agentCfg.path || agentId || 'claude', session: false, resume: '', skip: '' },
  };
  const cli = cliMap[cliType] || cliMap['claude-code'];
  const skipPermissionsFlag = agentCfg.skipPermissionsFlag
    || (cliType === 'codex' ? probeCodexSkipFlag(agentCfg.path || 'codex') : cli.skip)
    || undefined;

  // Resolve env/model from this agent's per-use models sub-table.
  // 顶层 model 字段已在 0.10 起被 migrate.ts 清理,这里按 scene 读 models[scene]
  // (默认 terminal;Help 传 'help' 等)。models[scene] === 'default' → 不加 --model。
  let env: Record<string, string> | undefined;
  let model: string | undefined;
  if (agentCfg.base || agentCfg.env || agentCfg.models) {
    if (agentCfg.env) env = { ...agentCfg.env };
    model = agentCfg.models?.[scene];
    if (model === 'default') model = undefined; // 'default' = 不加 --model
  } else if (agentCfg.profile) {
    const profileCfg = settings.agents?.[agentCfg.profile];
    if (profileCfg) {
      if (profileCfg.env) env = { ...profileCfg.env };
      model = profileCfg.models?.[scene];
      if (model === 'default') model = undefined;
    }
  }

  return {
    // Prefer the user-configured absolute binary path over the bare command
    // name. A bare `claude` resolves via the tmux pane's PATH — non-login shell
    // + forge-server's inherited PATH can pick a different/older install than
    // the user's interactive shell, and an old claude crashes resuming a
    // session a newer claude wrote. An absolute path bypasses PATH entirely.
    // Derived agents (base: 'claude' with no own path — e.g. forti-k2) must
    // inherit the base agent's resolved path, otherwise launching them falls
    // back to bare `claude` even though the base IS configured.
    // If the result is still a bare command name (e.g. legacy settings have
    // `path: 'claude'` because old detectClaude stored the input literally),
    // resolve to an absolute path via `which` so tmux pane PATH can't shadow
    // it with a stale global install.
    cliCmd: resolveAbsoluteBin(agentCfg.path || baseCfg.path || cli.cmd),
    cliType,
    supportsSession: cli.session,
    resumeFlag: agentCfg.resumeFlag || cli.resume,
    skipPermissionsFlag,
    env,
    model,
  };
}
