import { execFile } from 'node:child_process';
import { promisify } from 'node:util';
import type { LlmProvider, CompletionOptions, CompletionResult } from './types.js';
import { AcpClient } from './acp-client.js';
import type { AcpClientConfig } from './acp-client.js';

const execFileAsync = promisify(execFile);

export interface GeminiAcpConfig {
  binaryPath?: string;
  defaultModel?: string;
  requestTimeoutMs?: number;
  initTimeoutMs?: number;
  /** Idle TTL for pooled sessions in ms (default: 8 hours) */
  idleTtlMs?: number;
  /** Override for testing — passed through to AcpClient */
  spawn?: AcpClientConfig['spawn'];
}

interface PoolEntry {
  client: AcpClient;
  lastUsed: number;
  queue: Promise<void>;
}

/**
 * Gemini CLI provider using ACP (Agent Client Protocol) mode.
 *
 * Maintains a pool of persistent subprocesses keyed by model name.
 * Each model gets its own `gemini --experimental-acp` subprocess with
 * a serial request queue. Idle sessions are evicted after 8 hours.
 *
 * NOTE: Gemini ACP currently doesn't support per-session model selection,
 * so all sessions use the same model. The pool infrastructure is ready for
 * when vLLM/OpenAI providers are added (they support per-request model).
 */
export class GeminiAcpProvider implements LlmProvider {
  readonly name = 'gemini-cli';
  private pool = new Map<string, PoolEntry>();
  private binaryPath: string;
  private defaultModel: string;
  private readonly requestTimeoutMs: number;
  private readonly initTimeoutMs: number;
  private readonly idleTtlMs: number;
  private readonly spawnOverride?: AcpClientConfig['spawn'];

  constructor(config?: GeminiAcpConfig) {
    this.binaryPath = config?.binaryPath ?? 'gemini';
    this.defaultModel = config?.defaultModel ?? 'gemini-2.5-flash';
    this.requestTimeoutMs = config?.requestTimeoutMs ?? 60_000;
    this.initTimeoutMs = config?.initTimeoutMs ?? 30_000;
    this.idleTtlMs = config?.idleTtlMs ?? 8 * 60 * 60 * 1000; // 8 hours
    if (config?.spawn) this.spawnOverride = config.spawn;
  }

  async complete(options: CompletionOptions): Promise<CompletionResult> {
    const model = options.model ?? this.defaultModel;
    const entry = this.getOrCreateEntry(model);
    entry.lastUsed = Date.now();

    this.evictIdle();

    return this.enqueue(entry, () => this.doComplete(entry.client, options));
  }

  async listModels(): Promise<string[]> {
    return ['gemini-2.5-flash', 'gemini-2.5-pro', 'gemini-2.0-flash'];
  }

  async isAvailable(): Promise<boolean> {
    try {
      await execFileAsync(this.binaryPath, ['--version'], { timeout: 5000 });
      return true;
    } catch {
      return false;
    }
  }

  dispose(): void {
    for (const entry of this.pool.values()) {
      entry.client.dispose();
    }
    this.pool.clear();
  }

  /**
   * Eagerly spawn the default model's ACP subprocess so it's ready
   * for the first request (avoids 30s cold-start on health checks).
   */
  warmup(): void {
    const entry = this.getOrCreateEntry(this.defaultModel);
    // Fire-and-forget: start the subprocess initialization in the background
    entry.client.ensureReady().catch(() => {
      // Ignore errors — next request will retry
    });
  }

  /** Number of active pool entries (for testing). */
  get poolSize(): number {
    return this.pool.size;
  }

  // --- Private ---

  private getOrCreateEntry(model: string): PoolEntry {
    const existing = this.pool.get(model);
    if (existing) return existing;

    const acpConfig: AcpClientConfig = {
      binaryPath: this.binaryPath,
      model,
      requestTimeoutMs: this.requestTimeoutMs,
      initTimeoutMs: this.initTimeoutMs,
    };
    if (this.spawnOverride) acpConfig.spawn = this.spawnOverride;

    const entry: PoolEntry = {
      client: new AcpClient(acpConfig),
      lastUsed: Date.now(),
      queue: Promise.resolve(),
    };
    this.pool.set(model, entry);
    return entry;
  }

  private evictIdle(): void {
    const now = Date.now();
    for (const [model, entry] of this.pool) {
      if (now - entry.lastUsed > this.idleTtlMs) {
        entry.client.dispose();
        this.pool.delete(model);
      }
    }
  }

  private async doComplete(client: AcpClient, options: CompletionOptions): Promise<CompletionResult> {
    const prompt = options.messages
      .map((m) => {
        if (m.role === 'system') return `System: ${m.content}`;
        if (m.role === 'user') return m.content;
        if (m.role === 'assistant') return `Assistant: ${m.content}`;
        return m.content;
      })
      .join('\n\n');

    const content = await client.prompt(prompt);

    return {
      content: content.trim(),
      toolCalls: [],
      usage: { promptTokens: 0, completionTokens: 0, totalTokens: 0 },
      finishReason: 'stop',
    };
  }

  private enqueue<T>(entry: PoolEntry, fn: () => Promise<T>): Promise<T> {
    const result = new Promise<T>((resolve, reject) => {
      entry.queue = entry.queue.then(
        () => fn().then(resolve, reject),
        () => fn().then(resolve, reject),
      );
    });
    return result;
  }
}
