import { randomUUID } from "node:crypto";
import { connect } from "node:net";
import { DEFAULT_REMOTE_AGENT_HOST, DEFAULT_REMOTE_AGENT_PORT, encodeRemoteAgentFrame, } from "./protocol.js";
function parseJsonLine(line) {
    try {
        return JSON.parse(line);
    }
    catch {
        return undefined;
    }
}
function isBinding(value) {
    if (!value || typeof value !== "object")
        return false;
    const candidate = value;
    return typeof candidate.sessionKey === "string" && typeof candidate.originalProfile === "string";
}
function isCapabilities(value) {
    if (!value || typeof value !== "object")
        return false;
    const candidate = value;
    return Array.isArray(candidate.actions);
}
export function createRemoteSlashCommandMap(capabilities) {
    const commands = new Map();
    for (const action of capabilities.actions) {
        for (const slashCommand of action.slashCommands) {
            commands.set(`/${slashCommand}`, action.name);
        }
    }
    return commands;
}
export class RemoteAgentSessionClient {
    socket;
    binding;
    capabilities;
    eventListeners = new Set();
    pendingResponses = new Map();
    buffer = "";
    constructor(socket, attached) {
        this.socket = socket;
        this.binding = attached.binding;
        this.capabilities = attached.capabilities;
        this.socket.on("data", (chunk) => this.handleData(String(chunk)));
        this.socket.once("close", () => this.rejectPendingResponses(new Error("Remote agent connection closed")));
        this.socket.once("error", (error) => this.rejectPendingResponses(error));
    }
    static async connect(options = {}) {
        const resolvedOptions = {
            host: options.host ?? DEFAULT_REMOTE_AGENT_HOST,
            port: options.port ?? DEFAULT_REMOTE_AGENT_PORT,
            sessionName: options.sessionName ?? "default",
            profile: options.profile ?? "pibo-minimal",
        };
        const socket = connect({ host: resolvedOptions.host, port: resolvedOptions.port });
        socket.setEncoding("utf-8");
        await new Promise((resolve, reject) => {
            socket.once("connect", resolve);
            socket.once("error", reject);
        });
        const attached = await attachRemoteAgent(socket, resolvedOptions);
        return new RemoteAgentSessionClient(socket, attached);
    }
    onEvent(listener) {
        this.eventListeners.add(listener);
        return () => this.eventListeners.delete(listener);
    }
    getSlashCommands() {
        return createRemoteSlashCommandMap(this.capabilities);
    }
    sendMessage(text) {
        return this.sendInput({ type: "message", text });
    }
    sendExecution(action, params) {
        return this.sendInput(params === undefined ? { type: "execution", action } : { type: "execution", action, params });
    }
    close() {
        this.socket.end();
    }
    sendInput(input) {
        return this.sendRequest({ type: "remote_input", id: randomUUID(), input });
    }
    sendRequest(frame) {
        if (this.socket.destroyed) {
            return Promise.reject(new Error("Remote agent connection is closed"));
        }
        return new Promise((resolve, reject) => {
            this.pendingResponses.set(frame.id, { resolve, reject });
            this.socket.write(encodeRemoteAgentFrame(frame));
        });
    }
    handleData(chunk) {
        this.buffer += chunk;
        let newlineIndex = this.buffer.indexOf("\n");
        while (newlineIndex !== -1) {
            const line = this.buffer.slice(0, newlineIndex).trim();
            this.buffer = this.buffer.slice(newlineIndex + 1);
            this.handleFrame(line ? parseJsonLine(line) : undefined);
            newlineIndex = this.buffer.indexOf("\n");
        }
    }
    handleFrame(frame) {
        if (!frame)
            return;
        if (frame.type === "remote_res") {
            const pending = this.pendingResponses.get(frame.id);
            if (!pending)
                return;
            this.pendingResponses.delete(frame.id);
            if (frame.ok) {
                pending.resolve(frame);
            }
            else {
                pending.reject(new Error(frame.error?.message ?? "Remote request failed"));
            }
            return;
        }
        if (frame.type === "remote_event") {
            for (const listener of this.eventListeners) {
                listener(frame.payload);
            }
        }
    }
    rejectPendingResponses(error) {
        for (const pending of this.pendingResponses.values()) {
            pending.reject(error);
        }
        this.pendingResponses.clear();
    }
}
async function attachRemoteAgent(socket, options) {
    const id = randomUUID();
    return await new Promise((resolve, reject) => {
        let buffer = "";
        const cleanup = () => {
            socket.off("data", onData);
            socket.off("error", onError);
            socket.off("close", onClose);
        };
        const onError = (error) => {
            cleanup();
            reject(error);
        };
        const onClose = () => {
            cleanup();
            reject(new Error("Remote agent connection closed before attach completed"));
        };
        const onData = (chunk) => {
            buffer += String(chunk);
            let newlineIndex = buffer.indexOf("\n");
            while (newlineIndex !== -1) {
                const line = buffer.slice(0, newlineIndex).trim();
                buffer = buffer.slice(newlineIndex + 1);
                const frame = line ? parseJsonLine(line) : undefined;
                if (frame?.type === "remote_res" && frame.id === id) {
                    cleanup();
                    if (!frame.ok) {
                        reject(new Error(frame.error?.message ?? "Remote attach failed"));
                        return;
                    }
                    const payload = frame.payload;
                    if (!isBinding(payload?.binding)) {
                        reject(new Error("Remote attach response did not include a binding"));
                        return;
                    }
                    if (!isCapabilities(payload.capabilities)) {
                        reject(new Error("Remote attach response did not include capabilities"));
                        return;
                    }
                    resolve({
                        binding: payload.binding,
                        capabilities: payload.capabilities,
                    });
                    return;
                }
                newlineIndex = buffer.indexOf("\n");
            }
        };
        socket.on("data", onData);
        socket.once("error", onError);
        socket.once("close", onClose);
        socket.write(encodeRemoteAgentFrame({
            type: "remote_attach",
            id,
            sessionName: options.sessionName,
            profile: options.profile,
        }));
    });
}
