'use strict';

const { spawn } = require('child_process');
const { mkdtemp, readFile, rm } = require('node:fs/promises');
const path = require('node:path');
const os = require('node:os');

// ChatGPT-subscription-authed Codex CLI restricts which models can be used.
// Surface the same small GPT-5.x roster CTM actually runs instead of the
// broader OpenAI API model catalog.
const MODELS = [
  {
    id: 'gpt-5.5',
    name: 'GPT-5.5 (via ChatGPT subscription)',
    capabilities: { vision: false, tools: false, streaming: false, reasoning: true },
  },
  {
    id: 'gpt-5.4',
    name: 'GPT-5.4 (via ChatGPT subscription)',
    capabilities: { vision: false, tools: false, streaming: false, reasoning: true },
  },
  {
    id: 'gpt-5.4-mini',
    name: 'GPT-5.4 Mini (via ChatGPT subscription)',
    capabilities: { vision: false, tools: false, streaming: false, reasoning: true },
  },
  {
    id: 'gpt-5.3-codex',
    name: 'GPT-5.3 Codex (via ChatGPT subscription)',
    capabilities: { vision: false, tools: false, streaming: false, reasoning: true },
  },
];

const CODEX_CLI_MODEL_RE = /^(gpt-5\.5|gpt-5\.4|gpt-5\.4-mini|gpt-5\.3-codex)$/i;

function _serializeMessages(messages, system) {
  const parts = [];
  if (system) parts.push(`[system]\n${system}`);
  for (const msg of messages || []) {
    if (!msg || !msg.role) continue;
    const text = typeof msg.content === 'string'
      ? msg.content
      : Array.isArray(msg.content)
        ? msg.content
            .filter((b) => b && b.type === 'text' && typeof b.text === 'string')
            .map((b) => b.text)
            .join('\n')
        : '';
    if (!text) continue;
    parts.push(`[${msg.role}]\n${text}`);
  }
  return parts.join('\n\n');
}

async function _spawnCodex(prompt, { model, signal, timeoutMs = 300000, cwd } = {}) {
  // Codex writes its session journal to ~/.codex/. We pass --ephemeral to
  // suppress that. We also need to capture the final assistant message
  // somehow — `--output-last-message <FILE>` is the cleanest path. Use a
  // tmp file and read it back after exit.
  const dir = await mkdtemp(path.join(os.tmpdir(), 'walle-codex-'));
  const outFile = path.join(dir, 'reply.txt');

  return new Promise((resolve, reject) => {
    if (signal && signal.aborted) {
      rm(dir, { recursive: true, force: true }).catch(() => {});
      return reject(new Error('Aborted'));
    }
    const args = [
      'exec',
      '--skip-git-repo-check',
      '--ephemeral',
      '--output-last-message', outFile,
    ];
    // ChatGPT-authed codex restricts which models work. Only forward `-m` for
    // known-subscription-compatible
    // models — otherwise let codex pick its own default. The wall-e caller's
    // model preference is best-effort; the trade-off for subscription billing.
    if (model && CODEX_CLI_MODEL_RE.test(model)) {
      args.push('-m', model);
    }
    args.push(prompt);

    // Force ChatGPT auth path — strip OPENAI_API_KEY so codex doesn't
    // silently fall back to API-key billing when env exposes one.
    const childEnv = { ...process.env };
    delete childEnv.OPENAI_API_KEY;

    let proc;
    try {
      proc = spawn('codex', args, {
        stdio: ['ignore', 'pipe', 'pipe'],
        env: childEnv,
        cwd: cwd || process.cwd(),
        timeout: timeoutMs,
      });
    } catch (err) {
      rm(dir, { recursive: true, force: true }).catch(() => {});
      return reject(new Error(`codex CLI spawn failed: ${err.message}`));
    }

    let stdout = '';
    let stderr = '';
    let aborted = false;

    proc.stdout.on('data', (d) => { stdout += d.toString(); });
    proc.stderr.on('data', (d) => { stderr += d.toString(); });
    proc.on('error', (err) => {
      rm(dir, { recursive: true, force: true }).catch(() => {});
      if (err.code === 'ENOENT') {
        reject(new Error('codex CLI not on PATH. Install Codex CLI from https://chatgpt.com/codex or set OPENAI_API_KEY to use API-key auth instead.'));
      } else {
        reject(new Error(`codex CLI error: ${err.message}`));
      }
    });
    proc.on('close', async (code) => {
      if (aborted) return;
      let reply = '';
      try {
        reply = (await readFile(outFile, 'utf8')).trim();
      } catch {
        // outFile not written — codex errored before producing a reply.
      }
      await rm(dir, { recursive: true, force: true }).catch(() => {});
      if (code !== 0 && !reply) {
        // Surface the API error JSON if codex printed one to stderr/stdout.
        const apiErrMatch = (stderr + '\n' + stdout).match(/ERROR:\s*(\{[^\n]+\})/);
        const apiErr = apiErrMatch ? apiErrMatch[1] : (stderr || stdout).slice(-500);
        return reject(new Error(`codex exited ${code}: ${apiErr}`));
      }
      resolve({ reply, stdout, stderr });
    });

    if (signal) {
      const onAbort = () => {
        aborted = true;
        try { proc.kill('SIGTERM'); } catch {}
        rm(dir, { recursive: true, force: true }).catch(() => {});
        reject(new Error('Aborted'));
      };
      signal.addEventListener('abort', onAbort, { once: true });
    }
  });
}

