/**
 * LLM calls for workflow steps via Claude Code subprocess.
 *
 * Spawns `claude --print` as a child process with the resolved prompt.
 * Claude Code handles its own OAuth authentication internally — no
 * credential reading, token refresh, or API endpoint logic needed here.
 * This is the same mechanism the admin chat uses (claude-agent.ts).
 *
 * Two spawn modes:
 *
 *   ONE-SHOT (no mcpServers)
 *   ────────────────────────
 *   VALIDATE PROMPT
 *     │
 *     ├─ empty → throw (no silent fallback)
 *     │
 *     v
 *   SPAWN claude --print --model <model> --max-turns 1
 *     │
 *     ├─ ENOENT (not in PATH) → throw
 *     ├─ non-zero exit → throw with stderr + stdout
 *     ├─ timeout → SIGTERM → throw
 *     ├─ empty stdout → throw
 *     └─ stdout text → return
 *
 *   AGENTIC (mcpServers declared)
 *   ─────────────────────────────
 *   VALIDATE PROMPT
 *     │
 *     v
 *   WRITE TEMP MCP CONFIG (mkdtemp + writeFile)
 *     │
 *     ├─ disk error → throw (cleanup skipped — nothing to clean)
 *     │
 *     v
 *   SPAWN claude --print --model <model> --max-turns <N>
 *         --mcp-config <tmp/mcp.json> --strict-mcp-config
 *         --permission-mode dontAsk
 *     │
 *     ├─ same error paths as one-shot
 *     │
 *     v
 *   CLEANUP TEMP CONFIG (finally — always runs)
 *     │
 *     ├─ ENOENT → swallow (already cleaned or never written)
 *     └─ other error → log, don't throw (cleanup must not mask spawn error)
 *
 * probeLlmHealth() shares the same spawn path with a trivial "pong"
 * prompt and a shorter timeout. Used as the pre-flight check before
 * executing workflow LLM steps (Task 74 — graceful degradation).
 */
import { spawn as realSpawn } from "node:child_process";
import { readFileSync, writeFileSync, rmSync, mkdtempSync } from "node:fs";
import { resolve, join } from "node:path";
import { tmpdir } from "node:os";
/**
 * Read the account's adminModel from account.json.
 * Returns {model, reason, path, error?} — callers branch on .model (null
 * means fail) and may surface .reason/.path into user-visible errors.
 */
export function readAdminModel(platformRoot, accountId) {
    // Validate accountId — block path traversal
    if (accountId.includes("..") || accountId.includes("/") || accountId.includes("\\")) {
        throw new Error(`Invalid accountId: "${accountId}" — path traversal blocked`);
    }
    const configPath = resolve(platformRoot, "..", "data", "accounts", accountId, "account.json");
    let raw;
    try {
        raw = readFileSync(configPath, "utf-8");
    }
    catch (err) {
        const code = err instanceof Error && "code" in err
            ? err.code
            : undefined;
        const message = err instanceof Error ? err.message : String(err);
        const reason = code === "ENOENT" ? "enoent" :
            code === "EACCES" ? "eacces" :
                "fs_error";
        logResolutionFailure(reason, configPath, message);
        return { model: null, reason, path: configPath, error: message };
    }
    let config;
    try {
        config = JSON.parse(raw);
    }
    catch (err) {
        const message = err instanceof Error ? err.message : String(err);
        logResolutionFailure("parse_error", configPath, message);
        return { model: null, reason: "parse_error", path: configPath, error: message };
    }
    const adminModel = config.adminModel;
    if (adminModel === undefined) {
        logResolutionFailure("field_missing", configPath);
        return { model: null, reason: "field_missing", path: configPath };
    }
    if (typeof adminModel !== "string") {
        logResolutionFailure("field_wrong_type", configPath, `adminModel is typeof ${typeof adminModel}`);
        return { model: null, reason: "field_wrong_type", path: configPath };
    }
    if (adminModel === "") {
        logResolutionFailure("field_empty", configPath);
        return { model: null, reason: "field_empty", path: configPath };
    }
    return { model: adminModel, reason: "ok", path: configPath };
}
function logResolutionFailure(reason, path, error) {
    const errSegment = error ? ` error=${error}` : "";
    console.error(`[llm-call] adminModel resolution FAILED: reason=${reason} path=${path}${errSegment}`);
}
// ─────────────────────────────────────────────────────────────
// Temp MCP config management
// ─────────────────────────────────────────────────────────────
/**
 * Write a temporary MCP config file for agentic steps.
 * Returns the path to the config file.
 *
 * The file is written to a unique temp directory (mkdtempSync)
 * to avoid collisions between concurrent workflow executions.
 * Format matches Claude Code's --mcp-config JSON input:
 * { "mcpServers": { "name": { "command": ..., "args": ..., "env": ... } } }
 */
