import { spawn } from "node:child_process";
import { createHash } from "node:crypto";
import { mkdtemp, readFile, realpath, rm, stat } from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { pathToFileURL } from "node:url";
import { sanitizeText } from "../../shared/index.js";
import { analyzeSeedUntrustedHtml, extractVisibleTextFromHtml } from "../content-security/index.js";
const DEFAULT_TIMEOUT_MS = 30000;
const DEFAULT_VIEWPORT = { width: 1280, height: 720 };
const MAX_HTML_CHARS = 500000;
export async function captureSeedBrowserSnapshot(input) {
    const viewport = normalizeViewport(input.viewport);
    const capturedAt = input.now?.() ?? new Date().toISOString();
    const target = await resolveBrowserTarget(input.target, input.cwd, input.allowExternal ?? false);
    if (!target.ok) {
        return {
            status: target.status,
            target: target.target,
            viewport,
            captured_at: capturedAt,
            diagnostics: target.diagnostics,
            command_results: [],
        };
    }
    const browser = await resolveBrowserCommand({
        browserCommand: input.browserCommand,
        browserKind: input.browserKind,
        allowCustomBrowserCommand: input.allowCustomBrowserCommand ?? false,
        timeoutMs: Math.min(input.timeoutMs ?? DEFAULT_TIMEOUT_MS, 10000),
    });
    if (!browser.ok) {
        return {
            status: browser.status,
            target: target.target,
            viewport,
            captured_at: capturedAt,
            diagnostics: browser.diagnostics,
            command_results: browser.probeResults,
            html: target.localHtml,
            text: target.localHtml ? htmlToText(target.localHtml) : undefined,
            title: target.localHtml ? extractTitle(target.localHtml) : undefined,
        };
    }
    const timeoutMs = input.timeoutMs ?? DEFAULT_TIMEOUT_MS;
    const profileDir = await mkdtemp(path.join(os.tmpdir(), "seed-browser-profile-"));
    const screenshotTemp = path.join(await mkdtemp(path.join(os.tmpdir(), "seed-browser-shot-")), "snapshot.png");
    const commandResults = browser.probe ? [browser.probe] : [];
    const diagnostics = [...browser.diagnostics];
    let html = target.localHtml;
    let screenshot;
    try {
        if (browser.kind === "chromium") {
            const dump = await runBrowserCommand([
                ...browser.command,
                "--headless=new",
                "--disable-gpu",
                "--no-first-run",
                "--no-default-browser-check",
                "--disable-dev-shm-usage",
                `--user-data-dir=${profileDir}`,
                `--window-size=${viewport.width},${viewport.height}`,
                "--dump-dom",
                target.browserUrl,
            ], timeoutMs, "dump_dom");
            commandResults.push(dump);
            if (dump.exit_code === 0 && dump.stdout.trim())
                html = truncate(dump.stdout.trim(), MAX_HTML_CHARS);
            else
                diagnostics.push(`DOM dump failed or returned empty output: ${dump.stderr || `exit ${dump.exit_code}`}`);
        }
        const screenshotResult = await runBrowserCommand(buildScreenshotCommand(browser.command, browser.kind, {
            targetUrl: target.browserUrl,
            outputPath: screenshotTemp,
            profileDir,
            viewport,
        }), timeoutMs, "screenshot");
        commandResults.push(screenshotResult);
        if (screenshotResult.exit_code === 0) {
            const content = await readFile(screenshotTemp);
            screenshot = {
                content,
                sha256: createHash("sha256").update(content).digest("hex"),
                byte_size: content.length,
                mime_type: "image/png",
            };
        }
        else {
            diagnostics.push(`Screenshot failed: ${screenshotResult.stderr || `exit ${screenshotResult.exit_code}`}`);
        }
    }
    finally {
        await rm(profileDir, { recursive: true, force: true }).catch(() => undefined);
        await rm(path.dirname(screenshotTemp), { recursive: true, force: true }).catch(() => undefined);
    }
    const security = html ? analyzeSeedUntrustedHtml(html) : undefined;
    const text = security ? security.visible_text : html ? htmlToText(html) : undefined;
    const title = html ? extractTitle(html) : undefined;
    if (security && (security.hidden_elements.length > 0 || security.aria_injections.length > 0 || security.unsafe_urls.length > 0)) {
        diagnostics.push(`Browser HTML security warnings: hidden=${security.hidden_elements.length}, aria_injection=${security.aria_injections.length}, unsafe_urls=${security.unsafe_urls.length}`);
    }
    const status = screenshot || html ? "captured" : "failed";
    if (status === "failed" && diagnostics.length === 0)
        diagnostics.push("Browser command completed without DOM or screenshot output.");
    return {
        status,
        target: target.target,
        browser: {
            command: browser.command,
            kind: browser.kind,
            discovered: browser.discovered,
        },
        viewport,
        captured_at: capturedAt,
        title,
        html,
        text,
        accessibility_tree: text,
        security: security
            ? {
                hidden_elements: security.hidden_elements,
                aria_injections: security.aria_injections,
                unsafe_urls: security.unsafe_urls,
            }
            : undefined,
        screenshot,
        diagnostics,
        command_results: commandResults,
    };
}
export function buildSeedBrowserSnapshotDocument(capture, input) {
    return {
        type: "seed.browser_snapshot",
        schema_version: 1,
        url: capture.target.url,
        title: capture.title,
        captured_at: capture.captured_at,
        viewport: capture.viewport,
        text: capture.text ?? "",
        html: capture.html ?? "",
        accessibility_tree: capture.accessibility_tree ?? "",
        security: capture.security,
        screenshot_path: input.screenshotPath,
        capture: {
            status: capture.status,
            target: capture.target,
            browser: capture.browser,
            diagnostics: capture.diagnostics,
            command_results: capture.command_results.map((result) => ({
                step: result.step,
                command: result.command,
                exit_code: result.exit_code,
                timed_out: result.timed_out,
                stdout_excerpt: result.stdout.slice(0, 1000),
                stderr_excerpt: result.stderr.slice(0, 1000),
            })),
        },
    };
}
async function resolveBrowserTarget(inputTarget, cwd, allowExternal) {
    const target = inputTarget.trim();
    if (isHttpUrl(target)) {
        return allowExternal
            ? {
                ok: true,
                target: { input: target, url: target, trust: "external_untrusted" },
                browserUrl: target,
            }
            : {
                ok: false,
                status: "blocked",
                target: { input: target, url: target, trust: "external_untrusted" },
                diagnostics: ["External browser capture requires allow_external: true."],
            };
    }
    if (looksLikeUrl(target)) {
        return {
            ok: false,
            status: "unsupported",
            target: { input: target, url: target, trust: "external_untrusted" },
            diagnostics: ["Only http(s) URLs or local files are supported for browser snapshot capture."],
        };
    }
    const candidate = path.isAbsolute(target) ? path.resolve(target) : path.resolve(cwd, target);
    const safeRoots = await Promise.all([cwd, os.tmpdir()].map((root) => realpath(root).catch(() => path.resolve(root))));
    let realCandidate;
    try {
        realCandidate = await realpath(candidate);
    }
    catch {
        return {
            ok: false,
            status: "blocked",
            target: { input: target, resolved_path: candidate, trust: "local_checked" },
            diagnostics: [`Local browser capture target not found: ${candidate}`],
        };
    }
    if (!safeRoots.some((root) => isPathWithin(realCandidate, root))) {
        return {
            ok: false,
            status: "blocked",
            target: { input: target, resolved_path: realCandidate, trust: "local_checked" },
            diagnostics: [`Local browser capture target is outside allowed roots: ${safeRoots.join(", ")}`],
        };
    }
    const fileStat = await stat(realCandidate);
    if (!fileStat.isFile()) {
        return {
            ok: false,
            status: "blocked",
            target: { input: target, resolved_path: realCandidate, trust: "local_checked" },
            diagnostics: [`Local browser capture target is not a regular file: ${realCandidate}`],
        };
    }
    const localHtml = isHtmlPath(realCandidate) ? await readFile(realCandidate, "utf8").then((value) => truncate(value, MAX_HTML_CHARS)) : undefined;
    return {
        ok: true,
        target: {
            input: target,
            url: pathToFileURL(realCandidate).toString(),
            resolved_path: realCandidate,
            trust: "local_checked",
        },
        browserUrl: pathToFileURL(realCandidate).toString(),
        localHtml,
    };
}
async function resolveBrowserCommand(input) {
    if (input.browserCommand?.length) {
        const inferred = inferBrowserKind(input.browserCommand[0]);
        if (!inferred && !input.allowCustomBrowserCommand) {
            return {
                ok: false,
                status: "blocked",
                diagnostics: ["Custom browser commands must set allow_custom_browser_command: true when the executable name is not a known browser."],
                probeResults: [],
            };
        }
        const kind = input.browserKind ?? inferred;
        if (!kind) {
            if (!input.allowCustomBrowserCommand) {
                return {
                    ok: false,
                    status: "blocked",
                    diagnostics: ["Custom browser commands must set allow_custom_browser_command: true when the executable name is not a known browser."],
                    probeResults: [],
                };
            }
            return { ok: true, command: input.browserCommand, kind: "chromium", discovered: false, diagnostics: ["Using custom browser command as chromium-compatible."] };
        }
        return { ok: true, command: input.browserCommand, kind, discovered: false, diagnostics: [] };
    }
    const candidates = [process.env.SEED_BROWSER, "chromium", "chromium-browser", "google-chrome", "google-chrome-stable", "firefox"]
        .filter((candidate) => typeof candidate === "string" && candidate.length > 0)
        .map((candidate) => [candidate]);
    const probeResults = [];
    for (const candidate of candidates) {
        const kind = inferBrowserKind(candidate[0]);
        if (!kind)
            continue;
        const probe = await runBrowserCommand([...candidate, "--version"], input.timeoutMs, "version");
        probeResults.push(probe);
        if (probe.exit_code === 0)
            return { ok: true, command: candidate, kind, discovered: true, diagnostics: [], probe };
    }
    return {
        ok: false,
        status: "browser_not_found",
        diagnostics: ["No supported headless browser was found. Set SEED_BROWSER or pass browser_command with a chromium/firefox-compatible executable."],
        probeResults,
    };
}
function buildScreenshotCommand(command, kind, input) {
    if (kind === "firefox") {
        return [
            ...command,
            "--headless",
            "--window-size",
            `${input.viewport.width},${input.viewport.height}`,
            "--profile",
            input.profileDir,
            "--screenshot",
            input.outputPath,
            input.targetUrl,
        ];
    }
    return [
        ...command,
        "--headless=new",
        "--disable-gpu",
        "--no-first-run",
        "--no-default-browser-check",
        "--disable-dev-shm-usage",
        `--user-data-dir=${input.profileDir}`,
        `--window-size=${input.viewport.width},${input.viewport.height}`,
        `--screenshot=${input.outputPath}`,
        input.targetUrl,
    ];
}
function runBrowserCommand(command, timeoutMs, step) {
    return new Promise((resolve) => {
        const child = spawn(command[0], command.slice(1), { shell: false });
        let stdout = "";
        let stderr = "";
        let timedOut = false;
        const timeout = setTimeout(() => {
            timedOut = true;
            child.kill("SIGTERM");
            stderr += `\nTimed out after ${timeoutMs}ms`;
        }, timeoutMs);
        child.stdout.on("data", (chunk) => {
            stdout += chunk.toString();
        });
        child.stderr.on("data", (chunk) => {
            stderr += chunk.toString();
        });
        child.on("error", (error) => {
            clearTimeout(timeout);
            resolve({ step, command, exit_code: 1, stdout: sanitizeText(stdout), stderr: sanitizeText(`${stderr}\n${error.message}`), timed_out: timedOut });
        });
        child.on("exit", (code) => {
            clearTimeout(timeout);
            resolve({ step, command, exit_code: code, stdout: sanitizeText(stdout), stderr: sanitizeText(stderr), timed_out: timedOut });
        });
    });
}
function normalizeViewport(viewport) {
    const width = Number.isFinite(viewport?.width) ? Math.max(320, Math.min(3840, Math.floor(viewport.width))) : DEFAULT_VIEWPORT.width;
    const height = Number.isFinite(viewport?.height) ? Math.max(240, Math.min(2160, Math.floor(viewport.height))) : DEFAULT_VIEWPORT.height;
    return { width, height };
}
function inferBrowserKind(command) {
    const base = path.basename(command).toLowerCase();
    if (base.includes("firefox"))
        return "firefox";
    if (base.includes("chrom") || base.includes("chrome") || base.includes("msedge"))
        return "chromium";
    return undefined;
}
function extractTitle(html) {
    return sanitizeText(html.match(/<title[^>]*>([\s\S]{1,200}?)<\/title>/i)?.[1]?.replace(/\s+/g, " ").trim() ?? "") || undefined;
}
function htmlToText(html) {
    return extractVisibleTextFromHtml(html);
}
function isHtmlPath(file) {
    return [".html", ".htm"].includes(path.extname(file).toLowerCase());
}
function isHttpUrl(value) {
    try {
        const url = new URL(value);
        return url.protocol === "http:" || url.protocol === "https:";
    }
    catch {
        return false;
    }
}
function looksLikeUrl(value) {
    try {
        new URL(value);
        return true;
    }
    catch {
        return false;
    }
}
function isPathWithin(candidate, root) {
    const relative = path.relative(root, candidate);
    return relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative));
}
function truncate(value, maxChars) {
    if (value.length <= maxChars)
        return value;
    return `${value.slice(0, maxChars)}\n[truncated]`;
}
//# sourceMappingURL=index.js.map