import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
import { spawn } from 'node:child_process';
import { randomUUID } from 'node:crypto';
import { dirname } from 'node:path';
import net from 'node:net';
import { buildSandboxLaunchPlan, executeSandboxCommand, executeSandboxManagedCommand, resolveSandboxCommandPlan } from './backend.js';
import { getSandboxConfigSnapshot, listSandboxProfiles, renderSandboxReview } from './manager.js';
import { summarizeError } from '../../utils/error-display.js';
import { logger } from '../../utils/logger.js';
function createSandboxSessionId() {
    return `sandbox_${Date.now().toString(36)}_${randomUUID().slice(0, 8)}`;
}
function inferShared(profile) {
    return profile.isolation === 'shared';
}
function inferKind(profile) {
    return profile.kind;
}
function readManagerString(manager, key) {
    return `${manager.get(key) ?? ''}`.trim();
}
function readManagerPort(manager, key, fallback) {
    const raw = manager.get(key);
    const parsed = typeof raw === 'number' ? raw : Number.parseInt(`${raw ?? ''}`, 10);
    return Number.isInteger(parsed) && parsed > 0 ? parsed : fallback;
}
function errorCode(error) {
    return error && typeof error === 'object' && 'code' in error
        ? String(error.code)
        : undefined;
}
function logGuestCleanupFailure(pid, context, error) {
    if (errorCode(error) === 'ESRCH')
        return;
    logger.warn('Sandbox managed guest cleanup failed', {
        pid,
        context,
        error: summarizeError(error),
    });
}
function waitForTcp(host, port, timeoutMs) {
    const deadline = Date.now() + timeoutMs;
    return new Promise((resolve) => {
        const attempt = () => {
            const socket = net.createConnection({ host, port });
            const finish = (ok) => {
                socket.removeAllListeners();
                socket.destroy();
                resolve(ok);
            };
            socket.once('connect', () => finish(true));
            socket.once('error', () => {
                socket.destroy();
                if (Date.now() >= deadline) {
                    resolve(false);
                    return;
                }
                const timer = setTimeout(attempt, 250);
                timer.unref?.();
            });
            socket.setTimeout(1000, () => {
                socket.destroy();
                if (Date.now() >= deadline) {
                    resolve(false);
                    return;
                }
                const timer = setTimeout(attempt, 250);
                timer.unref?.();
            });
        };
        attempt();
    });
}
async function launchManagedQemuGuest(launchPlan, configManager) {
    if (!launchPlan || launchPlan.backend !== 'qemu')
        return null;
    const host = readManagerString(configManager, 'sandbox.qemuGuestHost');
    const sessionMode = readManagerString(configManager, 'sandbox.qemuSessionMode');
    if (!host || sessionMode !== 'attach')
        return null;
    const port = readManagerPort(configManager, 'sandbox.qemuGuestPort', 2222);
    const proc = spawn(launchPlan.command, [...launchPlan.args], {
        cwd: launchPlan.workspaceRoot,
        detached: true,
        // QEMU is a detached, long-running guest. Startup failures are surfaced by
        // the TCP readiness wait and wrapper startup probe, so keeping stdio open
        // here would tie host lifetime to the VM process.
        stdio: 'ignore',
        windowsHide: true,
    });
    proc.unref();
    if (!proc.pid) {
        return null;
    }
    const ready = await waitForTcp(host, port, 15000);
    if (!ready) {
        try {
            process.kill(proc.pid, 'SIGTERM');
        }
        catch (error) {
            logGuestCleanupFailure(proc.pid, 'startup-timeout', error);
        }
        throw new Error(`Timed out waiting for QEMU guest SSH on ${host}:${port}.`);
    }
    return { pid: proc.pid, host, port };
}
export class SandboxSessionRegistry {
    sessions = new Map();
    workspaceRoot;
    constructor(workspaceRoot) {
        this.workspaceRoot = workspaceRoot;
    }
    updateSession(sessionId, updater) {
        const existing = this.sessions.get(sessionId);
        if (!existing) {
            throw new Error(`Unknown sandbox session: ${sessionId}`);
        }
        const next = Object.freeze(updater(existing));
        this.sessions.set(sessionId, next);
        return next;
    }
    list() {
        return [...this.sessions.values()].sort((a, b) => b.startedAt - a.startedAt);
    }
    async start(profileId, label, configManager) {
        const profile = listSandboxProfiles(configManager).find((entry) => entry.id === profileId);
        if (!profile) {
            throw new Error(`Unknown sandbox profile: ${profileId}`);
        }
        if (inferShared(profile)) {
            const existing = [...this.sessions.values()].find((session) => session.profileId === profileId && session.state === 'running');
            if (existing)
                return existing;
        }
        const config = getSandboxConfigSnapshot(configManager);
        const launchPlan = buildSandboxLaunchPlan(profile, label?.trim() || profile.label, configManager, this.workspaceRoot);
        let state = 'running';
        let startupStatus = 'verified';
        let startupDetail = launchPlan.summary;
        let managedGuestPid;
        let managedGuestHost;
        let managedGuestPort;
        if (launchPlan.backend === 'qemu') {
            if (launchPlan.imagePath && config.qemuExecWrapper) {
                try {
                    const managedGuest = await launchManagedQemuGuest(launchPlan, configManager);
                    managedGuestPid = managedGuest?.pid;
                    managedGuestHost = managedGuest?.host;
                    managedGuestPort = managedGuest?.port;
                    const startupProbe = executeSandboxManagedCommand(launchPlan, 'bash', ['-lc', 'printf sandbox-ready'], configManager, {
                        timeoutMs: 4000,
                    });
                    if (startupProbe.status !== 0 || !startupProbe.stdout.includes('sandbox-ready')) {
                        state = 'failed';
                        startupStatus = 'failed';
                        startupDetail = (startupProbe.stderr || startupProbe.stdout || 'QEMU sandbox wrapper startup probe failed.').trim();
                    }
                    else {
                        state = 'running';
                        startupStatus = 'verified';
                        startupDetail = managedGuestPid
                            ? `QEMU guest launched and verified via ${config.qemuExecWrapper} on ${managedGuestHost}:${managedGuestPort} using image ${launchPlan.imagePath}.`
                            : `QEMU backend verified via ${config.qemuExecWrapper} using image ${launchPlan.imagePath}.`;
                    }
                }
                catch (error) {
                    state = 'failed';
                    startupStatus = 'failed';
                    startupDetail = summarizeError(error);
                }
            }
            else {
                state = 'planned';
                startupStatus = 'planned';
                startupDetail = launchPlan.imagePath
                    ? `QEMU backend resolved with image ${launchPlan.imagePath}. Guest launch planning is wired, but command execution still requires sandbox.qemuExecWrapper.`
                    : 'QEMU backend resolved. Guest launch planning is available, but full guest execution still requires sandbox.qemuImagePath and sandbox.qemuExecWrapper.';
            }
        }
        else {
            const startupProbe = executeSandboxCommand(launchPlan, 'bash', ['-lc', 'printf sandbox-ready'], {
                timeoutMs: 2000,
            });
            if (startupProbe.status !== 0 || !startupProbe.stdout.includes('sandbox-ready')) {
                state = 'failed';
                startupStatus = 'failed';
                startupDetail = (startupProbe.stderr || startupProbe.stdout || 'Sandbox backend startup probe failed.').trim();
            }
        }
        const session = Object.freeze({
            id: createSandboxSessionId(),
            profileId: profile.id,
            kind: inferKind(profile),
            label: label?.trim() || profile.label,
            shared: inferShared(profile),
            startedAt: Date.now(),
            state,
            backend: config.vmBackend,
            resolvedBackend: launchPlan.backend,
            launchPlan,
            startupStatus,
            startupDetail,
            managedGuestPid,
            managedGuestHost,
            managedGuestPort,
            notes: profile.notes,
        });
        this.sessions.set(session.id, session);
        return session;
    }
    stop(sessionId) {
        const existing = this.sessions.get(sessionId);
        if (!existing)
            return null;
        if (existing.managedGuestPid) {
            try {
                process.kill(existing.managedGuestPid, 'SIGTERM');
            }
            catch (error) {
                logGuestCleanupFailure(existing.managedGuestPid, 'session-stop', error);
            }
        }
        return this.updateSession(sessionId, (session) => ({
            ...session,
            state: 'stopped',
        }));
    }
    get(sessionId) {
        return this.sessions.get(sessionId) ?? null;
    }
    execute(sessionId, command, args, configManager, options = {}) {
        const session = this.get(sessionId);
        if (!session) {
            throw new Error(`Unknown sandbox session: ${sessionId}`);
        }
        if (!session.launchPlan) {
            throw new Error(`Sandbox session ${sessionId} does not have a launch plan.`);
        }
        const resolvedPlan = resolveSandboxCommandPlan(session.launchPlan, command, args, configManager);
        const result = session.launchPlan.backend === 'qemu'
            ? executeSandboxManagedCommand(session.launchPlan, command, args, configManager, options)
            : executeSandboxCommand(session.launchPlan, command, args, options);
        const stdoutPreview = (result.stdout || '').trim().slice(0, 200);
        const stderrPreview = (result.stderr || '').trim().slice(0, 200);
        const nextState = result.status === 0
            ? (session.state === 'stopped' ? 'stopped' : 'running')
            : 'failed';
        this.updateSession(sessionId, (current) => ({
            ...current,
            state: nextState,
            lastRunAt: Date.now(),
            lastCommandSummary: resolvedPlan.summary,
            lastExitStatus: result.status,
            lastStdoutPreview: stdoutPreview || undefined,
            lastStderrPreview: stderrPreview || undefined,
            executionCount: (current.executionCount ?? 0) + 1,
        }));
        return result;
    }
    exportArtifact(sessionId, targetPath, configManager) {
        const session = this.get(sessionId);
        if (!session) {
            throw new Error(`Unknown sandbox session: ${sessionId}`);
        }
        const artifact = {
            version: 1,
            exportedAt: Date.now(),
            session,
            reviewText: renderSandboxReview(configManager),
        };
        mkdirSync(dirname(targetPath), { recursive: true });
        writeFileSync(targetPath, `${JSON.stringify(artifact, null, 2)}\n`, 'utf-8');
        return artifact;
    }
    inspectArtifact(targetPath) {
        return JSON.parse(readFileSync(targetPath, 'utf-8'));
    }
}
