import { existsSync } from 'node:fs';
import { homedir } from 'node:os';
import { resolve } from 'node:path';
import { parseShellConf, } from '../core/shellEnv.js';
import { extractLastAssistantText, isProcessAlive, killProcessGroup, parseClaudeSessionId } from '../providers/outputParser.js';
// ─── Supervisor ─────────────────────────────────────────────────
export class ProcessSupervisor {
    /** Global env: process.env + ~/.coral/env (Layer 1+2) */
    globalEnv;
    /** Active worker handles by ID */
    workers = new Map();
    /** Orphan PID poll timers (for Recovery) */
    orphanTimers = new Map();
    /** Pending PostAction promises (from exit callbacks) */
    pendingActions = [];
    constructor() {
        this.globalEnv = this.loadGlobalEnv();
    }
    // ─── Spawn (removed — all workers use ACP transport via AgentRuntime) ──
    // ─── Kill / Query ───────────────────────────────────────────
    async kill(id) {
        const handle = this.workers.get(id);
        if (!handle)
            return;
        if (handle.pid && handle.pid > 0 && isProcessAlive(handle.pid)) {
            await killProcessGroup(handle.pid);
        }
        this.workers.delete(id);
    }
    get(id) {
        return this.workers.get(id);
    }
    getByProject(project) {
        return Array.from(this.workers.values()).filter(w => w.project === project);
    }
    getAll() {
        return Array.from(this.workers.values());
    }
    remove(id) {
        this.workers.delete(id);
        const timer = this.orphanTimers.get(id);
        if (timer) {
            clearInterval(timer);
            this.orphanTimers.delete(id);
        }
    }
    get activeCount() {
        return this.workers.size;
    }
    registerAcpHandle(handle) {
        return this.upsertHandle(this.normalizeAcpHandle(handle));
    }
    updateAcpHandle(id, patch) {
        const current = this.workers.get(id);
        if (!current)
            return undefined;
        return this.upsertHandle(this.normalizeAcpHandle({ ...current, ...patch }));
    }
    /**
     * Wait for all pending PostAction promises to settle.
     * Called by tick after each cycle to ensure exit callbacks complete.
     */
    async drainPendingActions() {
        if (this.pendingActions.length === 0)
            return;
        await Promise.allSettled(this.pendingActions);
        this.pendingActions.length = 0; // clear
    }
    // ─── Orphan PID Monitoring (Recovery mode) ──────────────────
    /**
     * Monitor a PID that we don't have a child handle for (tick restarted).
     * Polls every 5s; when PID dies, calls onDead.
     */
    monitorOrphanPid(id, pid, handle, onDead) {
        // Clear any existing orphan timer for this ID to prevent stale polls
        const existingTimer = this.orphanTimers.get(id);
        if (existingTimer) {
            clearInterval(existingTimer);
            this.orphanTimers.delete(id);
        }
        // Store handle without child reference
        const orphanHandle = {
            ...handle,
            transport: 'acp-sdk',
            child: null,
            pid,
            outputFile: handle.outputFile ?? null,
            runId: handle.runId ?? null,
            sessionState: handle.sessionState ?? null,
            remoteStatus: handle.remoteStatus ?? null,
            lastEventAt: handle.lastEventAt ?? null,
        };
        this.upsertHandle(orphanHandle);
        const timer = setInterval(() => {
            if (!isProcessAlive(pid)) {
                clearInterval(timer);
                this.orphanTimers.delete(id);
                // Infer exit code from output file instead of hardcoding 1
                const inferredCode = this.inferExitCode(orphanHandle);
                orphanHandle.exitCode = inferredCode;
                orphanHandle.exitedAt = new Date().toISOString();
                // Try to extract session ID
                if (!orphanHandle.sessionId) {
                    orphanHandle.sessionId = this.extractSessionId(orphanHandle);
                }
                this.log(`Orphan PID ${pid} (${id}) is dead (inferred exitCode=${inferredCode}), triggering completion check`);
                // Track the promise like spawn exit callbacks
                const result = onDead(inferredCode);
                if (result && typeof result.then === 'function') {
                    const promise = result.catch(err => {
                        this.log(`PostActions error for orphan ${id}: ${err}`);
                    });
                    this.pendingActions.push(promise);
                }
            }
        }, 5_000);
        this.orphanTimers.set(id, timer);
        this.log(`Monitoring orphan PID ${pid} for ${id} (5s poll)`);
    }
    /**
     * Infer exit code for an orphan worker by checking output file.
     * If output contains a successful result, assume exit code 0.
     */
    inferExitCode(handle) {
        if (!handle.outputFile)
            return 1;
        try {
            const lastText = extractLastAssistantText(handle.outputFile);
            // If worker produced meaningful output and said "done", likely succeeded
            if (/\b(done|完成|全部完成|已推送)\b|🎉/i.test(lastText)) {
                return 0;
            }
        }
        catch { /* can't read output */ }
        return 1; // default to failure if can't determine
    }
    // ─── Environment Loading ────────────────────────────────────
    loadGlobalEnv() {
        const env = { ...process.env };
        const envPath = resolve(homedir(), '.coral', 'env');
        if (existsSync(envPath)) {
            Object.assign(env, parseShellConf(envPath));
        }
        return env;
    }
    reloadGlobalEnv() {
        this.globalEnv = this.loadGlobalEnv();
        this.log('Global environment reloaded');
    }
    // ─── Helpers ────────────────────────────────────────────────
    extractSessionId(handle) {
        if (!handle.outputFile)
            return null;
        return parseClaudeSessionId(handle.outputFile);
    }
    log(msg) {
        process.stderr.write(`[supervisor] ${msg}\n`);
    }
    normalizeAcpHandle(handle) {
        return {
            ...handle,
            transport: 'acp-sdk',
            pid: null,
            child: handle.child ?? null,
            outputFile: handle.outputFile ?? null,
            runId: handle.runId ?? null,
            sessionState: handle.sessionState ?? null,
            remoteStatus: handle.remoteStatus ?? null,
            lastEventAt: handle.lastEventAt ?? null,
        };
    }
    upsertHandle(handle) {
        this.workers.set(handle.id, handle);
        return handle;
    }
}
//# sourceMappingURL=supervisor.js.map