import type { Sql } from '../db.js';
import type { FastifyBaseLogger } from 'fastify';
import { execFile as execFileCb } from 'node:child_process';
import { promisify } from 'node:util';
import { PROVIDERS_BY_NAME } from './provider-registry.js';
import { resolveAcpProbeBinaries } from './acp-spawn.js';
import { clearProviderSnapshotCache, fetchRegistryModels, prefixBoocodeLocalModels } from './provider-snapshot.js';
import { readQwenSettingsModels } from './qwen-settings.js';
import { loadConfig } from '../config.js';
import { loadProviderConfig } from './provider-config-registry.js';

const execFile = promisify(execFileCb);

// `which` via execFile (no shell) — the binary name can come from the config
// file (custom ACP entries), so avoid interpolating it into a shell string.
async function whichBinary(bin: string): Promise<string | null> {
  try {
    const { stdout } = await execFile('which', [bin], { timeout: 10_000 });
    const path = stdout.trim();
    return path || null;
  } catch {
    return null;
  }
}

async function resolveInstallPath(agentName: string): Promise<string | null> {
  const candidates = resolveAcpProbeBinaries(agentName);
  for (const bin of candidates) {
    const path = await whichBinary(bin);
    if (path) return path;
  }
  return null;
}

async function detectAcpSupport(agentName: string, installPath: string): Promise<boolean> {
  const transport = PROVIDERS_BY_NAME.get(agentName)?.transport;
  if (transport !== 'acp') return false;

  if (agentName === 'qwen') {
    try {
      const { stdout } = await execFile(installPath, ['--help'], { timeout: 10_000 });
      return stdout.includes('--acp');
    } catch {
      return false;
    }
  }

  if (agentName === 'reasonix') {
    try {
      await execFile(installPath, ['acp', '--help'], { timeout: 10_000 });
      return true;
    } catch (err) {
      const out =
        err && typeof err === 'object' && 'stdout' in err
          ? String((err as { stdout?: unknown }).stdout ?? '')
          : '';
      const errOut =
        err && typeof err === 'object' && 'stderr' in err
          ? String((err as { stderr?: unknown }).stderr ?? '')
          : '';
      return `${out}\n${errOut}`.includes('Usage of acp:');
    }
  }

  try {
    await execFile(installPath, ['acp', '--help'], { timeout: 10_000 });
    return true;
  } catch {
    return false;
  }
}

/**
 * Probe for available agents on the HOST.
 *
 * v2.3: iterates the resolved provider registry (built-ins + config-backed
 * custom ACP entries) rather than the hardcoded `PROBED_AGENT_NAMES`. Native
 * boocode is not probed; disabled providers are skipped (their `available_agents`
 * row is kept, not deleted). `enabled` is read from the in-memory registry only —
 * no DB column in Phase 1 (design.md §3.3).
 */
export async function probeAgents(sql: Sql, log: FastifyBaseLogger): Promise<void> {
  clearProviderSnapshotCache();
  log.info('agent-probe: scanning for known agents');

  const registry = loadProviderConfig(loadConfig().CODER_PROVIDERS_PATH);

  for (const resolved of registry.values()) {
    const agentName = resolved.id;

    // Native boocode is not a probed host agent.
    if (resolved.transport === 'native') continue;

    // Disabled providers: skip the probe, keep any existing row.
    if (!resolved.enabled) {
      log.info({ agent: agentName }, 'agent-probe: skipping disabled provider');
      continue;
    }

    try {
      // Custom ACP entries resolve their binary from command[0]; built-ins use
      // the per-agent probe binaries.
      const installPath = resolved.isCustomAcp && resolved.launchCommand
        ? await whichBinary(resolved.launchCommand[0])
        : await resolveInstallPath(agentName);
      if (!installPath) continue;

      let version: string | null = null;
      try {
        const { stdout: verOut } = await execFile(installPath, ['--version'], { timeout: 15_000 });
        version = verOut.trim().slice(0, 100);
      } catch {
        /* optional */
      }

      // Custom ACP entries are ACP by declaration; built-ins detect support.
      let supportsAcp: boolean;
      if (resolved.isCustomAcp) {
        supportsAcp = true;
      } else {
        supportsAcp = resolved.transport === 'acp';
        if (supportsAcp) {
          supportsAcp = await detectAcpSupport(agentName, installPath);
        }
      }

      let models: Array<{ id: string; label: string }> = [];
      if (!resolved.isCustomAcp) {
        const providerDef = PROVIDERS_BY_NAME.get(agentName);
        if (providerDef?.modelSource === 'static' && providerDef.staticModels) {
          models = providerDef.staticModels;
        }
        if (agentName === 'qwen') {
          models = await readQwenSettingsModels();
        }
        if (providerDef?.mergeLlamaSwap) {
          try {
            // W7: use composite registry models with boocode-local prefix (D-6)
            // instead of llama-swap-prefixed ids.
            const registryModels = await fetchRegistryModels();
            models = [...models, ...prefixBoocodeLocalModels(registryModels)];
          } catch (err) {
            log.warn({ agent: agentName, err: err instanceof Error ? err.message : String(err) }, 'agent-probe: registry model fetch failed (non-fatal)');
          }
        }
      }

      const label = resolved.configLabel ?? resolved.label;
      const transport = resolved.isCustomAcp
        ? 'acp'
        : resolved.transport === 'acp' && !supportsAcp
          ? 'pty'
          : (resolved.transport ?? 'pty');

      await sql`
        INSERT INTO available_agents (name, install_path, version, supports_acp, last_probed_at, models, label, transport)
        VALUES (${agentName}, ${installPath}, ${version}, ${supportsAcp}, clock_timestamp(), ${sql.json(models as never)}, ${label}, ${transport})
        ON CONFLICT (name) DO UPDATE SET
          install_path = EXCLUDED.install_path,
          version = EXCLUDED.version,
          supports_acp = EXCLUDED.supports_acp,
          last_probed_at = EXCLUDED.last_probed_at,
          models = EXCLUDED.models,
          label = EXCLUDED.label,
          transport = EXCLUDED.transport
      `;
      log.info({ agent: agentName, version, installPath, supportsAcp, modelCount: models.length }, 'agent-probe: found');
    } catch (err) {
      const msg = err instanceof Error ? err.message : String(err);
      log.debug({ agent: agentName, err: msg }, 'agent-probe: not found');
    }
  }

  log.info('agent-probe: scan complete');
}
