/**
 * @module        daemonClient
 * @description   守护进程客户端，通过 Unix 域套接字与 SessionDaemon 通信
 *
 * @author        eddy
 * @organization  wykj
 * @ownership     wykj/eddy
 *
 * @created       2026-03-31
 * @updated       2026-04-03
 *
 * @role          daemon
 * @layer         daemon
 * @boundedContext session-management
 */
import { existsSync } from 'node:fs';
import { createConnection } from 'node:net';
import { resolve } from 'node:path';
const DEFAULT_SOCKET = process.env.SPS_DAEMON_SOCKET || resolve(process.env.HOME || '/home/coral', '.coral', 'sessions', 'daemon.sock');
export class DaemonClient {
    socketPath;
    constructor(socketPath = DEFAULT_SOCKET) {
        this.socketPath = socketPath;
    }
    /** Check if daemon is reachable. */
    async isRunning() {
        if (!existsSync(this.socketPath))
            return false;
        try {
            await this.request('inspect', {});
            return true;
        }
        catch {
            return false;
        }
    }
    async ensureSession(slot, tool, cwd, opts) {
        return this.request('ensureSession', { slot, tool, cwd, opts });
    }
    async startRun(slot, prompt, tool, cwd, opts) {
        return this.request('startRun', { slot, prompt, tool, cwd, opts });
    }
    async inspect(slot) {
        return this.request('inspect', { slot });
    }
    async stopSession(slot) {
        await this.request('stopSession', { slot });
    }
    async clearRun(slot) {
        await this.request('clearRun', { slot });
    }
    /** Cancel the in-flight run (prompt). Session stays alive for reuse. */
    async cancelRun(slot) {
        await this.request('cancelRun', { slot });
    }
    async shutdown() {
        try {
            await this.request('shutdown', {});
        }
        catch { /* daemon exits */ }
    }
    /**
     * Long-lived subscription: opens socket, sends subscribeRun, yields DaemonEvent
     * frames as async iterator. Terminates when daemon closes socket (run complete)
     * or when caller breaks out of loop (cancels via iterator.return).
     *
     * Usage:
     *   for await (const evt of client.subscribeRun(slot)) {
     *     if (evt.event === 'text') ...
     *     if (evt.event === 'complete') break;
     *   }
     */
    subscribeRun(slot) {
        const socket = createConnection(this.socketPath);
        const queue = [];
        const waiters = [];
        let done = false;
        let errored = null;
        let buffer = '';
        const close = () => {
            if (done)
                return;
            done = true;
            try {
                socket.destroy();
            }
            catch { /* noop */ }
            // drain waiters
            while (waiters.length > 0) {
                waiters.shift()({ value: undefined, done: true });
            }
        };
        socket.on('connect', () => {
            const req = { method: 'subscribeRun', params: { slot } };
            socket.write(JSON.stringify(req) + '\n');
        });
        socket.on('data', (chunk) => {
            buffer += chunk.toString('utf-8');
            let idx;
            while ((idx = buffer.indexOf('\n')) >= 0) {
                const line = buffer.slice(0, idx);
                buffer = buffer.slice(idx + 1);
                if (!line.trim())
                    continue;
                try {
                    const parsed = JSON.parse(line);
                    // Skip the initial ack frame (which is a DaemonResponse, has `ok`, no `event` key)
                    if ('ok' in parsed && !('event' in parsed)) {
                        if (!parsed.ok) {
                            errored = new Error(parsed.error ?? 'subscribe failed');
                            close();
                        }
                        continue;
                    }
                    const evt = parsed;
                    if (waiters.length > 0) {
                        waiters.shift()({ value: evt, done: false });
                    }
                    else {
                        queue.push(evt);
                    }
                }
                catch {
                    /* malformed line */
                }
            }
        });
        socket.on('error', (err) => {
            errored = err;
            close();
        });
        socket.on('close', () => close());
        const iterator = {
            next: () => new Promise((resolve, reject) => {
                if (errored)
                    return reject(errored);
                if (queue.length > 0)
                    return resolve({ value: queue.shift(), done: false });
                if (done)
                    return resolve({ value: undefined, done: true });
                waiters.push(resolve);
            }),
            return: () => {
                close();
                return Promise.resolve({ value: undefined, done: true });
            },
        };
        return {
            [Symbol.asyncIterator]: () => iterator,
            cancel: close,
        };
    }
    /** Send a single NDJSON request, wait for response. */
    request(method, params) {
        return new Promise((resolve, reject) => {
            const socket = createConnection(this.socketPath);
            let buffer = '';
            let settled = false;
            const finish = (err, data) => {
                if (settled)
                    return;
                settled = true;
                if (connectTimeout)
                    clearTimeout(connectTimeout);
                socket.destroy();
                if (err)
                    reject(err);
                else
                    resolve(data);
            };
            // 5s connection timeout (separate from 30s request timeout)
            const connectTimeout = setTimeout(() => {
                finish(new Error('Daemon connection timeout (5s)'));
            }, 5_000);
            socket.on('connect', () => {
                clearTimeout(connectTimeout);
                const req = { method: method, params };
                socket.write(JSON.stringify(req) + '\n');
            });
            socket.on('data', (chunk) => {
                buffer += chunk.toString('utf-8');
                const idx = buffer.indexOf('\n');
                if (idx < 0)
                    return;
                const line = buffer.slice(0, idx);
                try {
                    const res = JSON.parse(line);
                    if (res.ok)
                        finish(undefined, res.data);
                    else
                        finish(new Error(res.error ?? 'Daemon error'));
                }
                catch {
                    finish(new Error('Invalid daemon response'));
                }
            });
            socket.on('error', (err) => finish(err));
            socket.setTimeout(30_000, () => finish(new Error('Daemon request timeout')));
        });
    }
}
/** Get the default daemon socket path. */
export function getDaemonSocketPath() {
    return DEFAULT_SOCKET;
}
//# sourceMappingURL=daemonClient.js.map