function writeTempMcpConfig(mcpServers) {
    const dir = mkdtempSync(join(tmpdir(), "maxy-mcp-"));
    const configPath = join(dir, "mcp.json");
    const config = { mcpServers };
    try {
        writeFileSync(configPath, JSON.stringify(config), "utf-8");
        console.error(`[llm-call] agentic: wrote MCP config to ${configPath}`);
        return configPath;
    }
    catch (err) {
        // Clean up the empty temp dir on write failure
        try {
            rmSync(dir, { recursive: true });
        }
        catch { /* best-effort */ }
        throw new Error(`Failed to write MCP config for agentic step: ${err instanceof Error ? err.message : String(err)}`);
    }
}
/**
 * Clean up a temporary MCP config file and its parent directory.
 * Never throws — cleanup failures are logged but must not mask
 * the spawn result (success or error).
 */
function cleanupTempMcpConfig(configPath) {
    try {
        // configPath is <tmpdir>/maxy-mcp-XXXXXX/mcp.json — remove the parent dir
        const dir = resolve(configPath, "..");
        rmSync(dir, { recursive: true });
        console.error(`[llm-call] agentic: cleanup MCP config ${configPath}`);
    }
    catch (err) {
        // ENOENT is fine (already cleaned or never fully written)
        if (err instanceof Error && "code" in err && err.code === "ENOENT") {
            return;
        }
        console.error(`[llm-call] agentic: cleanup failed for ${configPath}: ${err instanceof Error ? err.message : String(err)}`);
    }
}
// ─────────────────────────────────────────────────────────────
// LLM call via Claude Code subprocess
// ─────────────────────────────────────────────────────────────
/** Max stderr to capture — enough for diagnostics, bounded to prevent OOM. */
const MAX_STDERR_BYTES = 4096;
/** Default maxTurns when mcpServers is declared but maxTurns is omitted. */
const DEFAULT_AGENTIC_MAX_TURNS = 10;
/**
 * Execute an LLM call by spawning Claude Code.
 *
 * When mcpServers is provided, spawns in agentic mode: writes a temp
 * config file, adds --mcp-config and --strict-mcp-config flags, and
 * uses the step's maxTurns (or default 10) instead of 1.
 *
 * Returns the model's text response. The caller is responsible for
 * parsing the response (JSON or raw text) via parseStepOutput().
 */
