import { randomUUID } from 'node:crypto';
import { EventEmitter } from 'node:events';
import { execa } from 'execa';
import { config } from '../config.js';
import { logger } from './logger.js';
class ProcessManager {
    processes = new Map();
    start(options) {
        if (this.runningCount() >= config.maxConcurrentProcesses) {
            throw new Error(`Refusing to start another process: already at the max of ${config.maxConcurrentProcesses} concurrent managed processes. Stop one first with stop_process.`);
        }
        const id = randomUUID();
        const emitter = new EventEmitter();
        const subprocess = execa(options.command, options.args, {
            cwd: options.cwd,
            env: options.env,
            reject: false,
            stdin: 'ignore',
            forceKillAfterDelay: 5000,
        });
        const managed = {
            id,
            label: options.label,
            command: options.command,
            args: options.args,
            cwd: options.cwd,
            pid: subprocess.pid,
            status: 'running',
            exitCode: null,
            startedAt: new Date().toISOString(),
            emitter,
            logs: [],
            subprocess,
        };
        const appendLog = (chunk) => {
            const text = chunk.toString();
            for (const line of text.split(/\r?\n/)) {
                if (line.length === 0)
                    continue;
                managed.logs.push(line);
                if (managed.logs.length > config.maxBufferedLogLines) {
                    managed.logs.splice(0, managed.logs.length - config.maxBufferedLogLines);
                }
                emitter.emit('log', line);
            }
        };
        subprocess.stdout?.on('data', appendLog);
        subprocess.stderr?.on('data', appendLog);
        subprocess
            .then((result) => {
            managed.status = managed.status === 'stopped' ? 'stopped' : 'exited';
            managed.exitCode = result.exitCode ?? null;
            managed.exitedAt = new Date().toISOString();
            emitter.emit('exit');
        })
            .catch((error) => {
            managed.status = 'error';
            managed.exitedAt = new Date().toISOString();
            appendLog(`[process error] ${error instanceof Error ? error.message : String(error)}`);
            emitter.emit('exit');
            logger.error(`managed process ${id} failed`, { error: error instanceof Error ? error.message : error });
        });
        this.processes.set(id, managed);
        return this.toInfo(managed);
    }
    /**
     * Resolves once a log line matches `pattern`, the process exits, or `timeoutMs` elapses.
     * Used by start_dev_server so it can report the actual "ready" URL instead of guessing.
     */
    waitForLog(id, pattern, timeoutMs) {
        const managed = this.require(id);
        const existing = managed.logs.find((line) => pattern.test(line));
        if (existing)
            return Promise.resolve({ matchedLine: existing, timedOut: false });
        return new Promise((resolve) => {
            const cleanup = () => {
                clearTimeout(timer);
                managed.emitter.off('log', onLog);
                managed.emitter.off('exit', onExit);
            };
            const onLog = (line) => {
                if (pattern.test(line)) {
                    cleanup();
                    resolve({ matchedLine: line, timedOut: false });
                }
            };
            const onExit = () => {
                cleanup();
                resolve({ matchedLine: null, timedOut: false });
            };
            const timer = setTimeout(() => {
                cleanup();
                resolve({ matchedLine: null, timedOut: true });
            }, timeoutMs);
            managed.emitter.on('log', onLog);
            managed.emitter.on('exit', onExit);
        });
    }
    stop(id) {
        const managed = this.require(id);
        if (managed.status === 'running') {
            managed.status = 'stopped';
            managed.subprocess.kill('SIGTERM');
        }
        return this.toInfo(managed);
    }
    getLogs(id, tail) {
        const managed = this.require(id);
        const logs = tail && tail > 0 ? managed.logs.slice(-tail) : managed.logs.slice();
        return { info: this.toInfo(managed), logs };
    }
    list() {
        return Array.from(this.processes.values()).map((p) => this.toInfo(p));
    }
    stopAll() {
        for (const managed of this.processes.values()) {
            if (managed.status === 'running') {
                managed.subprocess.kill('SIGTERM');
            }
        }
    }
    runningCount() {
        return Array.from(this.processes.values()).filter((p) => p.status === 'running').length;
    }
    require(id) {
        const managed = this.processes.get(id);
        if (!managed) {
            throw new Error(`Unknown processId "${id}". It may have already exited and been forgotten, or never existed.`);
        }
        return managed;
    }
    toInfo(p) {
        return {
            id: p.id,
            label: p.label,
            command: p.command,
            args: p.args,
            cwd: p.cwd,
            pid: p.pid,
            status: p.status,
            exitCode: p.exitCode,
            startedAt: p.startedAt,
            exitedAt: p.exitedAt,
        };
    }
}
export const processManager = new ProcessManager();
//# sourceMappingURL=processManager.js.map