import { spawnProcess } from "../../../../utils/spawn.js";
import { terminateWithTreeKill } from "../../../../utils/tree-kill.js";
import { buildPiLaunch, } from "./runtime.js";
const DEFAULT_PI_COMMAND = [
    process.env.PI_COMMAND ?? process.env.PI_ACP_PI_COMMAND ?? "pi",
];
const DEFAULT_TIMEOUT_MS = 30000;
const STDERR_BUFFER_LIMIT = 8192;
const GRACEFUL_SHUTDOWN_TIMEOUT_MS = 2000;
const FORCE_SHUTDOWN_TIMEOUT_MS = 1000;
function assertChildWithPipes(child) {
    if (!child.stdin || !child.stdout || !child.stderr) {
        throw new Error("Pi process was spawned without stdio streams");
    }
}
export class PiCliRuntime {
    constructor(options) {
        this.options = options;
        this.command = options.command ?? DEFAULT_PI_COMMAND;
        this.spawnProcess =
            options.spawnProcess ??
                ((launch) => {
                    const [command, ...args] = launch.argv;
                    const child = spawnProcess(command, args, {
                        cwd: launch.cwd,
                        envOverlay: launch.env,
                        stdio: ["pipe", "pipe", "pipe"],
                    });
                    assertChildWithPipes(child);
                    return child;
                });
    }
    async startSession(input) {
        const launch = buildPiLaunch({
            command: this.command,
            runtimeSettings: this.options.runtimeSettings,
            session: input,
        });
        return new PiCliRuntimeSession(launch, this.spawnProcess(launch), this.options.logger);
    }
}
class PiCliRuntimeSession {
    constructor(_launch, child, logger) {
        this.child = child;
        this.logger = logger;
        this.pending = new Map();
        this.subscribers = new Set();
        this.stderrBuffer = "";
        this.nextRequestId = 1;
        this.disposed = false;
        this.stdoutBuffer = "";
        child.stdout.on("data", (chunk) => {
            this.handleStdoutChunk(chunk.toString());
        });
        child.stderr.on("data", (chunk) => {
            this.stderrBuffer += chunk.toString();
            if (this.stderrBuffer.length > STDERR_BUFFER_LIMIT) {
                this.stderrBuffer = this.stderrBuffer.slice(-STDERR_BUFFER_LIMIT);
            }
        });
        child.on("error", (error) => {
            this.failAll(error instanceof Error ? error : new Error(String(error)));
        });
        child.on("exit", (code, signal) => {
            const error = new Error(`Pi RPC process exited with code ${code ?? "null"} and signal ${signal ?? "null"}\n${this.stderrBuffer}`.trim());
            this.emit({ type: "process_exit", error: error.message });
            this.failAll(error);
        });
    }
    onEvent(callback) {
        this.subscribers.add(callback);
        return () => {
            this.subscribers.delete(callback);
        };
    }
    async prompt(message, images) {
        await this.request({ type: "prompt", message, ...(images?.length ? { images } : {}) });
    }
    async abort() {
        await this.request({ type: "abort" });
    }
    async getState() {
        return (await this.request({ type: "get_state" }));
    }
    async getMessages() {
        const data = (await this.request({ type: "get_messages" }));
        return data.messages ?? [];
    }
    async getAvailableModels() {
        const data = (await this.request({ type: "get_available_models" }));
        return data.models ?? [];
    }
    async setModel(provider, modelId) {
        return (await this.request({ type: "set_model", provider, modelId }));
    }
    async setThinkingLevel(level) {
        await this.request({ type: "set_thinking_level", level: level });
    }
    async getSessionStats() {
        return (await this.request({ type: "get_session_stats" }));
    }
    async getCommands() {
        const data = (await this.request({ type: "get_commands" }));
        return data.commands ?? [];
    }
    respondToExtensionUiRequest(id, response) {
        this.writeJsonLine({ type: "extension_ui_response", id, ...response });
    }
    cancelExtensionUiRequest(id) {
        this.respondToExtensionUiRequest(id, { cancelled: true });
    }
    async close() {
        if (this.disposed)
            return;
        this.disposed = true;
        try {
            this.child.stdin.end();
        }
        catch {
            // ignore
        }
        const result = await terminateWithTreeKill(this.child, {
            gracefulTimeoutMs: GRACEFUL_SHUTDOWN_TIMEOUT_MS,
            forceTimeoutMs: FORCE_SHUTDOWN_TIMEOUT_MS,
            onForceSignal: () => {
                this.logger.warn({ timeoutMs: GRACEFUL_SHUTDOWN_TIMEOUT_MS }, "Pi RPC process did not exit after SIGTERM; sending SIGKILL");
            },
        });
        if (result === "kill-timeout") {
            this.logger.warn({ timeoutMs: FORCE_SHUTDOWN_TIMEOUT_MS }, "Pi RPC process did not report exit after SIGKILL");
        }
    }
    request(command, timeoutMs = DEFAULT_TIMEOUT_MS) {
        if (this.disposed) {
            return Promise.reject(new Error("Pi RPC session is closed"));
        }
        const id = `req_${this.nextRequestId}`;
        this.nextRequestId += 1;
        return new Promise((resolve, reject) => {
            const timer = setTimeout(() => {
                this.pending.delete(id);
                reject(new Error(`Pi RPC request timed out for ${command.type}\n${this.stderrBuffer}`.trim()));
            }, timeoutMs);
            this.pending.set(id, { resolve, reject, timer });
            this.writeJsonLine({ ...command, id });
        });
    }
    writeJsonLine(value) {
        if (this.disposed || this.child.stdin.destroyed || !this.child.stdin.writable) {
            return;
        }
        this.child.stdin.write(`${JSON.stringify(value)}\n`);
    }
    handleStdoutChunk(chunk) {
        this.stdoutBuffer += chunk;
        for (;;) {
            const newlineIndex = this.stdoutBuffer.indexOf("\n");
            if (newlineIndex === -1) {
                break;
            }
            const line = this.stdoutBuffer.slice(0, newlineIndex).replace(/\r$/, "");
            this.stdoutBuffer = this.stdoutBuffer.slice(newlineIndex + 1);
            if (line.trim()) {
                this.handleLine(line);
            }
        }
    }
    handleLine(line) {
        let parsed;
        try {
            parsed = JSON.parse(line);
        }
        catch (error) {
            this.logger.warn({ error, line }, "Ignoring non-JSON Pi RPC stdout line");
            return;
        }
        if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
            return;
        }
        const message = parsed;
        if (message.type === "response") {
            this.handleResponse(message);
            return;
        }
        this.emit(message);
    }
    handleResponse(response) {
        const id = response.id;
        if (!id) {
            return;
        }
        const pending = this.pending.get(id);
        if (!pending) {
            return;
        }
        clearTimeout(pending.timer);
        this.pending.delete(id);
        if (!response.success) {
            pending.reject(new Error(response.error ?? `Pi RPC ${response.command} failed`));
            return;
        }
        pending.resolve(response.data);
    }
    emit(event) {
        for (const subscriber of this.subscribers) {
            subscriber(event);
        }
    }
    failAll(error) {
        if (this.disposed) {
            return;
        }
        this.disposed = true;
        for (const pending of this.pending.values()) {
            clearTimeout(pending.timer);
            pending.reject(error);
        }
        this.pending.clear();
    }
}
//# sourceMappingURL=cli-runtime.js.map