/**
 * @module        sessionDaemon
 * @description   会话守护进程，管理持久化 ACP 会话的后台服务
 *
 * @author        eddy
 * @organization  wykj
 * @ownership     wykj/eddy
 *
 * @created       2026-03-31
 * @updated       2026-04-03
 *
 * @role          daemon
 * @layer         daemon
 * @boundedContext session-management
 */
import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from 'node:fs';
import { createServer } from 'node:net';
import { dirname, resolve } from 'node:path';
import { appendAudit } from '../core/auditLog.js';
import { createSessionContext } from '../core/sessionContext.js';
import { createSessionRuntime } from '../providers/registry.js';
const DEFAULT_SOCKET = resolve(process.env.HOME || '/home/coral', '.coral', 'sessions', 'daemon.sock');
const DEFAULT_PID = resolve(process.env.HOME || '/home/coral', '.coral', 'sessions', 'daemon.pid');
export class SessionDaemon {
    socketPath;
    pidFile;
    server = null;
    pollTimer = null;
    runtime;
    ctx;
    clients = new Set();
    constructor(socketPath = DEFAULT_SOCKET, pidFile = DEFAULT_PID) {
        this.socketPath = socketPath;
        this.pidFile = pidFile;
        this.ctx = createSessionContext();
        this.runtime = createSessionRuntime(this.ctx);
    }
    async start() {
        mkdirSync(dirname(this.socketPath), { recursive: true });
        // Check if another daemon is already running (PID file race protection)
        if (existsSync(this.pidFile)) {
            try {
                const oldPid = parseInt(readFileSync(this.pidFile, 'utf-8').trim(), 10);
                if (oldPid > 0) {
                    process.kill(oldPid, 0); // signal 0: test if alive
                    throw new Error(`Daemon already running (PID ${oldPid})`);
                }
            }
            catch (err) {
                if (err.code !== 'ESRCH')
                    throw err; // ESRCH = process not found = stale PID, safe to continue
            }
        }
        // Clean stale socket
        if (existsSync(this.socketPath)) {
            try {
                unlinkSync(this.socketPath);
            }
            catch { /* noop */ }
        }
        // Write PID file
        writeFileSync(this.pidFile, String(process.pid));
        // Start Unix socket server
        this.server = createServer((socket) => this.handleClient(socket));
        this.server.listen(this.socketPath, () => {
            this.log(`Daemon listening on ${this.socketPath} (pid ${process.pid})`);
            try {
                appendAudit({ action: 'daemon', project: '(daemon)', detail: `daemon 启动(pid ${process.pid})`, source: 'daemon' });
            }
            catch { /* 审计不阻塞 */ }
        });
        // Poll sessions every 2s to keep state.json fresh
        this.pollTimer = setInterval(() => this.pollSessions(), 2_000);
        // Graceful shutdown on signals and unexpected exit(记录触发源用于审计)
        process.on('SIGINT', () => this.shutdown('SIGINT'));
        process.on('SIGTERM', () => this.shutdown('SIGTERM'));
        process.on('exit', () => {
            // Sync cleanup: remove socket and PID files so clients don't connect to dead daemon
            try {
                unlinkSync(this.socketPath);
            }
            catch { /* noop */ }
            try {
                unlinkSync(this.pidFile);
            }
            catch { /* noop */ }
        });
    }
    handleClient(socket) {
        this.clients.add(socket);
        let buffer = '';
        socket.on('data', (chunk) => {
            buffer += chunk.toString('utf-8');
            const lines = buffer.split('\n');
            buffer = lines.pop() || '';
            for (const line of lines) {
                if (!line.trim())
                    continue;
                this.handleMessage(socket, line).catch((err) => {
                    this.log(`Client error: ${err instanceof Error ? err.message : String(err)}`);
                });
            }
        });
        socket.on('close', () => { this.clients.delete(socket); });
        socket.on('error', () => { this.clients.delete(socket); });
    }
    async handleMessage(socket, raw) {
        let req;
        try {
            req = JSON.parse(raw);
        }
        catch {
            this.send(socket, { ok: false, error: 'Invalid JSON' });
            return;
        }
        // subscribeRun keeps socket open for NDJSON event streaming — handled specially
        if (req.method === 'subscribeRun') {
            this.handleSubscribeRun(socket, req);
            return;
        }
        try {
            const res = await this.dispatch(req);
            this.send(socket, { id: req.id, ...res });
        }
        catch (err) {
            this.send(socket, {
                id: req.id,
                ok: false,
                error: err instanceof Error ? err.message : String(err),
            });
        }
    }
    /**
     * Long-lived subscription: hooks listener on the session's accumulator, pipes
     * structured events as NDJSON. Socket stays open until 'complete' fires or
     * client disconnects.
     */
    handleSubscribeRun(socket, req) {
        const slot = req.params.slot;
        if (!slot) {
            this.send(socket, { id: req.id, ok: false, error: 'slot required' });
            socket.end();
            return;
        }
        const unsubscribe = this.runtime.subscribe(slot, (evt) => {
            // Translate AccumulatorEvent → DaemonEvent (1:1 mapping, just rename type→event)
            const { type, ...rest } = evt;
            this.send(socket, { event: type, ...rest });
            if (evt.type === 'complete') {
                socket.end();
            }
        });
        // ack with subscription confirmation so client knows listener is hooked
        this.send(socket, { id: req.id, ok: true, data: { subscribed: true } });
        socket.on('close', unsubscribe);
        socket.on('error', unsubscribe);
    }
    async dispatch(req) {
        const p = req.params;
        switch (req.method) {
            case 'ensureSession': {
                const opts = p.opts;
                const session = await this.runtime.ensureSession(p.slot, p.tool, p.cwd, opts);
                return { ok: true, data: session };
            }
            case 'startRun': {
                const opts = p.opts;
                const session = await this.runtime.startRun(p.slot, p.prompt, p.tool, p.cwd, opts);
                return { ok: true, data: session };
            }
            case 'inspect': {
                const state = await this.runtime.inspect(p.slot);
                return { ok: true, data: state };
            }
            case 'stopSession': {
                await this.runtime.stopSession(p.slot);
                return { ok: true };
            }
            case 'cancelRun': {
                await this.runtime.cancelRun(p.slot);
                return { ok: true };
            }
            case 'clearRun': {
                const { readState, writeState } = await import('../core/state.js');
                const state = readState(this.ctx.paths.stateFile, 0);
                const s = state.sessions?.[p.slot];
                if (s?.currentRun) {
                    s.currentRun = null;
                    s.status = 'idle';
                    writeState(this.ctx.paths.stateFile, state, 'daemon-clear-run');
                }
                return { ok: true };
            }
            case 'shutdown': {
                this.shutdown('command');
                return { ok: true };
            }
            default:
                return { ok: false, error: `Unknown method: ${req.method}` };
        }
    }
    async pollSessions() {
        try {
            const state = await this.runtime.inspect();
            // TTL cleanup: stop sessions idle longer than SESSION_TTL_MS
            const ttlMs = parseInt(process.env.SPS_SESSION_TTL_HOURS || '4', 10) * 3_600_000;
            const now = Date.now();
            for (const [slot, session] of Object.entries(state.sessions)) {
                if (!session.currentRun && session.lastSeenAt) {
                    const idleMs = now - Date.parse(session.lastSeenAt);
                    if (idleMs > ttlMs) {
                        const idleMin = Math.round(idleMs / 60000);
                        this.log(`TTL expired for ${slot} (idle ${idleMin}min) — stopping`);
                        try {
                            await this.runtime.stopSession(slot);
                            try {
                                appendAudit({ action: 'daemon', project: '(daemon)', detail: `会话 ${slot} 空闲 ${idleMin}min 超 TTL,自动停止`, source: 'daemon' });
                            }
                            catch { /* 审计不阻塞 */ }
                        }
                        catch (err) {
                            this.log(`Failed to stop expired session ${slot}: ${err instanceof Error ? err.message : String(err)}`);
                        }
                    }
                }
            }
        }
        catch (err) {
            this.log(`Poll error: ${err instanceof Error ? err.message : String(err)}`);
        }
    }
    shuttingDown = false;
    /** trigger:触发源(SIGTERM/SIGINT 信号 或 'command' socket 命令),记录到审计便于排查"会话为何被关"。 */
    async shutdown(trigger = 'unknown') {
        if (this.shuttingDown)
            return; // 防重复(信号可能连发两次)
        this.shuttingDown = true;
        this.log(`Shutting down (trigger=${trigger})...`);
        if (this.pollTimer)
            clearInterval(this.pollTimer);
        if (this.server)
            this.server.close();
        // Stop all sessions(统计数量用于审计)
        let stopped = 0;
        try {
            const state = await this.runtime.inspect();
            const slots = Object.keys(state.sessions);
            stopped = slots.length;
            for (const slot of slots) {
                try {
                    await this.runtime.stopSession(slot);
                }
                catch { /* best effort */ }
            }
        }
        catch { /* noop */ }
        // 审计:记录 daemon 关闭(触发源 + 受影响会话数)→ console 审计页可查"会话为何被回收"。
        try {
            appendAudit({ action: 'daemon', project: '(daemon)', detail: `daemon 关闭(${trigger}),停止 ${stopped} 个会话`, source: 'daemon' });
        }
        catch { /* 审计不阻塞关闭 */ }
        // Clean up files
        try {
            unlinkSync(this.socketPath);
        }
        catch { /* noop */ }
        try {
            unlinkSync(this.pidFile);
        }
        catch { /* noop */ }
        process.exit(0);
    }
    send(socket, msg) {
        try {
            if (!socket.destroyed) {
                socket.write(JSON.stringify(msg) + '\n');
            }
        }
        catch { /* client disconnected */ }
    }
    log(msg) {
        const ts = new Date().toISOString().slice(11, 23);
        process.stderr.write(`[daemon ${ts}] ${msg}\n`);
    }
}
// Direct execution entry point
const isDirectRun = process.argv[1]?.endsWith('sessionDaemon.js');
if (isDirectRun) {
    const daemon = new SessionDaemon();
    daemon.start().catch((err) => {
        process.stderr.write(`Daemon failed: ${err instanceof Error ? err.message : String(err)}\n`);
        process.exit(1);
    });
}
//# sourceMappingURL=sessionDaemon.js.map