import { readFile } from "node:fs/promises";
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;
    }
}
function createLiveViewRemoteBrowser(liveView) {
    return {
        containerName: "live-view",
        novncUrl: `/noVNC/vnc_lite.html?path=api/live-view/${liveView.websockifyPort}/websockify`,
        vncPort: liveView.vncPort,
        novncPort: liveView.websockifyPort,
        startedAt: nowIso(),
    };
}
export class PreviewService {
    surfaceRegistry;
    sessions = new Map();
    nextLogId = 1;
    runner;
    _onSessionChanged = null;
    constructor(surfaceRegistry) {
        this.surfaceRegistry = surfaceRegistry;
        this.runner = createPreviewRunner(false);
    }
    onSessionChanged(callback) {
        this._onSessionChanged = callback;
    }
    notifyChanged(session) {
        this._onSessionChanged?.(this.toPublicSession(session));
    }
    async start(input) {
        if (!input.sessionId)
            throw new Error("sessionId is required");
        await this.stop(input.sessionId);
        const createdAt = nowIso();
        const normalizedTarget = normalizeTargetUrl(input.target ?? "");
        const isLocal = Boolean(input.command?.trim()) || (!normalizedTarget && Boolean(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: [],
            metrics: null,
            remoteBrowser: null,
            lastError: null,
            createdAt,
            updatedAt: createdAt,
            browserUrl: null,
            process: null,
            runnerResult: null,
            terminating: false,
        };
        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.browserUrl = result.url;
                session.url = `/api/dev-proxy/${result.port}/`;
                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" || session.terminating)
                            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);
                        this.notifyChanged(session);
                    });
                }
            }
            else {
                session.browserUrl = normalizedTarget;
                if (!session.browserUrl) {
                    throw new Error("A loopback target URL or port is required");
                }
                const targetPort = new URL(session.browserUrl).port;
                session.url = `/api/dev-proxy/${targetPort}/`;
            }
            await this.ensureBrowser(session);
            if (!session.remoteBrowser) {
                throw new Error("Preview browser did not expose a live VNC session");
            }
            session.url = session.remoteBrowser?.novncUrl ?? session.url;
            session.status = "ready";
            session.updatedAt = nowIso();
            this.appendLog(session, "system", `Preview ready at ${session.url}`);
            this.notifyChanged(session);
            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);
            this.notifyChanged(session);
            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;
        session.terminating = true;
        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.notifyChanged(session);
        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);
    }
    async refreshSessionCapture(sessionId) {
        const session = this.sessions.get(sessionId);
        if (!session)
            return null;
        const nextEvents = this.readBrowserEvents(session);
        const nextMetrics = await this.readBrowserMetrics(session).catch(() => session.metrics);
        const eventsChanged = nextEvents.length !== session.browserEvents.length
            || nextEvents[nextEvents.length - 1]?.id !== session.browserEvents[session.browserEvents.length - 1]?.id;
        const metricsChanged = nextMetrics?.sampledAt !== session.metrics?.sampledAt;
        session.browserEvents = nextEvents;
        session.metrics = nextMetrics;
        if (eventsChanged || metricsChanged) {
            session.updatedAt = nowIso();
        }
        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;
        const screenshot = await surface.screenshot();
        if (!screenshot)
            return null;
        try {
            const bytes = await readFile(screenshot);
            return bytes.toString("base64");
        }
        catch {
            return 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, selector) {
        const session = this.sessions.get(sessionId);
        if (!session)
            return null;
        const events = this.readBrowserEvents(session);
        const screenshotBase64 = await this.screenshot(sessionId).catch(() => null);
        const inspection = await this.inspectBrowserPage(session, selector).catch(() => null);
        return {
            status: session.status,
            url: session.url,
            browserEvents: events,
            metrics: await this.readBrowserMetrics(session).catch(() => session.metrics),
            logs: [...session.logs],
            screenshot: screenshotBase64,
            page: inspection?.snapshot ?? null,
            snapshot: inspection ? this.formatInspectionSnapshot(inspection.snapshot) : null,
            target: inspection?.target ?? null,
        };
    }
    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(),
            requireLiveView: true,
        });
        if (started.type !== "browser") {
            throw new Error("Preview browser surface failed to start");
        }
        const browser = started;
        await browser.navigate(session.browserUrl);
        session.browserEvents = browser.getEvents();
        session.metrics = await browser.getMetrics().catch(() => null);
        const liveView = this.readLiveView(session, browser);
        if (liveView) {
            session.remoteBrowser = createLiveViewRemoteBrowser(liveView);
        }
    }
    readLiveView(session, surfaceOverride) {
        const surface = surfaceOverride
            ?? (session.browserId ? this.surfaceRegistry.getSurface(session.browserId) : null);
        if (!surface || surface.type !== "browser" || surface.state !== "running")
            return null;
        return typeof surface.getLiveViewInfo === "function"
            ? surface.getLiveViewInfo()
            : null;
    }
    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();
    }
    async inspectBrowserPage(session, selector) {
        if (!session.browserId)
            return null;
        const surface = this.surfaceRegistry.getSurface(session.browserId);
        if (!surface || surface.type !== "browser" || surface.state !== "running")
            return null;
        return surface.inspect(selector);
    }
    async readBrowserMetrics(session) {
        if (!session.browserId)
            return null;
        const surface = this.surfaceRegistry.getSurface(session.browserId);
        if (!surface || surface.type !== "browser" || surface.state !== "running")
            return session.metrics;
        return surface.getMetrics();
    }
    formatInspectionSnapshot(snapshot) {
        const lines = [
            `URL: ${snapshot.url}`,
            `Title: ${snapshot.title || "(untitled)"}`,
            snapshot.activeElement
                ? `Active element: ${[
                    snapshot.activeElement.role ?? snapshot.activeElement.tagName ?? "element",
                    snapshot.activeElement.name,
                    snapshot.activeElement.selector,
                ].filter(Boolean).join(" - ")}`
                : "Active element: (none)",
            snapshot.dialogs?.length
                ? `Dialogs: ${snapshot.dialogs.map((dialog) => dialog.title || dialog.name || dialog.selector || dialog.role || "dialog").join(", ")}`
                : "Dialogs: (none)",
            "",
            "Text:",
            snapshot.text.trim() || "(no textual content)",
        ];
        return lines.join("\n").trim();
    }
    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],
            metrics: session.metrics ? { ...session.metrics } : null,
            remoteBrowser: session.remoteBrowser ? { ...session.remoteBrowser } : null,
            lastError: session.lastError,
            createdAt: session.createdAt,
            updatedAt: session.updatedAt,
        };
    }
}
//# sourceMappingURL=preview.js.map