import { execFile } from 'child_process';

export type ExternalBackendName = 'claude_code' | 'opencode' | 'codex' | 'gemini' | 'pi_embedded';

export interface ExternalBackendRequest {
  prompt: string;
  history: Array<{ role: 'user' | 'assistant'; content: string }>;
  /**
   * Optional assembled Flynn system prompt (SOUL/AGENTS/IDENTITY/USER/TOOLS + runtime sections).
   * Backends that support system-level instructions can use this directly.
   */
  systemPrompt?: string;
}

export interface ExternalBackend {
  name: ExternalBackendName;
  process(input: ExternalBackendRequest): Promise<string>;
}

interface ExternalCliBackendOptions {
  name: ExternalBackendName;
  command: string;
  args?: string[];
  timeoutMs?: number;
}

const DEFAULT_TIMEOUT_MS = 120_000;

function buildPrompt(request: ExternalBackendRequest): string {
  const lines: string[] = [];
  for (const item of request.history) {
    if (!item.content.trim()) {
      continue;
    }
    lines.push(`${item.role.toUpperCase()}: ${item.content}`);
  }
  lines.push(`USER: ${request.prompt}`);
  return lines.join('\n\n');
}

function inferArgs(name: ExternalBackendName, prompt: string): string[] {
  if (name === 'claude_code') {
    return ['--print', prompt];
  }
  if (name === 'opencode') {
    return ['run', '--format', 'default', prompt];
  }
  return ['-p', prompt];
}

export class ExternalCliBackend implements ExternalBackend {
  readonly name: ExternalBackendName;
  private readonly command: string;
  private readonly args: string[];
  private readonly timeoutMs: number;

  constructor(options: ExternalCliBackendOptions) {
    this.name = options.name;
    this.command = options.command;
    this.args = options.args ?? [];
    this.timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
  }

  async process(input: ExternalBackendRequest): Promise<string> {
    const prompt = buildPrompt(input);
    const configuredArgs = this.args
      .map((arg) => arg.includes('{prompt}') ? arg.replaceAll('{prompt}', prompt) : arg);
    const hasPromptPlaceholder = this.args.some((arg) => arg.includes('{prompt}'));
    const args = hasPromptPlaceholder
      ? configuredArgs
      : [...configuredArgs, ...inferArgs(this.name, prompt)];

    const output = await execFileAsync(this.command, args, this.timeoutMs);
    const trimmed = output.trim();
    if (!trimmed) {
      throw new Error(`External backend "${this.name}" returned no output`);
    }
    return trimmed;
  }
}

export class ClaudeCodeBackend extends ExternalCliBackend {
  constructor(path?: string, args?: string[], timeoutMs?: number) {
    super({ name: 'claude_code', command: path ?? 'claude', args, timeoutMs });
  }
}

export class OpenCodeBackend extends ExternalCliBackend {
  constructor(path?: string, args?: string[], timeoutMs?: number) {
    super({ name: 'opencode', command: path ?? 'opencode', args, timeoutMs });
  }
}

export class CodexBackend extends ExternalCliBackend {
  constructor(path?: string, args?: string[], timeoutMs?: number) {
    super({ name: 'codex', command: path ?? 'codex', args, timeoutMs });
  }
}

export class GeminiBackend extends ExternalCliBackend {
  constructor(path?: string, args?: string[], timeoutMs?: number) {
    super({ name: 'gemini', command: path ?? 'gemini', args, timeoutMs });
  }
}

function execFileAsync(command: string, args: string[], timeoutMs: number): Promise<string> {
  return new Promise((resolve, reject) => {
    execFile(command, args, { timeout: timeoutMs, maxBuffer: 1024 * 1024 }, (error, stdout, stderr) => {
      if (error) {
        const details = [stderr, stdout]
          .map((entry) => entry.trim())
          .filter((entry) => entry.length > 0)
          .join('\n');
        reject(new Error(details ? `${error.message}\n${details}` : error.message));
        return;
      }
      resolve(stdout || '');
    });
  });
}