/**
 * Create a Codex CLI–backed LLM provider. Wraps `codex exec` so the user's
 * ChatGPT subscription pays for tokens instead of an OpenAI API key.
 *
 * Limitations (v1):
 *   - No tool use. Codex runs its own internal sandbox + tools but doesn't
 *     surface OpenAI-format tool_calls back to the caller. Callers passing
 *     `tools` get `toolCalls: []` — they should fall through to a non-CLI
 *     path or accept text-only output.
 *   - No streaming. We capture the final assistant message via
 *     `--output-last-message` and return it whole.
 *   - Model whitelist: ChatGPT-authed codex blocks some models (e.g. gpt-5).
 *     Default model uses codex's own choice; explicit -m must be one the
 *     subscription tier supports.
 */
function createCodexCliProvider(config = {}) {
  return {
    type: 'codex-cli',
    capabilities: { vision: false, tools: false, streaming: false, reasoning: true },

    async chat({ model, messages, tools, system, signal, cwd } = {}) {
      const prompt = _serializeMessages(messages, system);
      if (!prompt) {
        return {
          content: '',
          toolCalls: [],
          stopReason: 'end_turn',
          usage: { input: 0, output: 0 },
          latencyMs: 0,
          model: model || '',
          provider: 'codex-cli',
          raw: null,
        };
      }
      const start = Date.now();
      const forwardedModel = model && CODEX_CLI_MODEL_RE.test(model) ? model : undefined;
      const { reply, stdout } = await _spawnCodex(prompt, {
        model: forwardedModel,
        signal,
        timeoutMs: config.timeoutMs,
        cwd: cwd || config.cwd,
      });
      const latencyMs = Date.now() - start;

      // codex exec prints "tokens used\nN" near end of stdout — best-effort
      // parse, no usage if absent.
      const tokensMatch = stdout.match(/tokens used\s*\n\s*([\d,]+)/i);
      const totalTokens = tokensMatch ? parseInt(tokensMatch[1].replace(/,/g, ''), 10) : 0;

      // tools requested but unsupported — interface-parity-only.
      void tools;

      return {
        content: reply || null,
        toolCalls: [],
        stopReason: 'end_turn',
        usage: totalTokens > 0 ? { input: 0, output: totalTokens } : undefined,
        latencyMs,
        model: forwardedModel || 'gpt-5.5',
        provider: 'codex-cli',
        raw: { reply, totalTokens },
      };
    },

    async listModels() {
      return [...MODELS];
    },
  };
}

function createCodexCliFromEnv() {
  return createCodexCliProvider({});
}

module.exports = {
  createCodexCliProvider,
  createCodexCliFromEnv,
};
