/**
 * Claude Code CLI Provider — wraps Anthropic's Claude Code CLI as a Jait provider.
 *
 * Spawns `claude` CLI with `--output-format stream-json` for structured output.
 * Claude Code supports MCP natively via its config, so Jait tools can be exposed.
 *
 * Claude Code outputs newline-delimited JSON events to stdout:
 *   { type: "assistant", message: { ... } }
 *   { type: "tool_use", tool: "...", input: {...} }
 *   { type: "tool_result", ... }
 *   { type: "result", ... }
 */
import { spawn, spawnSync } from "node:child_process";
import { EventEmitter } from "node:events";
import { existsSync, writeFileSync, mkdirSync } from "node:fs";
import { join } from "node:path";
import { homedir, tmpdir } from "node:os";
import { uuidv7 } from "../db/uuidv7.js";
// ── Provider implementation ──────────────────────────────────────────
export class ClaudeCodeProvider {
    id = "claude-code";
    info = {
        id: "claude-code",
        name: "Claude Code",
        description: "Anthropic Claude Code CLI agent with agentic coding and MCP support",
        available: false,
        modes: ["full-access", "supervised"],
    };
    sessions = new Map();
    emitter = new EventEmitter();
    claudePath = null;
    async checkAvailability() {
        try {
            const paths = ["claude"];
            for (const cmd of paths) {
                const available = await this.testCommand(cmd);
                if (available) {
                    this.claudePath = cmd;
                    this.info.available = true;
                    return true;
                }
            }
            if (!this.claudePath) {
                this.info.available = false;
                this.info.unavailableReason = "Claude Code CLI not found. Install from https://docs.anthropic.com/en/docs/claude-code";
                return false;
            }
            const hasApiKey = !!process.env.ANTHROPIC_API_KEY?.trim();
            const hasClaudeConfig = this.hasClaudeConfig();
            if (!hasApiKey && !hasClaudeConfig) {
                this.info.available = false;
                this.info.unavailableReason = "Claude Code is not configured. Run `claude` once or set ANTHROPIC_API_KEY.";
                return false;
            }
            this.info.available = true;
            this.info.unavailableReason = undefined;
            return true;
        }
        catch {
            this.info.available = false;
            this.info.unavailableReason = "Failed to check Claude Code CLI availability";
            return false;
        }
    }
    /**
     * Claude Code doesn't expose a model listing API.
     * Prefer stable aliases over dated full model names so the picker doesn't go stale.
     */
    async listModels() {
        return [
            { id: "default", name: "Default", description: "Claude Code default model selection", isDefault: true },
            { id: "sonnet", name: "Sonnet", description: "Claude Sonnet alias for day-to-day coding" },
            { id: "opus", name: "Opus", description: "Claude Opus alias for the most capable model" },
            { id: "haiku", name: "Haiku", description: "Claude Haiku alias for faster lightweight tasks" },
            { id: "opusplan", name: "Opus Plan", description: "Planning-focused Claude alias" },
        ];
    }
    async startSession(options) {
        const sessionId = uuidv7();
        const session = {
            id: sessionId,
            providerId: "claude-code",
            threadId: options.threadId,
            status: "starting",
            runtimeMode: options.mode,
            startedAt: new Date().toISOString(),
        };
        const env = {
            ...process.env,
            ...options.env,
        };
        const state = {
            session,
            process: null,
            buffer: "",
            workingDirectory: options.workingDirectory,
            env,
            model: options.model,
            mcpConfigPath: this.buildClaudeMcpConfig(options.mcpServers, sessionId),
            exitMode: "normal",
        };
        this.sessions.set(sessionId, state);
        state.session.status = "running";
        this.emit({ type: "session.started", sessionId });
        return session;
    }
    async sendTurn(sessionId, message, _attachments) {
        const state = this.getState(sessionId);
        if (state.process) {
            throw new Error("Claude Code turn already running");
        }
        const args = [
            "--print",
            "--output-format", "stream-json",
            "--include-partial-messages",
            "--verbose",
            "--session-id", state.session.id,
        ];
        if (state.session.runtimeMode === "full-access") {
            args.push("--dangerously-skip-permissions");
        }
        else {
            args.push("--permission-mode", "default");
        }
        if (state.model) {
            args.push("--model", state.model);
        }
        if (state.mcpConfigPath) {
            args.push("--mcp-config", state.mcpConfigPath);
        }
        args.push(message);
        const cmd = this.claudePath ?? "claude";
        const child = spawn(cmd, args, {
            cwd: state.workingDirectory,
            env: state.env,
            stdio: ["pipe", "pipe", "pipe"],
            shell: true,
        });
        state.process = child;
        state.buffer = "";
        state.exitMode = "normal";
        state.session.status = "running";
        this.emit({ type: "turn.started", sessionId });
        child.stdout?.on("data", (data) => {
            state.buffer += data.toString();
            this.processBuffer(sessionId, state);
        });
        child.stderr?.on("data", (data) => {
            const text = data.toString().trim();
            if (text) {
                console.error(`[claude-code:${sessionId}] stderr: ${text}`);
            }
        });
        return new Promise((resolve, reject) => {
            child.on("exit", (code, signal) => {
                state.process = null;
                const exitMode = state.exitMode;
                state.exitMode = "normal";
                if (exitMode === "stop") {
                    state.session.status = "completed";
                    state.session.completedAt = new Date().toISOString();
                    this.emit({ type: "session.completed", sessionId });
                    resolve();
                    return;
                }
                if (exitMode === "interrupt") {
                    state.session.status = "interrupted";
                    this.emit({ type: "turn.completed", sessionId });
                    resolve();
                    return;
                }
                if (code === 0) {
                    state.session.status = "idle";
                    state.session.error = undefined;
                    this.emit({ type: "turn.completed", sessionId });
                    resolve();
                    return;
                }
                const error = `Claude Code exited with code ${code}${signal ? ` (signal=${signal})` : ""}`;
                state.session.status = "error";
                state.session.error = error;
                this.emit({ type: "session.error", sessionId, error });
                reject(new Error(error));
            });
            child.on("error", (err) => {
                state.process = null;
                state.exitMode = "normal";
                state.session.status = "error";
                state.session.error = err.message;
                this.emit({ type: "session.error", sessionId, error: err.message });
                reject(err);
            });
        });
    }
    async interruptTurn(sessionId) {
        const state = this.getState(sessionId);
        if (state.process) {
            state.exitMode = "interrupt";
            killChildTree(state.process, "SIGINT");
        }
    }
    async respondToApproval(_sessionId, _requestId, _approved) {
        // Claude Code's --print mode doesn't support interactive approvals.
        // In supervised mode, the CLI would pause; we'd need to write to stdin.
        console.warn("Claude Code approval response not yet implemented for --print mode");
    }
    async stopSession(sessionId) {
        const state = this.sessions.get(sessionId);
        if (!state)
            return;
        if (state.process) {
            state.exitMode = "stop";
            killChildTree(state.process, "SIGTERM");
            await new Promise((resolve) => {
                const timeout = setTimeout(() => {
                    if (state.process) {
                        killChildTree(state.process, "SIGKILL");
                    }
                    resolve();
                }, 3000);
                state.process?.on("exit", () => {
                    clearTimeout(timeout);
                    resolve();
                });
            });
        }
        else {
            state.session.status = "completed";
            state.session.completedAt = new Date().toISOString();
            this.emit({ type: "session.completed", sessionId });
        }
        this.sessions.delete(sessionId);
    }
    onEvent(handler) {
        this.emitter.on("event", handler);
        return () => this.emitter.off("event", handler);
    }
    // ── Private helpers ────────────────────────────────────────────────
    emit(event) {
        this.emitter.emit("event", event);
    }
    getState(sessionId) {
        const state = this.sessions.get(sessionId);
        if (!state)
            throw new Error(`No Claude Code session found: ${sessionId}`);
        return state;
    }
    processBuffer(sessionId, state) {
        const lines = state.buffer.split("\n");
        state.buffer = lines.pop() ?? "";
        for (const line of lines) {
            const trimmed = line.trim();
            if (!trimmed)
                continue;
            try {
                const event = JSON.parse(trimmed);
                this.handleEvent(sessionId, event);
            }
            catch {
                // Not JSON — could be status output
            }
        }
    }
    handleEvent(sessionId, event) {
        const type = event["type"];
        switch (type) {
            case "assistant": {
                const message = event["message"];
                const content = message?.["content"];
                if (typeof content === "string") {
                    this.emit({ type: "token", sessionId, content });
                }
                else if (Array.isArray(content)) {
                    for (const block of content) {
                        if (block?.["type"] === "text") {
                            this.emit({ type: "token", sessionId, content: String(block["text"] ?? "") });
                        }
                    }
                }
                break;
            }
            case "content_block_delta": {
                const delta = event["delta"];
                if (delta?.["type"] === "text_delta") {
                    this.emit({ type: "token", sessionId, content: String(delta["text"] ?? "") });
                }
                break;
            }
            case "tool_use": {
                const rawTool = String(event["name"] ?? event["tool"] ?? "");
                this.emit({
                    type: "tool.start",
                    sessionId,
                    tool: normalizeClaudeToolName(rawTool),
                    args: normalizeClaudeToolArgs(rawTool, event["input"] ?? {}),
                });
                break;
            }
            case "tool_result": {
                const rawTool = String(event["tool"] ?? "");
                this.emit({
                    type: "tool.result",
                    sessionId,
                    tool: normalizeClaudeToolName(rawTool),
                    ok: event["is_error"] !== true,
                    message: String(event["content"] ?? event["output"] ?? ""),
                });
                break;
            }
            case "result": {
                const resultContent = event["result"];
                if (resultContent) {
                    this.emit({ type: "message", sessionId, role: "assistant", content: resultContent });
                }
                break;
            }
            default:
                this.emit({
                    type: "activity",
                    sessionId,
                    kind: type ?? "unknown",
                    summary: `Claude Code: ${type}`,
                    payload: event,
                });
        }
    }
    buildClaudeMcpConfig(servers, sessionId) {
        if (!servers || servers.length === 0)
            return undefined;
        const config = { mcpServers: {} };
        const mcpServers = config["mcpServers"];
        for (const server of servers) {
            if (server.transport === "stdio" && server.command) {
                mcpServers[server.name] = {
                    command: server.command,
                    args: server.args ?? [],
                    env: server.env ?? {},
                };
            }
            else if (server.transport === "sse" && server.url) {
                mcpServers[server.name] = {
                    url: server.url,
                };
            }
        }
        // Write to a temp file and return the path
        const configDir = join(tmpdir(), "jait-claude-code", sessionId);
        mkdirSync(configDir, { recursive: true });
        const configPath = join(configDir, "mcp-config.json");
        writeFileSync(configPath, JSON.stringify(config, null, 2));
        return configPath;
    }
    hasClaudeConfig() {
        const claudeDir = join(homedir(), ".claude");
        const claudeState = join(homedir(), ".claude.json");
        return existsSync(claudeDir) || existsSync(claudeState);
    }
    testCommand(cmd) {
        return new Promise((resolve) => {
            const child = spawn(cmd, ["--version"], {
                stdio: "pipe",
                shell: true,
            });
            const timer = setTimeout(() => { child.kill(); resolve(false); }, 5000);
            child.on("exit", (code) => { clearTimeout(timer); resolve(code === 0); });
            child.on("error", () => { clearTimeout(timer); resolve(false); });
        });
    }
}
function normalizeClaudeToolName(tool) {
    const normalized = tool.trim().toLowerCase();
    if (normalized === "edit")
        return "edit";
    if (normalized === "multiedit")
        return "edit";
    if (normalized === "write")
        return "file.write";
    if (normalized === "read")
        return "read";
    if (normalized === "websearch")
        return "web";
    return tool;
}
function normalizeClaudeToolArgs(tool, input) {
    const normalized = normalizeClaudeToolName(tool);
    if (normalized === "edit" || normalized === "file.write" || normalized === "read") {
        return {
            path: String(input["path"] ?? input["file_path"] ?? input["filePath"] ?? input["file"] ?? ""),
            ...(input["old_string"] != null ? { search: input["old_string"] } : {}),
            ...(input["new_string"] != null ? { replace: input["new_string"] } : {}),
            ...(input["content"] != null ? { content: input["content"] } : {}),
            ...(input["new_file_contents"] != null ? { content: input["new_file_contents"] } : {}),
            ...input,
        };
    }
    if (normalized === "web") {
        return {
            query: String(input["query"] ?? input["search_query"] ?? input["q"] ?? ""),
            ...(input["url"] != null ? { url: input["url"] } : {}),
            ...input,
        };
    }
    return input;
}
function killChildTree(child, signal = "SIGTERM") {
    if (process.platform === "win32" && child.pid !== undefined) {
        try {
            const taskkillArgs = signal === "SIGINT"
                ? ["/pid", String(child.pid), "/T", "/F"]
                : ["/pid", String(child.pid), "/T", "/F"];
            spawnSync("taskkill", taskkillArgs, { stdio: "ignore" });
            return;
        }
        catch {
            // Fall through to the default kill path.
        }
    }
    child.kill(signal);
}
//# sourceMappingURL=claude-code-provider.js.map