/**
 * chat-daemon RPC client (v0.7.1 Phase B).
 *
 * Used by the MCP server when it wants to send a chat message but
 * doesn't want to start its own long-poll adapter (which would
 * collide with any other MCP server holding the same bot token).
 *
 * Behavior:
 *   - One short-lived connection per request — keeps the implementation
 *     trivial and avoids reconnect/backoff logic. Performance is fine
 *     for v0.7.1's expected traffic (a handful of agent reports per
 *     project session).
 *   - Returns a typed result on success.
 *   - Throws `DaemonUnreachableError` when the socket can't be reached
 *     so callers can fall back to in-process send.
 *   - Throws `DaemonRpcError` on JSON-RPC error responses.
 */
import { connect } from "node:net";
import { daemonSockAddress, } from "./protocol.js";
export class DaemonUnreachableError extends Error {
    constructor(message) {
        super(message);
        this.name = "DaemonUnreachableError";
    }
}
export class DaemonRpcError extends Error {
    code;
    constructor(message, code) {
        super(message);
        this.code = code;
        this.name = "DaemonRpcError";
    }
}
let requestCounter = 0;
export class DaemonClient {
    address;
    connectTimeoutMs;
    requestTimeoutMs;
    constructor(opts = {}) {
        this.address = daemonSockAddress(opts.dataRoot);
        this.connectTimeoutMs = opts.connectTimeoutMs ?? 1500;
        this.requestTimeoutMs = opts.requestTimeoutMs ?? 5000;
    }
    ping() {
        return this.call("ping", {});
    }
    listChannels() {
        return this.call("list_channels", {});
    }
    send(params) {
        return this.call("send", params);
    }
    createTopic(params) {
        return this.call("create_topic", params);
    }
    async call(method, params) {
        const sock = await this.openSocket();
        try {
            const id = ++requestCounter;
            const req = { jsonrpc: "2.0", id, method: method, params };
            sock.write(`${JSON.stringify(req)}\n`);
            const line = await readOneLine(sock, this.requestTimeoutMs);
            const res = JSON.parse(line);
            if ("error" in res) {
                throw new DaemonRpcError(res.error.message, res.error.code);
            }
            return res.result;
        }
        finally {
            sock.destroy();
        }
    }
    openSocket() {
        return new Promise((resolve, reject) => {
            const sock = connect({ path: this.address });
            const t = setTimeout(() => {
                sock.destroy();
                reject(new DaemonUnreachableError(`connect timeout (${this.connectTimeoutMs}ms)`));
            }, this.connectTimeoutMs);
            sock.once("connect", () => {
                clearTimeout(t);
                resolve(sock);
            });
            sock.once("error", (err) => {
                clearTimeout(t);
                // ENOENT (no socket file) / ECONNREFUSED (no listener) /
                // EACCES (permission) → daemon-unreachable. Other errors
                // propagate as the original error.
                if (err.code === "ENOENT" || err.code === "ECONNREFUSED" || err.code === "EACCES") {
                    reject(new DaemonUnreachableError(`${err.code}: ${err.message}`));
                }
                else {
                    reject(err);
                }
            });
        });
    }
}
function readOneLine(sock, timeoutMs) {
    return new Promise((resolve, reject) => {
        sock.setEncoding("utf8");
        let buf = "";
        const onData = (chunk) => {
            buf += chunk;
            const nl = buf.indexOf("\n");
            if (nl >= 0) {
                cleanup();
                resolve(buf.slice(0, nl));
            }
        };
        const onError = (err) => {
            cleanup();
            reject(err);
        };
        const onEnd = () => {
            cleanup();
            reject(new DaemonUnreachableError("daemon closed connection without response"));
        };
        const t = setTimeout(() => {
            cleanup();
            reject(new DaemonUnreachableError(`request timeout (${timeoutMs}ms)`));
        }, timeoutMs);
        const cleanup = () => {
            clearTimeout(t);
            sock.off("data", onData);
            sock.off("error", onError);
            sock.off("end", onEnd);
        };
        sock.on("data", onData);
        sock.on("error", onError);
        sock.on("end", onEnd);
    });
}