export async function executeLlmCall(params) {
    const { prompt, model, timeoutMs, mcpServers, maxTurns } = params;
    const spawn = params._spawn ?? realSpawn;
    if (!prompt.trim()) {
        throw new Error("LLM step has an empty prompt — nothing to send to the model.");
    }
    const isAgentic = mcpServers !== undefined && Object.keys(mcpServers).length > 0;
    const effectiveMaxTurns = isAgentic
        ? (maxTurns ?? DEFAULT_AGENTIC_MAX_TURNS)
        : (maxTurns ?? 1);
    const serverNames = isAgentic ? Object.keys(mcpServers) : [];
    console.error(`[llm-call] spawning Claude Code: model=${model} maxTurns=${effectiveMaxTurns}` +
        (isAgentic ? ` mcpServers=[${serverNames.join(",")}]` : ""));
    // Write temp MCP config for agentic steps (before building args)
    let tempConfigPath;
    if (isAgentic) {
        tempConfigPath = writeTempMcpConfig(mcpServers);
    }
    const args = [
        "--print",
        "--model", model,
        "--max-turns", String(effectiveMaxTurns),
        "--permission-mode", "dontAsk",
    ];
    if (tempConfigPath) {
        args.push("--mcp-config", tempConfigPath, "--strict-mcp-config");
    }
    // Prompt is the last positional argument
    args.push(prompt);
    try {
        return await new Promise((promiseResolve, reject) => {
            const proc = spawn("claude", args, {
                stdio: ["ignore", "pipe", "pipe"],
            });
            let stdout = "";
            let stderr = "";
            proc.stdout.on("data", (chunk) => {
                stdout += chunk.toString("utf-8");
            });
            proc.stderr.on("data", (chunk) => {
                if (stderr.length < MAX_STDERR_BYTES) {
                    stderr += chunk.toString("utf-8").slice(0, MAX_STDERR_BYTES - stderr.length);
                }
            });
            const timer = setTimeout(() => {
                proc.kill("SIGTERM");
                reject(new Error(`LLM call timed out after ${timeoutMs}ms`));
            }, timeoutMs);
            proc.on("close", (code) => {
                clearTimeout(timer);
                if (code !== 0) {
                    const stderrSnippet = stderr ? `\nstderr: ${stderr.trim()}` : "";
                    const stdoutSnippet = stdout
                        ? `\nstdout: ${stdout.slice(0, MAX_STDERR_BYTES).trim()}`
                        : "";
                    reject(new Error(`Claude Code exited with code ${code}${stderrSnippet}${stdoutSnippet}`));
                    return;
                }
                if (!stdout.trim()) {
                    reject(new Error("Claude Code returned an empty response."));
                    return;
                }
                console.error(`[llm-call] response: ${stdout.length} chars`);
                promiseResolve(stdout);
            });
            proc.on("error", (err) => {
                clearTimeout(timer);
                reject(new Error(`Failed to spawn Claude Code: ${err.message}`));
            });
        });
    }
    finally {
        if (tempConfigPath) {
            cleanupTempMcpConfig(tempConfigPath);
        }
    }
}
// ─────────────────────────────────────────────────────────────
// Health probe — pre-flight check before running LLM workflow steps
// ─────────────────────────────────────────────────────────────
const DEFAULT_PROBE_TIMEOUT_MS = 10_000;
/**
 * Lightweight pre-flight probe for LLM availability.
 *
 * Spawns `claude --print` with a trivial prompt ("pong") and a short
 * timeout. Any failure mode that would cause a real LLM step to fail
 * (missing binary, dead OAuth token, Anthropic outage, network partition)
 * also fails this probe — it is the strongest available signal short
 * of running the real workflow.
 *
 * Called at most once per workflow run, only when the workflow contains
 * at least one LLM step. Per-step spawn failures mid-run are handled
 * separately by the workflow-execute convergence arm.
 *
 *   SUCCESS  →  { ok: true, latencyMs }
 *   FAILURE  →  { ok: false, latencyMs, error: one-line summary }
 */
export async function probeLlmHealth(params) {
    const { model, timeoutMs = DEFAULT_PROBE_TIMEOUT_MS, _spawn } = params;
    const start = Date.now();
    console.error(`[llm-probe] probing: model=${model}`);
    try {
        await executeLlmCall({
            prompt: "pong",
            model,
            maxTokens: 8,
            timeoutMs,
            _spawn,
        });
        const latencyMs = Date.now() - start;
        console.error(`[llm-probe] ok: latencyMs=${latencyMs}`);
        return { ok: true, latencyMs };
    }
    catch (err) {
        const latencyMs = Date.now() - start;
        const error = err instanceof Error ? err.message : String(err);
        // Collapse multi-line errors to a one-liner for log + template substitution
        const oneLine = error.replace(/\s*\n\s*/g, " — ");
        console.error(`[llm-probe] FAILED: ${oneLine} latencyMs=${latencyMs}`);
        return { ok: false, latencyMs, error: oneLine };
    }
}
//# sourceMappingURL=llm-call.js.map