import { spawn as spawnProcess } from "node:child_process";
import { fileURLToPath } from "node:url";
import { parseRpcOutgoingLine, RPC_PROTOCOL_VERSION, } from "../core/modes/rpc_protocol.js";
import { TauProcessError, TauRpcResponseError, TauTransportError } from "./errors.js";
const DEFAULT_CONNECT_TIMEOUT_MS = 5_000;
const DEFAULT_CLOSE_TIMEOUT_MS = 2_000;
const DEFAULT_MAIN_SCRIPT_PATH = fileURLToPath(new URL("../main.js", import.meta.url));
const DEFAULT_INITIALIZE_PARAMS = {
    client: {
        name: "tau-sdk",
        version: "1",
    },
};
class TauSdkClientImpl {
    process;
    eventListeners = new Set();
    pendingRequests = new Map();
    readyDeferred = createDeferred();
    exitDeferred = createDeferred();
    closeTimeoutMs;
    readyValue;
    stdoutBuffer = "";
    stderrBuffer = "";
    nextRequestId = 1;
    closePromise;
    shutdownPromise;
    isClosed = false;
    isExited = false;
    fatalError;
    constructor(process) {
        this.process = process;
        this.closeTimeoutMs = DEFAULT_CLOSE_TIMEOUT_MS;
        this.process.stdout.setEncoding("utf8");
        this.process.stderr.setEncoding("utf8");
        this.process.stdout.on("data", this.handleStdoutData);
        this.process.stderr.on("data", this.handleStderrData);
        this.process.on("error", this.handleProcessError);
        this.process.on("exit", this.handleProcessExit);
        this.process.on("close", this.handleProcessClose);
    }
    get ready() {
        if (!this.readyValue) {
            throw new TauTransportError("tau sdk client is not connected");
        }
        return this.readyValue;
    }
    async connect(initializeParams, timeoutMs) {
        const ready = await withTimeout(this.readyDeferred.promise, timeoutMs, new TauTransportError(`timed out waiting for rpc ready message after ${timeoutMs}ms`));
        this.readyValue = ready;
        await this.request("initialize", initializeParams);
    }
    onEvent(listener) {
        this.eventListeners.add(listener);
        return () => {
            this.eventListeners.delete(listener);
        };
    }
    submit(text, options = {}) {
        return this.request("session.submit", {
            text,
            ...(options.historyEntryId === undefined ? {} : { historyEntryId: options.historyEntryId }),
        });
    }
    interrupt() {
        return this.request("session.interrupt", {});
    }
    snapshot() {
        return this.request("session.snapshot", {});
    }
    reset() {
        return this.request("session.reset", {});
    }
    async shutdown() {
        if (this.shutdownPromise) {
            return this.shutdownPromise;
        }
        if (this.isClosed) {
            this.shutdownPromise = Promise.resolve({ shutdown: true });
            return this.shutdownPromise;
        }
        this.shutdownPromise = (async () => {
            try {
                return await this.request("session.shutdown", {});
            }
            finally {
                await this.close();
            }
        })();
        return this.shutdownPromise;
    }
    async close() {
        if (this.closePromise) {
            return this.closePromise;
        }
        this.isClosed = true;
        const closeError = new TauTransportError("tau sdk client was closed");
        this.failPendingRequests(closeError);
        this.rejectReadyIfPending(closeError);
        this.eventListeners.clear();
        this.process.stdout.off("data", this.handleStdoutData);
        this.process.stderr.off("data", this.handleStderrData);
        this.closePromise = (async () => {
            if (!this.isExited) {
                try {
                    this.process.stdin.end();
                }
                catch {
                    // ignore transport shutdown errors
                }
                try {
                    this.process.kill("SIGTERM");
                }
                catch {
                    // ignore transport shutdown errors
                }
                await Promise.race([
                    this.exitDeferred.promise,
                    delay(this.closeTimeoutMs).then(() => {
                        if (!this.isExited) {
                            try {
                                this.process.kill("SIGKILL");
                            }
                            catch {
                                // ignore
                            }
                        }
                    }),
                ]);
            }
            this.process.off("error", this.handleProcessError);
            this.process.off("exit", this.handleProcessExit);
            this.process.off("close", this.handleProcessClose);
        })();
        return this.closePromise;
    }
    request(method, params) {
        if (this.isClosed) {
            return Promise.reject(new TauTransportError("tau sdk client is closed"));
        }
        if (this.fatalError) {
            return Promise.reject(this.fatalError);
        }
        const requestId = this.nextRequestId++;
        const payload = JSON.stringify({
            version: RPC_PROTOCOL_VERSION,
            type: "request",
            id: requestId,
            method,
            params,
        });
        return new Promise((resolve, reject) => {
            const pending = {
                method,
                requestId,
                resolve: resolve,
                reject,
            };
            this.pendingRequests.set(requestId, pending);
            try {
                this.process.stdin.write(`${payload}\n`, "utf8", (error) => {
                    if (!error) {
                        return;
                    }
                    if (this.pendingRequests.get(requestId) !== pending) {
                        return;
                    }
                    this.pendingRequests.delete(requestId);
                    reject(new TauTransportError("failed to write request to tau rpc process", { cause: error }));
                });
            }
            catch (error) {
                this.pendingRequests.delete(requestId);
                reject(new TauTransportError("failed to write request to tau rpc process", { cause: error }));
            }
        });
    }
    handleStdoutData = (chunk) => {
        this.stdoutBuffer += chunk.toString();
        while (true) {
            const nextLineIndex = this.stdoutBuffer.indexOf("\n");
            if (nextLineIndex === -1) {
                break;
            }
            const line = this.stdoutBuffer.slice(0, nextLineIndex).trim();
            this.stdoutBuffer = this.stdoutBuffer.slice(nextLineIndex + 1);
            if (!line) {
                continue;
            }
            this.handleRpcLine(line);
        }
    };
    handleStderrData = (chunk) => {
        this.stderrBuffer += chunk.toString();
    };
    handleProcessError = (error) => {
        this.failTransport(new TauProcessError("tau rpc process transport failure", {
            stderr: this.stderrBuffer,
            cause: error,
        }));
    };
    handleProcessExit = (code, signal) => {
        this.onProcessTerminated({ code, signal });
    };
    handleProcessClose = (code, signal) => {
        this.onProcessTerminated({ code, signal });
    };
    onProcessTerminated(exit) {
        if (!this.isExited) {
            this.isExited = true;
            this.exitDeferred.resolve(exit);
        }
        if (this.isClosed) {
            return;
        }
        this.isClosed = true;
        const message = [
            "tau rpc process exited unexpectedly",
            `code=${exit.code ?? "null"}`,
            `signal=${exit.signal ?? "null"}`,
        ].join(" ");
        this.failTransport(new TauProcessError(message, {
            exitCode: exit.code,
            signal: exit.signal,
            stderr: this.stderrBuffer,
        }));
    }
    handleRpcLine(line) {
        const parsed = parseRpcOutgoingLine(line);
        if (!parsed.ok) {
            this.handleOutgoingParseFailure(parsed);
            return;
        }
        const message = parsed.message;
        if (message.type === "ready") {
            if (!this.readyValue) {
                this.readyValue = message;
                this.readyDeferred.resolve(message);
            }
            return;
        }
        if (message.type === "event") {
            for (const listener of [...this.eventListeners]) {
                try {
                    listener(message);
                }
                catch {
                    // ignore listener errors so transport handling can continue
                }
            }
            return;
        }
        this.handleResponse(message);
    }
    handleOutgoingParseFailure(failure) {
        if (failure.messageType === "response" && failure.id !== null) {
            const pending = this.pendingRequests.get(failure.id);
            if (pending) {
                this.pendingRequests.delete(failure.id);
                pending.reject(new TauTransportError("received malformed rpc response"));
            }
            return;
        }
        if (failure.reason === "parse_error") {
            this.failTransport(new TauTransportError("received malformed JSON from tau rpc process"));
            return;
        }
        if (failure.reason === "response_invalid_id") {
            this.failTransport(new TauTransportError("received response without a valid request id"));
            return;
        }
        if (failure.reason === "unsupported_version" || failure.reason === "unsupported_message_type") {
            this.failTransport(new TauTransportError(`received ${failure.error.message} from tau process`));
            return;
        }
        this.failTransport(new TauTransportError(`received invalid rpc payload from tau process: ${failure.error.message}`));
    }
    handleResponse(message) {
        if (message.ok) {
            const pending = this.pendingRequests.get(message.id);
            if (!pending) {
                return;
            }
            this.pendingRequests.delete(message.id);
            pending.resolve(message.result);
            return;
        }
        if (message.id === null) {
            this.failTransport(new TauTransportError("received uncorrelated rpc error response"));
            return;
        }
        const pending = this.pendingRequests.get(message.id);
        if (!pending) {
            return;
        }
        this.pendingRequests.delete(message.id);
        pending.reject(new TauRpcResponseError({
            requestId: message.id,
            error: message.error,
        }));
    }
    failTransport(error) {
        if (this.fatalError) {
            return;
        }
        this.fatalError = error;
        this.failPendingRequests(error);
        this.rejectReadyIfPending(error);
    }
    rejectReadyIfPending(error) {
        if (this.readyValue) {
            return;
        }
        this.readyDeferred.reject(error);
    }
    failPendingRequests(error) {
        for (const pending of this.pendingRequests.values()) {
            pending.reject(error);
        }
        this.pendingRequests.clear();
    }
}
export async function createTauSdkClient(options = {}) {
    const initializeParams = resolveInitializeParams(options.initialize);
    const spawn = options.spawn ?? spawnProcess;
    const command = options.executable ?? process.execPath;
    const args = buildProcessArgs(options);
    const env = options.env ? { ...process.env, ...options.env } : process.env;
    let childProcess;
    try {
        childProcess = spawn(command, args, {
            cwd: options.cwd,
            env,
            stdio: "pipe",
        });
    }
    catch (error) {
        throw new TauProcessError("failed to spawn tau rpc process", { cause: error });
    }
    const client = new TauSdkClientImpl(childProcess);
    try {
        await client.connect(initializeParams, options.connectTimeoutMs ?? DEFAULT_CONNECT_TIMEOUT_MS);
        return client;
    }
    catch (error) {
        await client.close();
        throw error;
    }
}
function resolveInitializeParams(params) {
    const candidate = params ?? DEFAULT_INITIALIZE_PARAMS;
    if (typeof candidate.client.name !== "string" ||
        !candidate.client.name.trim() ||
        typeof candidate.client.version !== "string" ||
        !candidate.client.version.trim()) {
        throw new TauTransportError("sdk initialize.client.name and initialize.client.version must be non-empty strings");
    }
    return {
        client: {
            name: candidate.client.name.trim(),
            version: candidate.client.version.trim(),
        },
    };
}
function buildProcessArgs(options) {
    const scriptPath = options.scriptPath === undefined ? DEFAULT_MAIN_SCRIPT_PATH : options.scriptPath;
    return [
        ...(options.executableArgs ?? []),
        ...(scriptPath ? [scriptPath] : []),
        ...(options.scriptArgs ?? []),
        "rpc",
        ...(options.persona ? ["--persona", options.persona] : []),
        ...(options.riskLevel ? ["--risk", options.riskLevel] : []),
        ...(options.noAgentContextFiles ? ["--no-agent-context-files"] : []),
        ...(options.rpcArgs ?? []),
    ];
}
function createDeferred() {
    let resolve;
    let reject;
    const promise = new Promise((res, rej) => {
        resolve = res;
        reject = rej;
    });
    return { promise, resolve, reject };
}
function withTimeout(promise, timeoutMs, timeoutError) {
    const timeoutPromise = new Promise((_, reject) => {
        const timeout = setTimeout(() => reject(timeoutError), timeoutMs);
        timeout.unref?.();
    });
    return Promise.race([promise, timeoutPromise]);
}
function delay(timeoutMs) {
    return new Promise((resolve) => {
        const timeout = setTimeout(resolve, timeoutMs);
        timeout.unref?.();
    });
}
//# sourceMappingURL=client.js.map