import { BrowserSurface } from "../surfaces/browser.js";
import { createPreviewRunner, } from "./preview-runner.js";
const MAX_PREVIEW_LOGS = 400;
function nowIso() {
    return new Date().toISOString();
}
function pushBounded(items, next, max) {
    items.push(next);
    if (items.length > max)
        items.splice(0, items.length - max);
}
function normalizeTargetUrl(target) {
    const trimmed = target.trim();
    if (!trimmed)
        return null;
    if (/^\d+$/.test(trimmed)) {
        return `http://127.0.0.1:${Number.parseInt(trimmed, 10)}/`;
    }
    const withScheme = /^[a-z]+:\/\//i.test(trimmed) ? trimmed : `http://${trimmed}`;
    try {
        const url = new URL(withScheme);
        const host = url.hostname.toLowerCase();
        if (!["127.0.0.1", "localhost", "0.0.0.0", "::1", "[::1]"].includes(host))
            return null;
        return url.toString();
    }
    catch {
        return null;
    }
}
export class PreviewService {
    surfaceRegistry;
    sessions = new Map();
    nextLogId = 1;
    runner;
    constructor(surfaceRegistry) {
        this.surfaceRegistry = surfaceRegistry;
        this.runner = createPreviewRunner(false);
    }
    async start(input) {
        if (!input.sessionId)
            throw new Error("sessionId is required");
        await this.stop(input.sessionId);
        const createdAt = nowIso();
        const isLocal = Boolean(input.command?.trim() || input.workspaceRoot?.trim());
        const session = {
            id: `preview-${input.sessionId}`,
            sessionId: input.sessionId,
            workspaceRoot: input.workspaceRoot?.trim() || null,
            mode: isLocal ? this.runner.mode : "url",
            status: "starting",
            target: input.target?.trim() || null,
            command: null,
            port: null,
            url: null,
            browserId: `preview-browser-${input.sessionId}`,
            processId: null,
            containerId: null,
            logs: [],
            browserEvents: [],
            lastError: null,
            createdAt,
            updatedAt: createdAt,
            process: null,
            runnerResult: null,
        };
        this.sessions.set(input.sessionId, session);
        this.appendLog(session, "system", "Starting preview session");
        try {
            if (session.mode !== "url") {
                if (!session.workspaceRoot) {
                    throw new Error("workspaceRoot is required for managed project previews");
                }
                const result = await this.runner.start({
                    workspaceRoot: session.workspaceRoot,
                    command: input.command?.trim() || null,
                    port: input.port,
                    target: input.target?.trim() || null,
                    frameworkHint: input.frameworkHint?.trim() || null,
                }, (stream, text) => this.appendLog(session, stream, text));
                session.runnerResult = result;
                session.process = result.process;
                session.port = result.port;
                session.command = result.command;
                session.url = result.url;
                session.processId = result.processId ?? null;
                session.containerId = result.containerId ?? null;
                // Listen for process exit
                if (result.process) {
                    result.process.on("exit", (code, signal) => {
                        if (session.status === "stopped")
                            return;
                        session.updatedAt = nowIso();
                        session.status = "error";
                        const message = `Preview process exited (code=${code ?? "null"}, signal=${signal ?? "null"})`;
                        session.lastError = message;
                        this.appendLog(session, "stderr", message);
                    });
                }
            }
            else {
                session.url = normalizeTargetUrl(input.target ?? "");
                if (!session.url) {
                    throw new Error("A loopback target URL or port is required");
                }
            }
            await this.ensureBrowser(session);
            session.status = "ready";
            session.updatedAt = nowIso();
            this.appendLog(session, "system", `Preview ready at ${session.url}`);
            return this.toPublicSession(session);
        }
        catch (error) {
            session.status = "error";
            session.lastError = error instanceof Error ? error.message : "Preview start failed";
            session.updatedAt = nowIso();
            this.appendLog(session, "stderr", session.lastError);
            return this.toPublicSession(session);
        }
    }
    async restart(sessionId) {
        const existing = this.sessions.get(sessionId);
        if (!existing)
            return null;
        return this.start({
            sessionId,
            workspaceRoot: existing.workspaceRoot,
            target: existing.target,
            command: existing.command,
            port: existing.port,
        });
    }
    async stop(sessionId) {
        const session = this.sessions.get(sessionId);
        if (!session)
            return false;
        if (session.runnerResult) {
            await this.runner.stop(session.runnerResult).catch(() => { });
        }
        else if (session.process && !session.process.killed) {
            session.process.kill("SIGTERM");
        }
        if (session.browserId) {
            await this.surfaceRegistry.stopSurface(session.browserId, "preview-stop").catch(() => { });
        }
        session.status = "stopped";
        session.updatedAt = nowIso();
        this.appendLog(session, "system", "Preview stopped");
        this.sessions.delete(sessionId);
        return true;
    }
    get(sessionId) {
        const session = this.sessions.get(sessionId);
        if (!session)
            return null;
        session.browserEvents = this.readBrowserEvents(session);
        return this.toPublicSession(session);
    }
    getLogs(sessionId, sinceId = 0) {
        const session = this.sessions.get(sessionId);
        if (!session)
            return [];
        return session.logs.filter((entry) => entry.id > sinceId);
    }
    list() {
        return [...this.sessions.values()].map((s) => this.get(s.sessionId)).filter(Boolean);
    }
    async screenshot(sessionId) {
        const session = this.sessions.get(sessionId);
        if (!session?.browserId)
            return null;
        const surface = this.surfaceRegistry.getSurface(session.browserId);
        if (!surface || surface.type !== "browser" || surface.state !== "running")
            return null;
        return surface.screenshot();
    }
    async snapshot(sessionId) {
        const session = this.sessions.get(sessionId);
        if (!session?.browserId)
            return null;
        const surface = this.surfaceRegistry.getSurface(session.browserId);
        if (!surface || surface.type !== "browser" || surface.state !== "running")
            return null;
        const browser = surface;
        return browser.describe();
    }
    async inspect(sessionId) {
        const session = this.sessions.get(sessionId);
        if (!session)
            return null;
        const events = this.readBrowserEvents(session);
        const screenshotBase64 = await this.screenshot(sessionId).catch(() => null);
        return {
            status: session.status,
            url: session.url,
            browserEvents: events,
            logs: [...session.logs],
            screenshot: screenshotBase64,
        };
    }
    async stopAll() {
        const sessionIds = [...this.sessions.keys()];
        for (const sessionId of sessionIds) {
            await this.stop(sessionId);
        }
    }
    async ensureBrowser(session) {
        const browserId = session.browserId;
        await this.surfaceRegistry.stopSurface(browserId, "preview-refresh").catch(() => { });
        const started = await this.surfaceRegistry.startSurface("browser", browserId, {
            sessionId: session.sessionId,
            workspaceRoot: session.workspaceRoot ?? process.cwd(),
        });
        if (started.type !== "browser") {
            throw new Error("Preview browser surface failed to start");
        }
        const browser = started;
        await browser.navigate(session.url);
        session.browserEvents = browser.getEvents();
    }
    readBrowserEvents(session) {
        if (!session.browserId)
            return [];
        const surface = this.surfaceRegistry.getSurface(session.browserId);
        if (!surface || surface.type !== "browser" || surface.state !== "running")
            return session.browserEvents;
        return surface.getEvents();
    }
    appendLog(session, stream, text) {
        const normalized = text.replace(/\r/g, "").split("\n").map((line) => line.trimEnd()).filter(Boolean);
        if (normalized.length === 0)
            return;
        for (const line of normalized) {
            pushBounded(session.logs, {
                id: this.nextLogId++,
                stream,
                text: line,
                timestamp: nowIso(),
            }, MAX_PREVIEW_LOGS);
        }
        session.updatedAt = nowIso();
    }
    toPublicSession(session) {
        return {
            id: session.id,
            sessionId: session.sessionId,
            workspaceRoot: session.workspaceRoot,
            mode: session.mode,
            status: session.status,
            target: session.target,
            command: session.command,
            port: session.port,
            url: session.url,
            browserId: session.browserId,
            processId: session.processId,
            containerId: session.containerId,
            logs: [...session.logs],
            browserEvents: [...session.browserEvents],
            lastError: session.lastError,
            createdAt: session.createdAt,
            updatedAt: session.updatedAt,
        };
    }
}
//# sourceMappingURL=preview.js.map