import { spawn } from 'node:child_process';
import os from 'node:os';
import { resolveConfig } from './config.js';
import { CodoriError } from './errors.js';
import { findAvailablePort } from './ports.js';
import { scanProjects } from './project-scanner.js';
import { RuntimeStore } from './runtime-store.js';
const CODORI_STOP_TIMEOUT_MS = 3_000;
const CODORI_STOP_POLL_MS = 50;
const defaultCommandFactory = (port) => ({
    command: process.env.CODORI_CODEX_BIN ?? 'codex',
    args: ['app-server', '--listen', `ws://0.0.0.0:${port}`]
});
const isProcessAlive = (pid) => {
    try {
        process.kill(pid, 0);
        return true;
    }
    catch {
        return false;
    }
};
const wait = async (ms) => new Promise((resolvePromise) => {
    setTimeout(resolvePromise, ms);
});
const waitForExit = async (pid, timeoutMs) => {
    const deadline = Date.now() + timeoutMs;
    while (Date.now() < deadline) {
        if (!isProcessAlive(pid)) {
            return true;
        }
        await wait(CODORI_STOP_POLL_MS);
    }
    return !isProcessAlive(pid);
};
const spawnDetached = async (command, args, cwd) => new Promise((resolvePromise, reject) => {
    const child = spawn(command, args, {
        cwd,
        detached: true,
        stdio: 'ignore',
        windowsHide: true
    });
    child.once('error', reject);
    child.once('spawn', () => {
        child.unref();
        resolvePromise(child);
    });
});
export class RuntimeManager {
    config;
    store;
    commandFactory;
    activeSessions = new Map();
    idleReaper = null;
    idleSweepInFlight = false;
    constructor(options = {}) {
        this.config = options.config ?? resolveConfig(options.configOverrides, options.homeDir);
        this.store = new RuntimeStore(options.homeDir);
        this.commandFactory = options.commandFactory ?? defaultCommandFactory;
        if (this.config.idleShutdown.enabled) {
            this.idleReaper = setInterval(() => {
                void this.reapIdleRuntimes();
            }, this.config.idleShutdown.sweepIntervalMs);
            this.idleReaper.unref?.();
        }
    }
    listProjects() {
        return scanProjects(this.config.root);
    }
    resolveProject(projectId) {
        const project = this.listProjects().find(entry => entry.id === projectId);
        if (!project) {
            throw new CodoriError('PROJECT_NOT_FOUND', `Project "${projectId}" was not found under ${this.config.root}.`);
        }
        return project;
    }
    normalizeStatus(project, runtime, error) {
        const activeSessionCount = this.getActiveSessionCount(project.id);
        return {
            projectId: project.id,
            projectPath: project.path,
            status: error ? 'error' : runtime ? 'running' : 'stopped',
            pid: runtime?.pid ?? null,
            port: runtime?.port ?? null,
            startedAt: runtime?.startedAt ?? null,
            lastActivityAt: runtime?.lastActivityAt ?? null,
            activeSessionCount,
            idleTimeoutMs: this.config.idleShutdown.enabled ? this.config.idleShutdown.timeoutMs : null,
            idleDeadlineAt: this.resolveIdleDeadline(runtime, activeSessionCount),
            error
        };
    }
    getActiveSessionCount(projectId) {
        return this.activeSessions.get(projectId) ?? 0;
    }
    resolveIdleDeadline(runtime, activeSessionCount) {
        if (!runtime || !this.config.idleShutdown.enabled || activeSessionCount > 0) {
            return null;
        }
        return runtime.lastActivityAt + this.config.idleShutdown.timeoutMs;
    }
    writeRuntime(record) {
        this.store.write(record);
        return record;
    }
    touchRuntimeRecord(record, at = Date.now()) {
        return this.writeRuntime({
            ...record,
            lastActivityAt: Math.max(record.lastActivityAt, at)
        });
    }
    incrementActiveSessions(projectId) {
        this.activeSessions.set(projectId, this.getActiveSessionCount(projectId) + 1);
    }
    decrementActiveSessions(projectId) {
        const next = this.getActiveSessionCount(projectId) - 1;
        if (next > 0) {
            this.activeSessions.set(projectId, next);
            return;
        }
        this.activeSessions.delete(projectId);
    }
    loadActiveRuntime(project) {
        const loaded = this.store.load(project.path);
        if (loaded.kind === 'missing') {
            return null;
        }
        if (loaded.kind === 'invalid') {
            this.store.remove(project.path);
            return null;
        }
        if (!isProcessAlive(loaded.record.pid)) {
            this.store.remove(project.path);
            return null;
        }
        return loaded.record;
    }
    readRunningRuntime(project) {
        const loaded = this.store.load(project.path);
        if (loaded.kind === 'missing') {
            return this.normalizeStatus(project, null, null);
        }
        if (loaded.kind === 'invalid') {
            return this.normalizeStatus(project, null, loaded.error);
        }
        if (!isProcessAlive(loaded.record.pid)) {
            this.store.remove(project.path);
            return this.normalizeStatus(project, null, null);
        }
        return this.normalizeStatus(project, loaded.record, null);
    }
    touchProjectRuntime(project, at = Date.now()) {
        const runtime = this.loadActiveRuntime(project);
        if (!runtime) {
            return this.normalizeStatus(project, null, null);
        }
        return this.normalizeStatus(project, this.touchRuntimeRecord(runtime, at), null);
    }
    noteProjectActivity(projectId, at = Date.now()) {
        return this.touchProjectRuntime(this.resolveProject(projectId), at);
    }
    acquireProjectSession(projectId) {
        const project = this.resolveProject(projectId);
        this.incrementActiveSessions(project.id);
        this.touchProjectRuntime(project);
        let released = false;
        return {
            touchActivity: (at = Date.now()) => this.touchProjectRuntime(project, at),
            release: () => {
                if (released) {
                    return;
                }
                released = true;
                this.decrementActiveSessions(project.id);
            }
        };
    }
    listProjectStatuses() {
        return this.listProjects().map(project => this.readRunningRuntime(project));
    }
    getProjectStatus(projectId) {
        return this.readRunningRuntime(this.resolveProject(projectId));
    }
    async startProject(projectId) {
        const project = this.resolveProject(projectId);
        const loaded = this.store.load(project.path);
        if (loaded.kind === 'valid' && isProcessAlive(loaded.record.pid)) {
            const runtime = this.touchRuntimeRecord(loaded.record);
            return {
                ...this.normalizeStatus(project, runtime, null),
                reusedExisting: true
            };
        }
        if (loaded.kind !== 'missing') {
            this.store.remove(project.path);
        }
        const port = await findAvailablePort(this.config.ports.start, this.config.ports.end);
        const command = this.commandFactory(port, project);
        const child = await spawnDetached(command.command, command.args, project.path);
        if (typeof child.pid !== 'number') {
            throw new CodoriError('PROCESS_START_FAILED', `Failed to determine PID for project "${projectId}".`);
        }
        const now = Date.now();
        const runtime = {
            projectId: project.id,
            projectPath: project.path,
            pid: child.pid,
            port,
            startedAt: now,
            lastActivityAt: now
        };
        this.writeRuntime(runtime);
        return {
            ...this.normalizeStatus(project, runtime, null),
            reusedExisting: false
        };
    }
    async stopProject(projectId) {
        const project = this.resolveProject(projectId);
        const loaded = this.store.load(project.path);
        if (loaded.kind === 'missing') {
            return this.normalizeStatus(project, null, null);
        }
        if (loaded.kind === 'invalid') {
            this.store.remove(project.path);
            return this.normalizeStatus(project, null, null);
        }
        if (isProcessAlive(loaded.record.pid)) {
            process.kill(loaded.record.pid, 'SIGTERM');
            const exited = await waitForExit(loaded.record.pid, CODORI_STOP_TIMEOUT_MS);
            if (!exited) {
                process.kill(loaded.record.pid, 'SIGKILL');
                await waitForExit(loaded.record.pid, CODORI_STOP_TIMEOUT_MS);
            }
        }
        this.store.remove(project.path);
        return this.normalizeStatus(project, null, null);
    }
    async reapIdleRuntimes() {
        if (!this.config.idleShutdown.enabled || this.idleSweepInFlight) {
            return 0;
        }
        this.idleSweepInFlight = true;
        let stopped = 0;
        try {
            const now = Date.now();
            for (const project of this.listProjects()) {
                const runtime = this.loadActiveRuntime(project);
                if (!runtime) {
                    continue;
                }
                if (this.getActiveSessionCount(project.id) > 0) {
                    continue;
                }
                if (now - runtime.lastActivityAt < this.config.idleShutdown.timeoutMs) {
                    continue;
                }
                await this.stopProject(project.id);
                stopped += 1;
            }
            return stopped;
        }
        finally {
            this.idleSweepInFlight = false;
        }
    }
    dispose() {
        if (!this.idleReaper) {
            return;
        }
        clearInterval(this.idleReaper);
        this.idleReaper = null;
    }
}
export const createRuntimeManager = (options = {}) => new RuntimeManager({
    homeDir: options.homeDir ?? os.homedir(),
    configOverrides: options.configOverrides,
    config: options.config,
    commandFactory: options.commandFactory
});
