/**
 * OpenCode CLI Provider — wraps the OpenCode CLI as a Jait provider.
 *
 * Spawns `opencode run <message> --format json` for structured NDJSON output.
 *
 * OpenCode JSON events:
 *   { type: "step_start", sessionID, part: { ... } }
 *   { type: "text", sessionID, part: { text, ... } }
 *   { type: "tool_start", sessionID, part: { tool, args, ... } }
 *   { type: "tool_result", sessionID, part: { tool, output, ... } }
 *   { type: "step_finish", sessionID, part: { reason, cost, tokens, ... } }
 */
import { spawn, spawnSync } from "node:child_process";
import { EventEmitter } from "node:events";
import { existsSync, writeFileSync, readFileSync, unlinkSync } from "node:fs";
import { join } from "node:path";
import { uuidv7 } from "../db/uuidv7.js";
import { DEVICE_PROVIDER_AUTH, killChildTree as killAuthChildTree, runAuthCommand, startDeviceLoginCommand, } from "./provider-auth.js";
// ── Provider implementation ──────────────────────────────────────────
export class OpenCodeProvider {
    id = "opencode";
    info = {
        id: "opencode",
        name: "OpenCode",
        description: "OpenCode CLI agent with multi-provider model support",
        available: false,
        modes: ["full-access", "supervised"],
        auth: DEVICE_PROVIDER_AUTH,
    };
    sessions = new Map();
    emitter = new EventEmitter();
    opencodePath = null;
    authLoginProcess = null;
    async checkAvailability() {
        try {
            const available = await this.testCommand("opencode");
            if (!available) {
                this.info.available = false;
                this.info.unavailableReason = "OpenCode CLI not found. Install from: https://github.com/nicepkg/opencode";
                return false;
            }
            this.opencodePath = "opencode";
            const hasApiKey = !!(process.env["ANTHROPIC_API_KEY"] ||
                process.env["OPENAI_API_KEY"] ||
                process.env["GOOGLE_API_KEY"] ||
                process.env["OPENROUTER_API_KEY"] ||
                process.env["AWS_ACCESS_KEY_ID"]);
            if (!hasApiKey) {
                this.info.available = false;
                this.info.unavailableReason =
                    "No LLM API key found for OpenCode. Set ANTHROPIC_API_KEY, OPENAI_API_KEY, or another provider key.";
                return false;
            }
            this.info.available = true;
            this.info.unavailableReason = undefined;
            return true;
        }
        catch {
            this.info.available = false;
            this.info.unavailableReason = "Failed to check OpenCode CLI availability";
            return false;
        }
    }
    async getAuthStatus() {
        const status = await runAuthCommand(this.id, this.opencodePath ?? "opencode", ["auth", "list"], 10_000);
        const envConfigured = !!(process.env["ANTHROPIC_API_KEY"] ||
            process.env["OPENAI_API_KEY"] ||
            process.env["GOOGLE_API_KEY"] ||
            process.env["OPENROUTER_API_KEY"] ||
            process.env["AWS_ACCESS_KEY_ID"]);
        const authenticated = envConfigured || status.ok;
        return {
            ...DEVICE_PROVIDER_AUTH,
            authenticated,
            detail: authenticated ? "OpenCode credentials are configured." : status.rawOutput ?? "OpenCode is not authenticated.",
        };
    }
    async startLogin() {
        if (this.authLoginProcess) {
            killAuthChildTree(this.authLoginProcess);
            this.authLoginProcess = null;
        }
        const { result, child } = await startDeviceLoginCommand({
            providerId: this.id,
            label: "OpenCode",
            commandLine: this.opencodePath ?? "opencode",
            args: ["auth", "login"],
            timeoutMs: 30_000,
        });
        if (child) {
            this.authLoginProcess = child;
            child.on("exit", () => {
                if (this.authLoginProcess === child)
                    this.authLoginProcess = null;
                void this.checkAvailability();
            });
        }
        return result;
    }
    async logout() {
        if (this.authLoginProcess) {
            killAuthChildTree(this.authLoginProcess);
            this.authLoginProcess = null;
        }
        const result = await runAuthCommand(this.id, this.opencodePath ?? "opencode", ["auth", "logout"]);
        await this.checkAvailability().catch(() => false);
        return {
            ...result,
            message: result.ok ? "OpenCode logout completed." : result.message,
        };
    }
    async listModels() {
        try {
            const models = await this.fetchModels();
            if (models.length > 0)
                return models;
        }
        catch { /* fall through */ }
        return [
            { id: "default", name: "Default", description: "OpenCode default model", isDefault: true },
        ];
    }
    async startSession(options) {
        const sessionId = uuidv7();
        const session = {
            id: sessionId,
            providerId: "opencode",
            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,
            exitMode: "normal",
            turnCount: 0,
            mcpConfigInjected: false,
        };
        this.sessions.set(sessionId, state);
        // Inject MCP servers into project-level opencode.json
        this.injectMcpConfig(state, options.mcpServers);
        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("OpenCode turn already running");
        }
        const args = ["run", message, "--format", "json"];
        if (state.model) {
            args.push("-m", state.model);
        }
        // Resume previous session on 2nd+ turn
        if (state.turnCount > 0 && state.openCodeSessionId) {
            args.push("--session", state.openCodeSessionId);
        }
        state.turnCount++;
        const cmd = this.opencodePath ?? "opencode";
        const child = spawn(cmd, args, {
            cwd: state.workingDirectory,
            env: state.env,
            stdio: ["pipe", "pipe", "pipe"],
            shell: true,
            windowsHide: true,
        });
        state.process = child;
        state.buffer = "";
        state.exitMode = "normal";
        state.session.status = "running";
        this.emit({ type: "turn.started", sessionId });
        // Suppress EPIPE errors when child exits before we finish writing
        child.stdin?.on("error", () => { });
        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(`[opencode:${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 = `OpenCode 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) {
        console.warn("OpenCode approval response not implemented for non-interactive mode");
    }
    async stopSession(sessionId) {
        const state = this.sessions.get(sessionId);
        if (!state)
            return;
        // Clean up injected MCP config
        this.cleanupMcpConfig(state);
        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 OpenCode 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(state, event);
            }
            catch {
                // Not JSON — ignore
            }
        }
    }
    handleEvent(state, event) {
        const sessionId = state.session.id;
        const type = event["type"];
        const part = event["part"];
        switch (type) {
            case "text": {
                const text = String(part?.["text"] ?? "");
                const partType = String(part?.["type"] ?? "text");
                if (text) {
                    if (partType === "thinking" || partType === "reasoning") {
                        this.emit({ type: "activity", sessionId, kind: "thinking", summary: text, payload: part });
                    }
                    else {
                        this.emit({ type: "token", sessionId, content: text });
                    }
                }
                const textSessionId = event["sessionID"];
                if (textSessionId && !state.openCodeSessionId) {
                    state.openCodeSessionId = textSessionId;
                }
                break;
            }
            case "tool_start": {
                const tool = String(part?.["tool"] ?? part?.["name"] ?? "");
                const args = (part?.["args"] ?? part?.["input"] ?? {});
                const callId = String(part?.["id"] ?? uuidv7());
                this.emit({
                    type: "tool.start",
                    sessionId,
                    tool,
                    args,
                    callId,
                });
                break;
            }
            case "tool_result": {
                const tool = String(part?.["tool"] ?? part?.["name"] ?? "");
                const callId = String(part?.["id"] ?? "");
                const output = String(part?.["output"] ?? part?.["content"] ?? "");
                const ok = part?.["error"] == null;
                this.emit({
                    type: "tool.result",
                    sessionId,
                    tool,
                    ok,
                    message: output,
                    callId,
                });
                break;
            }
            case "step_start": {
                // Capture session ID from events for resume
                const startSessionId = event["sessionID"];
                if (startSessionId && !state.openCodeSessionId) {
                    state.openCodeSessionId = startSessionId;
                }
                this.emit({
                    type: "activity",
                    sessionId,
                    kind: "step_start",
                    summary: "OpenCode step started",
                    payload: event,
                });
                break;
            }
            case "step_finish": {
                const reason = String(part?.["reason"] ?? "");
                const cost = part?.["cost"];
                const tokens = part?.["tokens"];
                this.emit({
                    type: "activity",
                    sessionId,
                    kind: "step_finish",
                    summary: `OpenCode step finished (reason: ${reason}${cost != null ? `, cost: $${cost}` : ""})`,
                    payload: { reason, cost, tokens },
                });
                break;
            }
            default:
                this.emit({
                    type: "activity",
                    sessionId,
                    kind: type ?? "unknown",
                    summary: `OpenCode: ${type}`,
                    payload: event,
                });
        }
    }
    /** Dynamically fetch models by running `opencode models` */
    fetchModels() {
        return new Promise((resolve) => {
            const child = spawn("opencode", ["models"], { stdio: "pipe", shell: true, windowsHide: true });
            let output = "";
            const timer = setTimeout(() => { child.kill(); resolve([]); }, 10000);
            child.stdout?.on("data", (d) => { output += d.toString(); });
            child.on("exit", () => {
                clearTimeout(timer);
                const models = [];
                // Parse lines like "provider/model-name" or table output
                const lines = output.split("\n").map(l => l.trim()).filter(Boolean);
                for (const line of lines) {
                    // Match patterns like "opencode/big-pickle" or "provider/model"
                    const match = line.match(/^(\S+\/\S+)/);
                    if (match?.[1]) {
                        const id = match[1];
                        const name = id.split("/").pop()?.replace(/-/g, " ").replace(/\b\w/g, c => c.toUpperCase()) ?? id;
                        models.push({
                            id,
                            name,
                            isDefault: models.length === 0,
                        });
                    }
                }
                resolve(models);
            });
            child.on("error", () => { clearTimeout(timer); resolve([]); });
        });
    }
    testCommand(cmd) {
        return new Promise((resolve) => {
            const child = spawn(cmd, ["--version"], { stdio: "pipe", shell: true, windowsHide: 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); });
        });
    }
    /** Inject MCP servers into project-level opencode.json */
    injectMcpConfig(state, servers) {
        if (!servers || servers.length === 0)
            return;
        const configPath = join(state.workingDirectory, "opencode.json");
        // Read existing config if present
        let config = {};
        try {
            if (existsSync(configPath)) {
                config = JSON.parse(readFileSync(configPath, "utf-8"));
            }
        }
        catch { /* start fresh */ }
        const mcp = (config["mcp"] ?? {});
        for (const server of servers) {
            if (server.transport === "stdio" && server.command) {
                const cmd = [server.command, ...(server.args ?? [])];
                mcp[server.name] = {
                    type: "local",
                    command: cmd,
                    enabled: true,
                    ...(server.env && Object.keys(server.env).length > 0 ? { environment: server.env } : {}),
                };
            }
            else if (server.transport === "sse" && server.url) {
                mcp[server.name] = {
                    type: "remote",
                    url: server.url,
                    enabled: true,
                };
            }
        }
        config["mcp"] = mcp;
        writeFileSync(configPath, JSON.stringify(config, null, 2));
        state.mcpConfigInjected = true;
    }
    /** Remove injected MCP servers from opencode.json */
    cleanupMcpConfig(state) {
        if (!state.mcpConfigInjected)
            return;
        const configPath = join(state.workingDirectory, "opencode.json");
        try {
            if (!existsSync(configPath))
                return;
            const config = JSON.parse(readFileSync(configPath, "utf-8"));
            const mcp = config["mcp"];
            if (mcp) {
                // Remove servers that were injected (enabled: true local/remote entries)
                for (const [name, entry] of Object.entries(mcp)) {
                    const e = entry;
                    if (e?.["enabled"] === true) {
                        delete mcp[name];
                    }
                }
                if (Object.keys(mcp).length === 0) {
                    delete config["mcp"];
                }
            }
            if (Object.keys(config).length === 0) {
                // If config is empty, don't leave an empty file behind
                unlinkSync(configPath);
            }
            else {
                writeFileSync(configPath, JSON.stringify(config, null, 2));
            }
        }
        catch { /* best effort */ }
    }
}
function killChildTree(child, signal = "SIGTERM") {
    if (process.platform === "win32" && child.pid !== undefined) {
        try {
            spawnSync("taskkill", ["/pid", String(child.pid), "/T", "/F"], { stdio: "ignore" });
            return;
        }
        catch { /* fall through */ }
    }
    child.kill(signal);
}
//# sourceMappingURL=opencode-provider.js.map