import { once } from "node:events";
import { createConnection } from "node:net";
import { createInterface } from "node:readline";
import { createDiffReviewErrorResponse, createDiffReviewSuccessResponse, DIFF_REVIEW_PROTOCOL_VERSION, parseDiffReviewMessageLine, serializeDiffReviewMessage, } from "../core/diff_review/index.js";
import { DIFF_TOOL_CODE_THEMES } from "./shared_types.js";
const codeThemes = new Set(DIFF_TOOL_CODE_THEMES);
export class DiffToolLaunchEnvironmentError extends Error {
    constructor(message) {
        super(message);
        this.name = "DiffToolLaunchEnvironmentError";
    }
}
export class DiffReviewProtocolClientError extends Error {
    code;
    data;
    constructor(code, message, data) {
        super(message);
        this.name = "DiffReviewProtocolClientError";
        this.code = code;
        this.data = data;
    }
}
export class DiffReviewProtocolClient {
    launchEnvironment;
    socket;
    readline;
    pendingRequests = new Map();
    closeListeners = new Set();
    sessionCloseListeners = new Set();
    connectPromise;
    writeQueue = Promise.resolve();
    requestCounter = 0;
    closed = false;
    sessionClosing = false;
    closeNotified = false;
    constructor(launchEnvironment) {
        this.launchEnvironment = launchEnvironment;
    }
    async connect() {
        if (this.closed) {
            throw new Error("diff review protocol client is closed");
        }
        if (this.sessionClosing) {
            throw new Error("diff review protocol client is closing");
        }
        if (!this.connectPromise) {
            this.connectPromise = this.connectInternal().catch((error) => {
                this.connectPromise = undefined;
                throw error;
            });
        }
        await this.connectPromise;
    }
    onClose(listener) {
        this.closeListeners.add(listener);
        return () => {
            this.closeListeners.delete(listener);
        };
    }
    onSessionClose(listener) {
        this.sessionCloseListeners.add(listener);
        return () => {
            this.sessionCloseListeners.delete(listener);
        };
    }
    async close() {
        if (this.closed) {
            return;
        }
        this.closed = true;
        this.sessionClosing = true;
        this.rejectAllPending(new Error("diff review protocol client closed"));
        this.notifyCloseListeners();
        const socket = this.socket;
        const readline = this.readline;
        this.socket = undefined;
        this.readline = undefined;
        this.connectPromise = undefined;
        this.writeQueue = Promise.resolve();
        readline?.close();
        if (!socket) {
            return;
        }
        const closePromise = once(socket, "close").catch(() => undefined);
        socket.destroy();
        await closePromise;
    }
    async getContext() {
        return await this.call("session.get_context", {});
    }
    async listFiles() {
        return await this.call("session.list_files", {});
    }
    async getDiff(params = {}) {
        return await this.call("session.get_diff", params);
    }
    async setUiText(params) {
        return await this.call("session.set_ui_text", params);
    }
    async submitThreadMessage(params) {
        return await this.call("thread.submit_message", params);
    }
    async returnReview(params) {
        return await this.call("session.return_review", params);
    }
    async cancelSession() {
        return await this.call("session.cancel", {});
    }
    async call(method, params, options = {}) {
        if (this.closed) {
            throw new Error("diff review protocol client is closed");
        }
        if (this.sessionClosing) {
            throw new Error("diff review protocol client is closing");
        }
        if (!options.skipConnect) {
            await this.connect();
        }
        const socket = this.socket;
        if (!socket || socket.destroyed) {
            throw new Error("diff review protocol socket is not available");
        }
        const id = `req-${++this.requestCounter}`;
        const request = {
            version: DIFF_REVIEW_PROTOCOL_VERSION,
            type: "request",
            id,
            method,
            params,
        };
        return await new Promise((resolve, reject) => {
            this.pendingRequests.set(id, {
                resolve: (result) => resolve(result),
                reject,
            });
            const writePromise = this.writeQueue.then(() => this.writeMessage(socket, request));
            this.writeQueue = writePromise.catch(() => { });
            void writePromise.catch((error) => {
                const pending = this.pendingRequests.get(id);
                if (!pending) {
                    return;
                }
                this.pendingRequests.delete(id);
                pending.reject(error instanceof Error ? error : new Error(String(error)));
            });
        });
    }
    async writeMessage(socket, message) {
        if (this.closed ||
            this.socket !== socket ||
            socket.destroyed ||
            socket.writableEnded ||
            !socket.writable) {
            throw new Error("diff review protocol socket is not available");
        }
        const payload = serializeDiffReviewMessage(message);
        await new Promise((resolve, reject) => {
            let settled = false;
            const finish = (callback) => {
                if (settled) {
                    return;
                }
                settled = true;
                socket.off("close", onClose);
                socket.off("error", onError);
                callback();
            };
            const onClose = () => {
                finish(() => {
                    reject(new Error("diff review protocol connection closed"));
                });
            };
            const onError = (error) => {
                finish(() => {
                    reject(normalizeSocketWriteError(error));
                });
            };
            socket.once("close", onClose);
            socket.once("error", onError);
            try {
                socket.write(payload, (error) => {
                    if (error) {
                        finish(() => {
                            reject(normalizeSocketWriteError(error));
                        });
                        return;
                    }
                    finish(resolve);
                });
            }
            catch (error) {
                finish(() => {
                    reject(normalizeSocketWriteError(error));
                });
            }
        });
    }
    async connectInternal() {
        const socket = createConnection(this.launchEnvironment.socketPath);
        const readline = createInterface({ input: socket, crlfDelay: Number.POSITIVE_INFINITY });
        this.socket = socket;
        this.readline = readline;
        readline.on("line", (line) => {
            void this.handleLine(line);
        });
        readline.on("error", () => { });
        socket.on("close", () => {
            this.rejectAllPending(new Error("diff review protocol connection closed"));
            this.socket = undefined;
            this.readline = undefined;
            this.connectPromise = undefined;
            this.writeQueue = Promise.resolve();
            this.notifyCloseListeners();
        });
        socket.on("error", (error) => {
            this.rejectAllPending(normalizeSocketWriteError(error));
        });
        await new Promise((resolve, reject) => {
            const onError = (error) => {
                socket.off("connect", onConnect);
                reject(error);
            };
            const onConnect = () => {
                socket.off("error", onError);
                resolve();
            };
            socket.once("error", onError);
            socket.once("connect", onConnect);
        });
        await this.initialize();
    }
    async initialize() {
        const result = await this.call("initialize", { token: this.launchEnvironment.token }, { skipConnect: true });
        if (result.protocolVersion !== DIFF_REVIEW_PROTOCOL_VERSION) {
            throw new Error(`unsupported diff review protocol version: ${String(result.protocolVersion)}`);
        }
    }
    async handleLine(line) {
        const parsed = parseDiffReviewMessageLine(line);
        if (!parsed.ok) {
            this.rejectAllPending(new Error(parsed.error.message));
            return;
        }
        if (parsed.message.type === "response") {
            this.handleResponse(parsed.message);
            return;
        }
        await this.handleRequest(parsed.message);
    }
    handleResponse(message) {
        if (message.id === null) {
            this.rejectAllPending(new Error("diff review response did not include a request id"));
            return;
        }
        const pending = this.pendingRequests.get(message.id);
        if (!pending) {
            return;
        }
        this.pendingRequests.delete(message.id);
        if (message.ok) {
            pending.resolve(message.result);
            return;
        }
        pending.reject(new DiffReviewProtocolClientError(message.error.code, message.error.message, message.error.data));
    }
    async handleRequest(message) {
        if (message.method !== "session.close") {
            await this.sendResponse(createDiffReviewErrorResponse(message.id, "method_not_found", `unsupported diff review method '${message.method}'`));
            return;
        }
        if (!this.sessionClosing) {
            this.sessionClosing = true;
        }
        await this.sendResponse(createDiffReviewSuccessResponse(message.id, { status: "closed" }));
        try {
            await this.notifySessionCloseListeners();
        }
        finally {
            await this.close();
        }
    }
    async sendResponse(message) {
        const socket = this.socket;
        if (!socket || socket.destroyed) {
            return;
        }
        try {
            const writePromise = this.writeQueue.then(() => this.writeMessage(socket, message));
            this.writeQueue = writePromise.catch(() => { });
            await writePromise;
        }
        catch {
            if (!socket.destroyed) {
                socket.destroy();
            }
        }
    }
    rejectAllPending(error) {
        for (const [id, pending] of this.pendingRequests) {
            this.pendingRequests.delete(id);
            pending.reject(error);
        }
    }
    async notifySessionCloseListeners() {
        for (const listener of this.sessionCloseListeners) {
            await listener();
        }
    }
    notifyCloseListeners() {
        if (this.closeNotified) {
            return;
        }
        this.closeNotified = true;
        for (const listener of this.closeListeners) {
            listener();
        }
    }
}
function normalizeSocketWriteError(error) {
    if (error &&
        typeof error === "object" &&
        "code" in error &&
        (error.code === "EPIPE" || error.code === "ECONNRESET")) {
        return new Error("diff review protocol connection closed");
    }
    return error instanceof Error ? error : new Error(String(error));
}
export function parseDiffToolLaunchEnvironment(env = process.env) {
    const socketPath = env.TAU_DIFF_SOCKET?.trim();
    if (!socketPath) {
        throw new DiffToolLaunchEnvironmentError("missing TAU_DIFF_SOCKET from the Tau diff review session environment");
    }
    const token = env.TAU_DIFF_TOKEN?.trim();
    if (!token) {
        throw new DiffToolLaunchEnvironmentError("missing TAU_DIFF_TOKEN from the Tau diff review session environment");
    }
    const protocolVersionRaw = env.TAU_DIFF_PROTOCOL_VERSION?.trim();
    const protocolVersion = Number(protocolVersionRaw);
    if (!Number.isInteger(protocolVersion)) {
        throw new DiffToolLaunchEnvironmentError("TAU_DIFF_PROTOCOL_VERSION must be an integer");
    }
    if (protocolVersion !== DIFF_REVIEW_PROTOCOL_VERSION) {
        throw new DiffToolLaunchEnvironmentError(`unsupported diff review protocol version: ${String(protocolVersion)}`);
    }
    let diffArgs = [];
    const diffArgsJson = env.TAU_DIFF_ARGS_JSON;
    if (diffArgsJson?.trim()) {
        let parsed;
        try {
            parsed = JSON.parse(diffArgsJson);
        }
        catch (error) {
            throw new DiffToolLaunchEnvironmentError(`TAU_DIFF_ARGS_JSON must be valid json: ${error instanceof Error ? error.message : String(error)}`);
        }
        if (!Array.isArray(parsed) || parsed.some((entry) => typeof entry !== "string")) {
            throw new DiffToolLaunchEnvironmentError("TAU_DIFF_ARGS_JSON must be a json string array");
        }
        diffArgs = [...parsed];
    }
    const codeTheme = env.TAU_DIFF_CODE_THEME?.trim();
    if (codeTheme && !codeThemes.has(codeTheme)) {
        throw new DiffToolLaunchEnvironmentError("TAU_DIFF_CODE_THEME is not supported");
    }
    return {
        protocolVersion,
        socketPath,
        token,
        sessionId: env.TAU_DIFF_SESSION_ID?.trim() || undefined,
        repoRoot: env.TAU_DIFF_REPO_ROOT?.trim() || undefined,
        cwd: env.TAU_DIFF_CWD?.trim() || undefined,
        diffArgs,
        ...(codeTheme ? { codeTheme: codeTheme } : {}),
    };
}
//# sourceMappingURL=protocol_client.js.map