/**
 * Short-lived ACP probe — opens a session and reads models/modes from the response.
 */
import { spawn } from 'node:child_process';
import {
  ClientSideConnection,
  type Client,
  type NewSessionResponse,
  type ReadTextFileRequest,
  type ReadTextFileResponse,
  type WriteTextFileRequest,
  type WriteTextFileResponse,
  type CreateTerminalRequest,
  type CreateTerminalResponse,
  type RequestPermissionRequest,
  type RequestPermissionResponse,
} from '@agentclientprotocol/sdk';
import { deriveModesFromACP, deriveModelDefinitionsFromACP } from './acp-derive.js';
import { getManifestDefaultModeId, getManifestModes } from './provider-manifest.js';
import { resolveAcpSpawnArgs } from './acp-spawn.js';
import { createAcpNdJsonStream } from './acp-stream.js';
import type { ProviderModel, ProviderMode } from './provider-types.js';
import type { AgentCommand } from './agent-commands-cache.js';

const PROBE_TIMEOUT_MS = 30_000;

export interface AcpProbeResult {
  ok: boolean;
  models: ProviderModel[];
  modes: ProviderMode[];
  defaultModeId: string | null;
  commands: AgentCommand[];
  error?: string;
}

function parseSessionResponse(session: NewSessionResponse, agent: string): AcpProbeResult {
  const fallbackModes = getManifestModes(agent);
  const { modes, currentModeId } = deriveModesFromACP(
    fallbackModes,
    session.modes,
    session.configOptions,
  );
  const models = deriveModelDefinitionsFromACP(session.models, session.configOptions);

  return {
    ok: true,
    models,
    modes,
    defaultModeId: currentModeId ?? getManifestDefaultModeId(agent),
    commands: [],
  };
}

export async function probeAcpProvider(
  agent: string,
  installPath: string,
  cwd: string,
): Promise<AcpProbeResult> {
  const args = resolveAcpSpawnArgs(agent);
  if (!args) {
    return {
      ok: false,
      models: [],
      modes: getManifestModes(agent),
      defaultModeId: getManifestDefaultModeId(agent),
      commands: [],
      error: 'no ACP spawn args',
    };
  }

  const child = spawn(installPath, args, {
    cwd,
    stdio: ['pipe', 'pipe', 'pipe'],
    env: { ...process.env },
  });

  let killed = false;
  const kill = () => {
    if (!killed) {
      killed = true;
      child.kill('SIGTERM');
      setTimeout(() => child.kill('SIGKILL'), 2_000);
    }
  };

  const timeout = setTimeout(kill, PROBE_TIMEOUT_MS);

  const probedCommands: AgentCommand[] = [];

  try {
    const stream = createAcpNdJsonStream(child);

    const connection = new ClientSideConnection(
      (_agentInterface): Client => ({
        async sessionUpdate(params) {
          const update = params.update;
          if (update.sessionUpdate === 'available_commands_update') {
            for (const cmd of update.availableCommands) {
              probedCommands.push({
                name: cmd.name,
                description: cmd.description ?? undefined,
              });
            }
          }
        },
        async requestPermission(params: RequestPermissionRequest): Promise<RequestPermissionResponse> {
          const first = params.options[0];
          if (first) {
            return { outcome: { outcome: 'selected', optionId: first.optionId } };
          }
          return { outcome: { outcome: 'cancelled' } };
        },
        async readTextFile(_params: ReadTextFileRequest): Promise<ReadTextFileResponse> {
          return { content: '' };
        },
        async writeTextFile(_params: WriteTextFileRequest): Promise<WriteTextFileResponse> {
          return {};
        },
        async createTerminal(_params: CreateTerminalRequest): Promise<CreateTerminalResponse> {
          return { terminalId: 'noop' };
        },
      }),
      stream,
    );

    await connection.initialize({
      protocolVersion: 1,
      clientInfo: { name: 'boocoder-probe', version: '2.2.0' },
      clientCapabilities: {},
    });

    const session = await connection.newSession({ cwd, mcpServers: [] });
    // available_commands_update is an async session notification opencode sends
    // shortly AFTER newSession resolves — reading probedCommands synchronously
    // here races it and captures nothing. Wait briefly for the first batch, then
    // a short settle for any stragglers (capped well under PROBE_TIMEOUT_MS).
    const deadline = Date.now() + 3_000;
    while (probedCommands.length === 0 && Date.now() < deadline) {
      await new Promise((r) => setTimeout(r, 150));
    }
    if (probedCommands.length > 0) {
      await new Promise((r) => setTimeout(r, 300));
    }
    const result = parseSessionResponse(session, agent);
    result.commands = probedCommands;
    await connection.closeSession({ sessionId: session.sessionId }).catch(() => {});
    return result;
  } catch (err) {
    const message = err instanceof Error ? err.message : String(err);
    return {
      ok: false,
      models: [],
      modes: getManifestModes(agent),
      defaultModeId: getManifestDefaultModeId(agent),
      commands: probedCommands,
      error: message,
    };
  } finally {
    clearTimeout(timeout);
    kill();
    await new Promise<void>((resolve) => {
      child.on('close', resolve);
      setTimeout(resolve, 2_000);
    });
  }
}
