import http from "node:http";
import { createReadStream } from "node:fs";
import { pipeline } from "node:stream/promises";
import os from "node:os";
import path from "node:path";
import net from "node:net";
import { randomBytes, randomUUID } from "node:crypto";
import { spawn, spawnSync } from "node:child_process";
import { mkdtemp, rm, mkdir, writeFile, stat, realpath } from "node:fs/promises";
import chalk from "chalk";
import { runBrowserMode } from "../browserMode.js";
import { MAX_REMOTE_ARTIFACT_BYTES } from "./types.js";
import { getCookies } from "@steipete/sweet-cookie";
import { CHATGPT_URL } from "../browser/constants.js";
import { getCliVersion } from "../version.js";
import { getOracleHomeDir } from "../oracleHome.js";
import { cleanupStaleProfileState, readDevToolsPort, verifyDevToolsReachable, writeChromePid, writeDevToolsActivePort, } from "../browser/profileState.js";
import { normalizeChatgptUrl } from "../browser/utils.js";
import { computeFileSha256, sanitizeArtifactFilename, sanitizeArtifactMimeType, validateArtifactFile, } from "../browser/artifacts.js";
const ARTIFACT_PROTOCOL_VERSION = 1;
const REMOTE_ARTIFACT_TTL_MS = 30 * 60 * 1000;
const ARTIFACT_CAPABILITIES = {
    artifactTransfer: true,
    artifactProtocolVersion: ARTIFACT_PROTOCOL_VERSION,
    maxArtifactBytes: MAX_REMOTE_ARTIFACT_BYTES,
};
async function findAvailablePort() {
    return await new Promise((resolve, reject) => {
        const srv = net.createServer();
        srv.on("error", (err) => reject(err));
        srv.listen(0, () => {
            const address = srv.address();
            if (typeof address === "object" && address?.port) {
                const port = address.port;
                srv.close(() => resolve(port));
            }
            else {
                srv.close(() => reject(new Error("Unable to allocate port")));
            }
        });
    });
}
export async function createRemoteServer(options = {}, deps = {}) {
    const runBrowser = deps.runBrowser ?? runBrowserMode;
    const server = http.createServer();
    const logger = options.logger ?? console.log;
    const authToken = options.token ?? randomBytes(16).toString("hex");
    const startedAt = Date.now();
    const verbose = process.argv.includes("--verbose") || process.env.ORACLE_SERVE_VERBOSE === "1";
    const color = process.stdout.isTTY
        ? (formatter, msg) => formatter(msg)
        : (_formatter, msg) => msg;
    // Single-flight guard: remote Chrome can only host one run at a time, so we serialize requests.
    let busy = false;
    const artifactRegistry = new Map();
    if (!process.listenerCount("unhandledRejection")) {
        process.on("unhandledRejection", (reason) => {
            logger(`Unhandled promise rejection in remote server: ${reason instanceof Error ? reason.message : String(reason)}`);
        });
    }
    server.on("request", async (req, res) => {
        if (req.method === "GET" && req.url === "/status") {
            logger("[serve] Health check /status");
            res.writeHead(200, { "Content-Type": "application/json" });
            res.end(JSON.stringify({ ok: true }));
            return;
        }
        if (req.method === "GET" && req.url === "/health") {
            const authHeader = req.headers.authorization ?? "";
            if (authHeader !== `Bearer ${authToken}`) {
                if (verbose) {
                    logger(`[serve] Unauthorized /health attempt from ${formatSocket(req)} (missing/invalid token)`);
                }
                res.writeHead(401, { "Content-Type": "application/json" });
                res.end(JSON.stringify({ error: "unauthorized" }));
                return;
            }
            res.writeHead(200, { "Content-Type": "application/json" });
            res.end(JSON.stringify({
                ok: true,
                version: getCliVersion(),
                uptimeSeconds: Math.round((Date.now() - startedAt) / 1000),
                capabilities: ARTIFACT_CAPABILITIES,
            }));
            return;
        }
        const artifactMatch = matchArtifactRequest(req);
        if (artifactMatch) {
            await serveRemoteArtifact({
                req,
                res,
                authToken,
                artifactRegistry,
                logger,
                verbose,
                runId: artifactMatch.runId,
                artifactId: artifactMatch.artifactId,
            });
            return;
        }
        if (req.method !== "POST" || req.url !== "/runs") {
            res.statusCode = 404;
            res.end();
            return;
        }
        const authHeader = req.headers.authorization ?? "";
        if (authHeader !== `Bearer ${authToken}`) {
            if (verbose) {
                logger(`[serve] Unauthorized /runs attempt from ${formatSocket(req)} (missing/invalid token)`);
            }
            res.writeHead(401, { "Content-Type": "application/json" });
            res.end(JSON.stringify({ error: "unauthorized" }));
            return;
        }
        if (busy) {
            if (verbose) {
                logger(`[serve] Busy: rejecting new run from ${formatSocket(req)} while another run is active`);
            }
            res.writeHead(409, { "Content-Type": "application/json" });
            res.end(JSON.stringify({ error: "busy" }));
            return;
        }
        busy = true;
        const runStartedAt = Date.now();
        let payload = null;
        try {
            const body = await readRequestBody(req);
            payload = JSON.parse(body);
            if (payload?.browserConfig) {
                payload.browserConfig.url = normalizeChatgptUrl(payload.browserConfig.url, CHATGPT_URL);
            }
        }
        catch {
            busy = false;
            res.writeHead(400, { "Content-Type": "application/json" });
            res.end(JSON.stringify({ error: "invalid_request" }));
            return;
        }
        res.writeHead(200, { "Content-Type": "application/x-ndjson" });
        const runId = randomUUID();
        logger(`[serve] Accepted run ${runId} from ${formatSocket(req)} (prompt ${payload?.prompt?.length ?? 0} chars)`);
        // Each run gets an isolated temp dir so attachments/logs don't collide.
        const runDir = await mkdtemp(path.join(os.tmpdir(), `oracle-serve-${runId}-`));
        const attachmentDir = path.join(runDir, "attachments");
        await mkdir(attachmentDir, { recursive: true });
        const sendEvent = (event) => {
            res.write(`${JSON.stringify(event)}\n`);
        };
        const attachments = [];
        let fallbackSubmission;
        try {
            const attachmentsPayload = Array.isArray(payload.attachments) ? payload.attachments : [];
            for (const [index, attachment] of attachmentsPayload.entries()) {
                const safeName = sanitizeName(attachment.fileName ?? `attachment-${index + 1}`);
                const filePath = path.join(attachmentDir, safeName);
                await writeFile(filePath, Buffer.from(attachment.contentBase64, "base64"));
                attachments.push({
                    path: filePath,
                    displayPath: attachment.displayPath,
                    sizeBytes: attachment.sizeBytes,
                });
            }
            if (payload.fallbackSubmission) {
                const fallbackAttachmentDir = path.join(runDir, "fallback-attachments");
                await mkdir(fallbackAttachmentDir, { recursive: true });
                const fallbackAttachments = [];
                const fallbackPayload = Array.isArray(payload.fallbackSubmission.attachments)
                    ? payload.fallbackSubmission.attachments
                    : [];
                for (const [index, attachment] of fallbackPayload.entries()) {
                    const safeName = sanitizeName(attachment.fileName ?? `fallback-attachment-${index + 1}`);
                    const filePath = path.join(fallbackAttachmentDir, safeName);
                    await writeFile(filePath, Buffer.from(attachment.contentBase64, "base64"));
                    fallbackAttachments.push({
                        path: filePath,
                        displayPath: attachment.displayPath,
                        sizeBytes: attachment.sizeBytes,
                    });
                }
                fallbackSubmission = {
                    prompt: payload.fallbackSubmission.prompt,
                    attachments: fallbackAttachments,
                };
            }
            // Reuse the existing browser logger surface so clients see the same log stream.
            const automationLogger = ((message) => {
                if (typeof message === "string") {
                    sendEvent({ type: "log", message });
                }
            });
            automationLogger.verbose = Boolean(payload.options.verbose);
            // Preserve an explicit request to leave the completed conversation tab
            // open before the service forces `keepBrowser` for process lifetime.
            const clientRequestedKeepBrowser = payload.browserConfig?.keepBrowser === true;
            // Remote runs always rely on the host's own Chrome profile; ignore any inline cookie transfer.
            if (payload.browserConfig) {
                payload.browserConfig.inlineCookies = null;
                payload.browserConfig.inlineCookiesSource = null;
                payload.browserConfig.cookieSync = true;
            }
            else {
                payload.browserConfig = {};
            }
            // Enforce manual-login profile when cookie sync is unavailable (e.g., Windows/WSL).
            if (options.manualLoginDefault) {
                payload.browserConfig.manualLogin = true;
                payload.browserConfig.manualLoginProfileDir = options.manualLoginProfileDir;
                payload.browserConfig.keepBrowser = true;
                if (verbose) {
                    logger(`[serve] Enforcing manual-login profile at ${options.manualLoginProfileDir ?? "default"} for remote run ${runId}`);
                }
            }
            const result = await runBrowser({
                prompt: payload.prompt,
                attachments,
                fallbackSubmission,
                config: payload.browserConfig,
                // `keepBrowser` above preserves the authenticated shared Chrome
                // process. This separate service policy closes only a successfully
                // captured tab owned by this run, preventing one renderer leak per
                // request while incomplete/reattachable tabs remain untouched.
                closeOwnedTabOnComplete: Boolean(options.manualLoginDefault && !clientRequestedKeepBrowser),
                log: automationLogger,
                heartbeatIntervalMs: payload.options.heartbeatIntervalMs,
                verbose: payload.options.verbose,
                sessionId: payload.options.sessionId,
                followUpPrompts: payload.options.followUpPrompts,
            });
            const artifactRegistration = await registerRemoteArtifacts({
                runId,
                result,
                artifactRegistry,
                logger,
            });
            const artifactDescriptors = artifactRegistration.descriptors;
            if (artifactDescriptors.length > 0) {
                sendEvent({
                    type: "log",
                    message: `[browser] ${artifactDescriptors.length} artifact(s) ready for bridge transfer. ` +
                        "If no cloud-local artifact path appears, upgrade both Oracle bridge endpoints or copy the file manually from the Windows browser host.",
                });
            }
            for (const artifact of artifactDescriptors) {
                sendEvent({ type: "artifact-ready", runId, artifact });
            }
            sendEvent({
                type: "result",
                result: sanitizeResult(result, artifactRegistration.warnings),
            });
            logger(`[serve] Run ${runId} completed in ${Date.now() - runStartedAt}ms${artifactDescriptors.length > 0
                ? `; ${artifactDescriptors.length} artifact(s) ready for bridge transfer`
                : ""}`);
        }
        catch (error) {
            const message = error instanceof Error ? error.message : String(error);
            sendEvent({ type: "error", message });
            logger(`[serve] Run ${runId} failed after ${Date.now() - runStartedAt}ms: ${message}`);
        }
        finally {
            busy = false;
            res.end();
            try {
                await rm(runDir, { recursive: true, force: true });
            }
            catch {
                // ignore cleanup errors
            }
        }
    });
    await new Promise((resolve) => {
        server.listen(options.port ?? 0, options.host ?? "0.0.0.0", () => resolve());
    });
    const address = server.address();
    if (!address || typeof address === "string") {
        throw new Error("Unable to determine server address.");
    }
    const reachable = formatReachableAddresses(address.address, address.port);
    const primary = reachable[0] ?? `${address.address}:${address.port}`;
    const extras = reachable.slice(1);
    const also = extras.length ? `, also [${extras.join(", ")}]` : "";
    logger(color(chalk.cyanBright.bold, `Listening at ${primary}${also}`));
    logger(color(chalk.yellowBright, `Access token: ${authToken}`));
    logger("Leave this terminal running; press Ctrl+C to stop oracle serve.");
    return {
        port: address.port,
        token: authToken,
        async close() {
            await new Promise((resolve, reject) => {
                server.close((err) => (err ? reject(err) : resolve()));
            });
        },
    };
}
export async function serveRemote(options = {}) {
    const manualProfileDir = options.manualLoginProfileDir ?? path.join(os.homedir(), ".oracle", "browser-profile");
    const preferManualLogin = options.manualLoginDefault || process.platform === "win32" || isWsl();
    let cookies = null;
    let opened = false;
    if (isWsl() && process.env.ORACLE_ALLOW_WSL_SERVE !== "1") {
        console.log("WSL detected. For reliable browser automation, run `oracle serve` from Windows PowerShell/Command Prompt so we can use your Windows Chrome profile.");
        console.log("If you want to stay in WSL anyway, set ORACLE_ALLOW_WSL_SERVE=1 and ensure a Linux Chrome is installed, then rerun.");
        console.log("Alternatively, start Windows Chrome with --remote-debugging-port=9222 and use `--remote-chrome <windows-ip>:9222`.");
        return;
    }
    if (!preferManualLogin) {
        // Warm-up: ensure this host has a ChatGPT login before accepting runs.
        const result = await loadLocalChatgptCookies(console.log, CHATGPT_URL);
        cookies = result.cookies;
        opened = result.opened;
    }
    if (!cookies || cookies.length === 0) {
        console.log("No ChatGPT cookies detected on this host.");
        if (preferManualLogin) {
            await mkdir(manualProfileDir, { recursive: true });
            console.log(`Cookie extraction is unavailable on this platform. Using manual-login Chrome profile at ${manualProfileDir}. Remote runs will reuse this profile; sign in once when the browser opens.`);
            const existingPort = await readDevToolsPort(manualProfileDir);
            if (existingPort) {
                const reachable = await verifyDevToolsReachable({ port: existingPort });
                if (reachable.ok) {
                    console.log("Detected an existing automation Chrome session; will reuse it for manual login.");
                }
                else {
                    console.log(`Found stale DevToolsActivePort (port ${existingPort}, ${reachable.error}); launching a fresh manual-login Chrome.`);
                    await cleanupStaleProfileState(manualProfileDir, console.log, {
                        lockRemovalMode: "never",
                    });
                    void launchManualLoginChrome(manualProfileDir, CHATGPT_URL, console.log);
                }
            }
            else {
                void launchManualLoginChrome(manualProfileDir, CHATGPT_URL, console.log);
            }
        }
        else if (opened) {
            console.log("Opened chatgpt.com for login. Sign in, then restart `oracle serve` to continue.");
            return;
        }
        else {
            console.log("Please open https://chatgpt.com/ in this host's browser and sign in; then rerun.");
            console.log("Tip: install xdg-utils (xdg-open) to enable automatic browser opening on Linux/WSL.");
            return;
        }
    }
    else {
        console.log(`Detected ${cookies.length} ChatGPT cookies on this host; runs will reuse this session.`);
    }
    const server = await createRemoteServer({
        ...options,
        manualLoginDefault: preferManualLogin,
        manualLoginProfileDir: manualProfileDir,
    });
    await new Promise((resolve) => {
        const shutdown = () => {
            console.log("Shutting down remote service...");
            server
                .close()
                .catch((error) => console.error("Failed to close remote server:", error))
                .finally(() => resolve());
        };
        process.on("SIGINT", shutdown);
        process.on("SIGTERM", shutdown);
    });
}
function matchArtifactRequest(req) {
    if (req.method !== "GET" || !req.url) {
        return null;
    }
    let url;
    try {
        url = new URL(req.url, "http://oracle.local");
    }
    catch {
        return null;
    }
    const match = /^\/runs\/([^/]+)\/artifacts\/([^/]+)$/.exec(url.pathname);
    if (!match) {
        return null;
    }
    try {
        return {
            runId: decodeURIComponent(match[1] ?? ""),
            artifactId: decodeURIComponent(match[2] ?? ""),
        };
    }
    catch {
        return null;
    }
}
async function serveRemoteArtifact(params) {
    const authHeader = params.req.headers.authorization ?? "";
    if (authHeader !== `Bearer ${params.authToken}`) {
        if (params.verbose) {
            params.logger(`[serve] Unauthorized artifact transfer attempt from ${formatSocket(params.req)} (missing/invalid token)`);
        }
        params.res.writeHead(401, { "Content-Type": "application/json" });
        params.res.end(JSON.stringify({ error: "unauthorized" }));
        return;
    }
    pruneExpiredArtifacts(params.artifactRegistry);
    const key = remoteArtifactKey(params.runId, params.artifactId);
    const artifact = params.artifactRegistry.get(key);
    if (!artifact) {
        params.res.writeHead(404, { "Content-Type": "application/json" });
        params.res.end(JSON.stringify({ error: "artifact_not_found" }));
        return;
    }
    if (Date.now() > artifact.expiresAt) {
        params.artifactRegistry.delete(key);
        params.res.writeHead(410, { "Content-Type": "application/json" });
        params.res.end(JSON.stringify({ error: "artifact_expired" }));
        return;
    }
    const fileStat = await stat(artifact.filePath).catch(() => null);
    if (!fileStat?.isFile() || fileStat.size <= 0) {
        params.res.writeHead(410, { "Content-Type": "application/json" });
        params.res.end(JSON.stringify({ error: "artifact_unavailable" }));
        return;
    }
    if (fileStat.size > MAX_REMOTE_ARTIFACT_BYTES) {
        params.res.writeHead(413, { "Content-Type": "application/json" });
        params.res.end(JSON.stringify({ error: "artifact_too_large" }));
        return;
    }
    const filename = sanitizeArtifactFilename(artifact.descriptor.filename, "artifact.bin");
    params.res.writeHead(200, {
        "Content-Type": sanitizeArtifactMimeType(artifact.descriptor.mimeType) ?? "application/octet-stream",
        "Content-Length": fileStat.size,
        "Content-Disposition": `attachment; filename="${filename.replace(/"/g, "")}"`,
        "Cache-Control": "no-store",
        "X-Content-Type-Options": "nosniff",
        "X-Oracle-Artifact-Id": artifact.descriptor.artifactId,
        "X-Oracle-Artifact-Sha256": artifact.descriptor.sha256,
    });
    await pipeline(createReadStream(artifact.filePath), params.res).catch((error) => {
        params.logger(`[serve] Artifact transfer failed for ${artifact.descriptor.artifactId}: ${error instanceof Error ? error.message : String(error)}`);
    });
}
function pruneExpiredArtifacts(artifactRegistry) {
    const now = Date.now();
    for (const [key, artifact] of artifactRegistry) {
        if (artifact.expiresAt <= now) {
            artifactRegistry.delete(key);
        }
    }
}
function remoteArtifactKey(runId, artifactId) {
    return `${runId}:${artifactId}`;
}
async function registerRemoteArtifacts(params) {
    pruneExpiredArtifacts(params.artifactRegistry);
    const seen = new Set();
    const fileArtifacts = [
        ...(params.result.savedFiles ?? []),
        ...(params.result.artifacts ?? []).filter((artifact) => artifact.kind === "file"),
    ];
    const descriptors = [];
    const warnings = [];
    for (const artifact of fileArtifacts) {
        if (!artifact?.path || seen.has(artifact.path)) {
            continue;
        }
        seen.add(artifact.path);
        const registration = await buildRemoteArtifactRegistration(params.runId, artifact).catch((error) => {
            const filename = sanitizeArtifactFilename(path.basename(artifact.path), "artifact.bin");
            params.logger(`[serve] Skipping remote artifact descriptor: ${error instanceof Error ? error.message : String(error)}`);
            warnings.push({
                code: "remote-artifact-registration-failed",
                severity: "warning",
                message: `Oracle captured the browser text response, but the bridge host could not prepare ${filename} for transfer. ` +
                    "Open the ChatGPT browser on the bridge host, download the ZIP/file shown in the current response, and copy it to a cloud-readable path.",
            });
            return null;
        });
        if (!registration) {
            continue;
        }
        params.artifactRegistry.set(remoteArtifactKey(params.runId, registration.descriptor.artifactId), {
            descriptor: registration.descriptor,
            filePath: registration.filePath,
            expiresAt: Date.now() + REMOTE_ARTIFACT_TTL_MS,
        });
        descriptors.push(registration.descriptor);
    }
    return { descriptors, warnings };
}
async function buildRemoteArtifactRegistration(runId, artifact) {
    if (artifact.path.endsWith(".crdownload")) {
        throw new Error("artifact is still a Chrome partial download");
    }
    const filePath = await resolveRegisteredArtifactPath(artifact.path);
    const fileStat = await stat(filePath);
    if (!fileStat.isFile() || fileStat.size <= 0) {
        throw new Error("artifact is not a completed non-empty file");
    }
    if (fileStat.size > MAX_REMOTE_ARTIFACT_BYTES) {
        throw new Error("artifact exceeds bridge transfer size limit");
    }
    const filename = sanitizeArtifactFilename(path.basename(filePath), "artifact.bin");
    const mimeType = sanitizeArtifactMimeType(artifact.mimeType);
    // Recompute security metadata from the exact file registered for transfer.
    const validation = await validateArtifactFile({
        path: filePath,
        filename,
        mimeType,
    });
    const sha256 = await computeFileSha256(filePath);
    return {
        filePath,
        descriptor: {
            artifactId: randomUUID(),
            runId,
            kind: "file",
            filename,
            mimeType,
            byteSize: fileStat.size,
            sha256,
            validation,
            sourceUrlKind: classifySourceUrlKind(artifact.sourceUrl),
            transferStatus: "ready",
        },
    };
}
async function resolveRegisteredArtifactPath(filePath) {
    const [resolvedFile, sessionsRoot] = await Promise.all([
        realpath(filePath),
        realpath(path.join(getOracleHomeDir(), "sessions")),
    ]);
    const relative = path.relative(sessionsRoot, resolvedFile);
    const segments = relative.split(path.sep);
    if (!relative ||
        relative.startsWith(`..${path.sep}`) ||
        path.isAbsolute(relative) ||
        segments.length < 3 ||
        segments[1] !== "artifacts") {
        throw new Error("artifact is outside Oracle's session artifact boundary");
    }
    return resolvedFile;
}
function classifySourceUrlKind(sourceUrl) {
    if (sourceUrl?.startsWith("sandbox:")) {
        return "sandbox";
    }
    if (sourceUrl === "browser-download") {
        return "browser-download";
    }
    return "chatgpt-file-endpoint";
}
async function readRequestBody(req) {
    const chunks = [];
    for await (const chunk of req) {
        chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
    }
    return Buffer.concat(chunks).toString("utf8");
}
function sanitizeName(raw) {
    return raw.replace(/[^a-zA-Z0-9._-]/g, "_");
}
function sanitizeResult(result, warnings = []) {
    return {
        answerText: result.answerText,
        answerMarkdown: result.answerMarkdown,
        answerHtml: result.answerHtml,
        tookMs: result.tookMs,
        answerTokens: result.answerTokens,
        answerChars: result.answerChars,
        warnings: warnings.length > 0 ? warnings : undefined,
        chromePid: undefined,
        chromePort: undefined,
        userDataDir: undefined,
    };
}
function formatSocket(req) {
    const socket = req.socket;
    const host = socket.remoteAddress ?? "unknown";
    const port = socket.remotePort ?? "0";
    return `${host}:${port}`;
}
function formatReachableAddresses(bindAddress, port) {
    const ipv4 = [];
    const ipv6 = [];
    if (bindAddress && bindAddress !== "::" && bindAddress !== "0.0.0.0") {
        if (bindAddress.includes(":")) {
            ipv6.push(`[${bindAddress}]:${port}`);
        }
        else {
            ipv4.push(`${bindAddress}:${port}`);
        }
    }
    try {
        const interfaces = os.networkInterfaces();
        for (const entries of Object.values(interfaces)) {
            if (!entries)
                continue;
            for (const entry of entries) {
                const iface = entry;
                if (!iface || iface.internal)
                    continue;
                const family = typeof iface.family === "string"
                    ? iface.family
                    : iface.family === 4
                        ? "IPv4"
                        : iface.family === 6
                            ? "IPv6"
                            : "";
                if (family === "IPv4") {
                    const addr = iface.address;
                    if (addr.startsWith("127."))
                        continue;
                    if (addr.startsWith("169.254."))
                        continue; // APIPA/link-local
                    ipv4.push(`${addr}:${port}`);
                }
                else if (family === "IPv6") {
                    const addr = iface.address.toLowerCase();
                    if (addr === "::1" || addr.startsWith("fe80:"))
                        continue; // loopback/link-local
                    ipv6.push(`[${iface.address}]:${port}`);
                }
            }
        }
    }
    catch {
        // network interface probing can fail in locked-down environments; ignore
    }
    // de-dup
    return Array.from(new Set([...ipv4, ...ipv6]));
}
async function loadLocalChatgptCookies(logger, targetUrl) {
    try {
        logger("Loading ChatGPT cookies from this host's Chrome profile...");
        const { cookies: rawCookies, warnings } = await getCookies({
            url: targetUrl,
            browsers: ["chrome"],
            mode: "merge",
            chromeProfile: "Default",
            timeoutMs: 5_000,
        });
        if (warnings.length) {
            logger(`Cookie warnings:\n- ${warnings.join("\n- ")}`);
        }
        const cookies = rawCookies.map(toCdpCookie).filter((c) => Boolean(c));
        if (!cookies || cookies.length === 0) {
            logger("No local ChatGPT cookies found on this host. Please log in once; opening ChatGPT...");
            const opened = triggerLocalLoginPrompt(logger, targetUrl);
            return { cookies: null, opened };
        }
        logger(`Loaded ${cookies.length} local ChatGPT cookies on this host.`);
        return { cookies, opened: false };
    }
    catch (error) {
        const message = error instanceof Error ? error.message : String(error);
        const missingDbMatch = message.match(/Unable to locate Chrome cookie DB at (.+?)(?:\.|$)/);
        if (missingDbMatch) {
            const lookedPath = missingDbMatch[1];
            logger(`Chrome cookies not found at ${lookedPath}. Set --browser-cookie-path to your Chrome profile or log in manually.`);
        }
        else {
            logger(`Unable to load local ChatGPT cookies on this host: ${message}`);
        }
        if (process.platform === "linux" && isWsl()) {
            logger("WSL hint: Chrome lives under /mnt/c/Users/<you>/AppData/Local/Google/Chrome/User Data/Default; pass --browser-cookie-path to that directory if auto-detect fails.");
        }
        const opened = triggerLocalLoginPrompt(logger, targetUrl);
        return { cookies: null, opened };
    }
}
function toCdpCookie(cookie) {
    if (!cookie?.name)
        return null;
    const out = {
        name: cookie.name,
        value: cookie.value,
        domain: cookie.domain,
        path: cookie.path ?? "/",
        secure: cookie.secure ?? true,
        httpOnly: cookie.httpOnly ?? false,
    };
    if (typeof cookie.expires === "number")
        out.expires = cookie.expires;
    if (cookie.sameSite === "Lax" || cookie.sameSite === "Strict" || cookie.sameSite === "None") {
        out.sameSite = cookie.sameSite;
    }
    return out;
}
function triggerLocalLoginPrompt(logger, url) {
    const verbose = process.argv.includes("--verbose") || process.env.ORACLE_SERVE_VERBOSE === "1";
    const openers = [];
    if (process.platform === "darwin") {
        openers.push({ cmd: "open" });
    }
    else if (process.platform === "win32") {
        openers.push({ cmd: "start" });
    }
    else {
        if (isWsl()) {
            // Prefer wslview when available, then fall back to Windows start.exe to open in the host browser.
            openers.push({ cmd: "wslview" });
            openers.push({ cmd: "cmd.exe", args: ["/c", "start", "", url] });
        }
        openers.push({ cmd: "xdg-open" });
    }
    // Add a cross-platform, low-friction fallback when nothing above is available.
    openers.push({ cmd: "sensible-browser" });
    try {
        // Fire and forget; user completes login in the opened browser window.
        if (verbose) {
            logger(`[serve] Login opener candidates: ${openers.map((o) => o.cmd).join(", ")}`);
        }
        const candidate = openers.find((opener) => canSpawn(opener.cmd));
        if (candidate) {
            const child = spawn(candidate.cmd, candidate.args ?? [url], {
                stdio: "ignore",
                detached: true,
            });
            child.unref();
            child.once("error", (error) => {
                if (verbose) {
                    logger(`[serve] Opener ${candidate.cmd} failed: ${error instanceof Error ? error.message : String(error)}`);
                }
                logger(`Please open ${url} in this host's browser and sign in; then rerun.`);
            });
            logger(`Opened ${url} locally via ${candidate.cmd}. Please sign in; subsequent runs will reuse the session.`);
            if (verbose && candidate.args) {
                logger(`[serve] Opener args: ${candidate.args.join(" ")}`);
            }
            return true;
        }
        if (verbose) {
            logger("[serve] No available opener found; prompting manual login.");
        }
        return false;
    }
    catch {
        return false;
    }
}
function isWsl() {
    if (process.platform !== "linux")
        return false;
    return Boolean(process.env.WSL_DISTRO_NAME || os.release().toLowerCase().includes("microsoft"));
}
function canSpawn(cmd) {
    if (!cmd)
        return false;
    try {
        if (process.platform === "win32") {
            // `where` returns non-zero when the command is not found.
            const result = spawnSync("where", [cmd], { stdio: "ignore" });
            return result.status === 0;
        }
        // `command -v` is a shell builtin; run through sh. Fallback to `which`.
        const shResult = spawnSync("sh", ["-c", `command -v ${cmd}`], { stdio: "ignore" });
        if (shResult.status === 0)
            return true;
        const whichResult = spawnSync("which", [cmd], { stdio: "ignore" });
        return whichResult.status === 0;
    }
    catch {
        return false;
    }
}
async function launchManualLoginChrome(profileDir, url, logger) {
    const timeoutMs = 7000;
    let finished = false;
    const timeout = setTimeout(() => {
        if (!finished) {
            logger(`Timed out launching Chrome for manual login. Launch Chrome manually with --user-data-dir=${profileDir} and log in to ${url}.`);
        }
    }, timeoutMs);
    try {
        const chromeLauncher = await import("chrome-launcher");
        const { launch } = chromeLauncher;
        const debugPort = await findAvailablePort();
        logger(`Planned manual-login Chrome DevTools port: ${debugPort}`);
        const chrome = await launch({
            // Expose DevTools so later runs can attach instead of spawning a second Chrome.
            // Use a per-serve free port so the login window stays stable for all runs.
            port: debugPort,
            userDataDir: profileDir,
            startingUrl: url,
            chromeFlags: [
                "--no-first-run",
                "--no-default-browser-check",
                `--user-data-dir=${profileDir}`,
                "--remote-allow-origins=*",
                `--remote-debugging-port=${debugPort}`, // ensure DevToolsActivePort is written even on Windows
            ],
        });
        const chosenPort = chrome?.port ?? debugPort ?? null;
        if (chosenPort) {
            // Persist DevToolsActivePort eagerly so future runs can attach/reuse this Chrome.
            await writeDevToolsActivePort(profileDir, chosenPort);
            if (chrome?.pid) {
                await writeChromePid(profileDir, chrome.pid);
            }
            logger(`Manual-login Chrome DevTools port: ${chosenPort}`);
            logger(`If needed, DevTools JSON at http://127.0.0.1:${chosenPort}/json/version`);
        }
        else {
            logger("Warning: unable to determine manual-login Chrome DevTools port. Remote runs may fail to attach.");
        }
        finished = true;
        clearTimeout(timeout);
        const portInfo = chosenPort ? ` (DevTools port ${chosenPort})` : "";
        logger(`Opened Chrome with manual-login profile at ${profileDir}${portInfo}. Complete login, then rerun remote sessions.`);
    }
    catch (error) {
        finished = true;
        clearTimeout(timeout);
        const message = error instanceof Error ? error.message : String(error);
        logger(`Unable to open Chrome for manual login (${message}). Launch Chrome manually with --user-data-dir=${profileDir} and log in to ${url}.`);
    }
}
