import { execFile, spawn } from "node:child_process";
import { mkdtemp, readFile, rm } from "node:fs/promises";
import { existsSync, readFileSync } from "node:fs";
import { homedir, tmpdir } from "node:os";
import path from "node:path";
import { promisify } from "node:util";
import { attachTokenCost, estimateTokens } from "./tokenAccounting.js";
const execFileAsync = promisify(execFile);
export const defaultCodexModel = "gpt-5.5";
/**
 * Resolve the Codex CLI executable. On Windows an npm-global CLI is installed
 * as `codex.cmd`/`codex.exe` rather than a bare `codex`, and Node's
 * spawn/execFile (without a shell) will not find a bare name with a PATHEXT
 * extension — so we scan PATH for the first matching launcher. Falls back to
 * the bare name (POSIX, or if nothing is found).
 */
export function resolveCodexExecutable() {
    if (process.platform !== "win32") {
        return "codex";
    }
    const pathDirs = (process.env.PATH ?? "").split(path.delimiter).filter(Boolean);
    const extensions = (process.env.PATHEXT ?? ".COM;.EXE;.BAT;.CMD")
        .split(";")
        .map((ext) => ext.trim().toLowerCase())
        .filter(Boolean);
    for (const dir of pathDirs) {
        for (const ext of extensions) {
            const candidate = path.join(dir, `codex${ext}`);
            if (existsSync(candidate)) {
                return candidate;
            }
        }
    }
    return "codex";
}
export const codexOAuthModels = [
    "gpt-5.5",
    "gpt-5.4",
    "gpt-5.2-codex",
    "gpt-5.1-codex-max",
    "gpt-5.4-mini",
    "gpt-5.3-codex",
    "gpt-5.2",
    "gpt-5.1-codex-mini"
];
export class CodexCliClient {
    workspace;
    timeoutMs;
    constructor(options) {
        this.workspace = options.workspace;
        this.timeoutMs = options.timeoutMs ?? 120_000;
    }
    async chat(options) {
        const startedAt = Date.now();
        const tempRoot = await mkdtemp(path.join(tmpdir(), "patchpilot-codex-"));
        const outputPath = path.join(tempRoot, "last-message.txt");
        try {
            const prompt = buildCodexPrompt(options);
            const usage = await runCodexExec({
                model: options.model,
                reasoningEffort: options.reasoningEffort,
                workspace: this.workspace,
                prompt,
                outputPath,
                timeoutMs: this.timeoutMs,
                signal: options.signal
            });
            const content = (await readFile(outputPath, "utf8")).trim();
            if (!content) {
                throw new Error("Codex CLI returned an empty response.");
            }
            const durationMs = Date.now() - startedAt;
            return {
                content,
                telemetry: usage
                    ? toTelemetryFromUsage(usage, durationMs, options.model)
                    : estimateTelemetry(prompt, content, durationMs, options.model)
            };
        }
        finally {
            await rm(tempRoot, {
                recursive: true,
                force: true
            });
        }
    }
    async listModels() {
        return listCodexModels();
    }
}
export async function listCodexModels() {
    try {
        const { stdout } = await execFileAsync(resolveCodexExecutable(), ["debug", "models", "--bundled"], {
            timeout: 2500,
            maxBuffer: 4_000_000,
            windowsHide: true
        });
        const catalog = JSON.parse(stdout);
        const models = catalog.models
            ?.filter((model) => model.visibility === "list" && typeof model.slug === "string")
            .map((model) => String(model.slug))
            .filter(Boolean) ?? [];
        return models.length > 0 ? models : codexOAuthModels;
    }
    catch {
        return codexOAuthModels;
    }
}
export function hasCodexCliOAuth(codexAuthPath = defaultCodexAuthPath()) {
    const auth = readCodexAuth(codexAuthPath);
    return Boolean(auth && isRecord(auth.tokens) && typeof auth.tokens.access_token === "string");
}
function buildCodexPrompt(options) {
    const modeInstruction = options.formatJson
        ? "Return exactly one JSON object and no Markdown. Do not wrap it in code fences."
        : "Return plain text.";
    const transcript = options.messages.map((message) => `${message.role.toUpperCase()}:\n${message.content}`).join("\n\n");
    return [
        "You are acting as PatchPilot's model backend.",
        "Do not edit files or run shell commands yourself.",
        "If repository context is needed, ask PatchPilot for tools by returning the required JSON tool_calls.",
        modeInstruction,
        "",
        transcript
    ].join("\n");
}
function runCodexExec(options) {
    return new Promise((resolve, reject) => {
        const child = spawn(resolveCodexExecutable(), [
            "exec",
            "--json",
            "--model",
            options.model,
            ...(options.reasoningEffort && options.reasoningEffort !== "none" ? ["-c", `model_reasoning_effort="${options.reasoningEffort}"`] : []),
            "--sandbox",
            "read-only",
            "--cd",
            options.workspace,
            "--output-last-message",
            options.outputPath,
            "-"
        ], {
            cwd: options.workspace,
            stdio: ["pipe", "pipe", "pipe"],
            signal: options.signal,
            windowsHide: true
        });
        let stdout = "";
        let stderr = "";
        const timeout = setTimeout(() => {
            child.kill();
            reject(new Error(`Codex CLI timed out after ${options.timeoutMs}ms.`));
        }, options.timeoutMs);
        child.stdout.on("data", (chunk) => {
            stdout += chunk.toString("utf8");
        });
        child.stderr.on("data", (chunk) => {
            stderr += chunk.toString("utf8");
        });
        child.on("error", (error) => {
            clearTimeout(timeout);
            reject(error.name === "AbortError"
                ? new Error("Codex CLI run aborted.")
                : new Error(`Cannot run Codex CLI. Install it and run codex login. ${error.message}`));
        });
        child.on("close", (exitCode) => {
            clearTimeout(timeout);
            if (exitCode === 0) {
                resolve(parseCodexUsageFromJsonl(stdout));
                return;
            }
            const output = `${stdout}\n${stderr}`.trim();
            reject(new Error(`Codex CLI exited ${exitCode}. ${clip(output.trim(), 1200)}`));
        });
        child.stdin.end(options.prompt);
    });
}
export function parseCodexUsageFromJsonl(value) {
    let usage = null;
    for (const line of value.split(/\r?\n/)) {
        const trimmedLine = line.trim();
        if (!trimmedLine.startsWith("{")) {
            continue;
        }
        const parsed = parseJsonObject(trimmedLine);
        if (!parsed || parsed.type !== "turn.completed" || !isRecord(parsed.usage)) {
            continue;
        }
        const inputTokens = readNonNegativeNumber(parsed.usage.input_tokens);
        const outputTokens = readNonNegativeNumber(parsed.usage.output_tokens);
        if (inputTokens === null || outputTokens === null) {
            continue;
        }
        usage = {
            input_tokens: inputTokens,
            cached_input_tokens: readNonNegativeNumber(parsed.usage.cached_input_tokens) ?? 0,
            output_tokens: outputTokens
        };
    }
    return usage;
}
function toTelemetryFromUsage(usage, durationMs, model) {
    return attachTokenCost({
        promptTokens: usage.input_tokens,
        cachedPromptTokens: usage.cached_input_tokens ?? 0,
        cacheWriteTokens: 0,
        responseTokens: usage.output_tokens,
        totalTokens: usage.input_tokens + usage.output_tokens,
        evalTokensPerSecond: usage.output_tokens > 0 && durationMs > 0 ? usage.output_tokens / (durationMs / 1000) : null,
        promptDurationMs: 0,
        responseDurationMs: durationMs,
        totalDurationMs: durationMs,
        tokenSource: "provider"
    }, "codex", model);
}
function estimateTelemetry(prompt, content, durationMs, model) {
    const promptTokens = estimateTokens(prompt);
    const responseTokens = estimateTokens(content);
    return attachTokenCost({
        promptTokens,
        cachedPromptTokens: 0,
        cacheWriteTokens: 0,
        responseTokens,
        totalTokens: promptTokens + responseTokens,
        evalTokensPerSecond: responseTokens > 0 && durationMs > 0 ? responseTokens / (durationMs / 1000) : null,
        promptDurationMs: 0,
        responseDurationMs: durationMs,
        totalDurationMs: durationMs,
        tokenSource: "estimated"
    }, "codex", model);
}
function readCodexAuth(codexAuthPath) {
    if (!existsSync(codexAuthPath)) {
        return null;
    }
    try {
        const parsed = JSON.parse(readFileSync(codexAuthPath, "utf8"));
        return isRecord(parsed) ? parsed : null;
    }
    catch {
        return null;
    }
}
function defaultCodexAuthPath() {
    return path.join(homedir(), ".codex", "auth.json");
}
function isRecord(value) {
    return typeof value === "object" && value !== null;
}
function parseJsonObject(value) {
    try {
        const parsed = JSON.parse(value);
        return isRecord(parsed) ? parsed : null;
    }
    catch {
        return null;
    }
}
function readNonNegativeNumber(value) {
    return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : null;
}
function clip(value, maxLength) {
    if (value.length <= maxLength) {
        return value;
    }
    return `${value.slice(0, maxLength)}...[clipped ${value.length - maxLength} chars]`;
}
//# sourceMappingURL=codex.js.map