// ADR-0017 D8 — eight /api/browser/* BFF route handlers. CSRF guarding is enforced by the
// server.ts state-changing-request gate (POST/DELETE all flow through it). GET status + GET
// events are exempt by the same gate. SSE framing reuses the existing sse.ts helpers.
import { BrowserToolError, } from "@oscharko-dev/keiko-tools";
import { SSE_HEADERS, readyMessage } from "./sse.js";
import { errorBody, STREAMING, } from "./routes.js";
const MAX_BROWSER_BODY_BYTES = 64_000;
class BodyTooLargeError extends Error {
    constructor() {
        super("browser request body too large");
        this.name = "BodyTooLargeError";
    }
}
function noBrowserDeps() {
    return {
        status: 503,
        body: errorBody("BROWSER_UNAVAILABLE", "Browser tool is not configured for this BFF."),
    };
}
function requireBrowser(deps) {
    return deps.browser ?? noBrowserDeps();
}
function isRouteResult(value) {
    return typeof value.status === "number";
}
function toRouteResult(error) {
    return { status: error.status, body: errorBody(error.code, error.message) };
}
function readBody(req) {
    return new Promise((resolve, reject) => {
        const chunks = [];
        let total = 0;
        let capped = false;
        req.on("data", (chunk) => {
            total += chunk.length;
            if (total > MAX_BROWSER_BODY_BYTES) {
                if (!capped) {
                    capped = true;
                    chunks.length = 0;
                    reject(new BodyTooLargeError());
                    req.resume();
                }
                return;
            }
            chunks.push(chunk);
        });
        req.on("end", () => {
            if (!capped)
                resolve(Buffer.concat(chunks).toString("utf8"));
        });
        req.on("error", reject);
    });
}
async function readJsonObject(req) {
    const raw = await readBody(req);
    if (raw.length === 0)
        return {};
    let parsed;
    try {
        parsed = JSON.parse(raw);
    }
    catch {
        throw new BrowserToolError("BAD_REQUEST", "Request body is not valid JSON.");
    }
    if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
        throw new BrowserToolError("BAD_REQUEST", "Request body must be a JSON object.");
    }
    return parsed;
}
function requireString(body, key) {
    const value = body[key];
    if (typeof value !== "string" || value.length === 0) {
        throw new BrowserToolError("BAD_REQUEST", `Field "${key}" must be a non-empty string.`);
    }
    return value;
}
function requireNumber(body, key) {
    const value = body[key];
    if (typeof value !== "number" || !Number.isFinite(value)) {
        throw new BrowserToolError("BAD_REQUEST", `Field "${key}" must be a finite number.`);
    }
    return value;
}
async function runHandler(work) {
    try {
        return await work();
    }
    catch (error) {
        if (error instanceof BodyTooLargeError) {
            return {
                status: 413,
                body: errorBody("PAYLOAD_TOO_LARGE", "Request body exceeds the size limit."),
            };
        }
        if (error instanceof BrowserToolError)
            return toRouteResult(error);
        throw error;
    }
}
const UNREACHABLE_BODY = {
    reachable: false,
    userAgent: null,
    browserVersion: null,
    webSocketDebuggerUrl: null,
};
export async function handleBrowserStatus(ctx, deps) {
    const guard = requireBrowser(deps);
    if (isRouteResult(guard))
        return guard;
    return runHandler(async () => {
        const portParam = ctx.url.searchParams.get("port");
        if (portParam === null) {
            throw new BrowserToolError("BAD_REQUEST", "Query parameter 'port' is required.");
        }
        const port = Number.parseInt(portParam, 10);
        if (!Number.isFinite(port)) {
            throw new BrowserToolError("BAD_REQUEST", "Query parameter 'port' must be an integer.");
        }
        try {
            const status = await guard.checkStatus(port);
            return { status: 200, body: status };
        }
        catch (err) {
            if (err instanceof BrowserToolError && err.code === "CHROME_UNREACHABLE") {
                return { status: 200, body: UNREACHABLE_BODY };
            }
            throw err;
        }
    });
}
export async function handleCreateBrowserSession(ctx, deps) {
    const guard = requireBrowser(deps);
    if (isRouteResult(guard))
        return guard;
    return runHandler(async () => {
        const body = await readJsonObject(ctx.req);
        const port = requireNumber(body, "port");
        const meta = await guard.openSession(port);
        return { status: 201, body: meta };
    });
}
export async function handleDeleteBrowserSession(ctx, deps) {
    const guard = requireBrowser(deps);
    if (isRouteResult(guard))
        return guard;
    return runHandler(async () => {
        const sessionId = ctx.params.sessionId ?? "";
        await guard.closeSession(sessionId);
        return { status: 200, body: { ok: true } };
    });
}
export async function handleBrowserNavigate(ctx, deps) {
    const guard = requireBrowser(deps);
    if (isRouteResult(guard))
        return guard;
    return runHandler(async () => {
        const body = await readJsonObject(ctx.req);
        const url = requireString(body, "url");
        const sessionId = ctx.params.sessionId ?? "";
        const result = await guard.navigate(sessionId, url);
        return { status: 200, body: result };
    });
}
export async function handleBrowserScreenshot(ctx, deps) {
    const guard = requireBrowser(deps);
    if (isRouteResult(guard))
        return guard;
    return runHandler(async () => {
        // Drain the body through readJsonObject so the 64 KB cap (MAX_BROWSER_BODY_BYTES) is
        // enforced even though this handler expects no fields. Raw node:http has no global cap.
        await readJsonObject(ctx.req);
        const sessionId = ctx.params.sessionId ?? "";
        const result = await guard.screenshot(sessionId);
        return { status: 200, body: result };
    });
}
export async function handleBrowserApplyScreenshot(ctx, deps) {
    const guard = requireBrowser(deps);
    if (isRouteResult(guard))
        return guard;
    return runHandler(async () => {
        const body = await readJsonObject(ctx.req);
        const captureSeq = requireNumber(body, "captureSeq");
        const sessionId = ctx.params.sessionId ?? "";
        const result = await guard.applyScreenshot(sessionId, captureSeq);
        return { status: 200, body: result };
    });
}
export async function handleBrowserContent(ctx, deps) {
    const guard = requireBrowser(deps);
    if (isRouteResult(guard))
        return guard;
    return runHandler(async () => {
        // Drain the body through readJsonObject so the 64 KB cap (MAX_BROWSER_BODY_BYTES) is
        // enforced even though this handler expects no fields. Raw node:http has no global cap.
        await readJsonObject(ctx.req);
        const sessionId = ctx.params.sessionId ?? "";
        const result = await guard.content(sessionId);
        return { status: 200, body: result };
    });
}
// SSE — re-uses the existing framer shape. Each browser event becomes one SSE message with
// event: <kind> and data: <JSON>. A 'ready' synthetic is sent first so clients can transition
// from connecting to live.
export function handleBrowserEvents(ctx, deps) {
    const guard = requireBrowser(deps);
    if (isRouteResult(guard))
        return guard;
    const sessionId = ctx.params.sessionId ?? "";
    if (sessionId.length === 0) {
        return { status: 400, body: errorBody("BAD_REQUEST", "sessionId is required.") };
    }
    if (!guard.hasSession(sessionId)) {
        return { status: 404, body: errorBody("SESSION_NOT_FOUND", "Browser session not found.") };
    }
    openBrowserSseStream(ctx.res, guard, sessionId, deps.redactor);
    ctx.req.on("close", () => {
        ctx.res.end();
    });
    return STREAMING;
}
function openBrowserSseStream(res, manager, sessionId, redactor) {
    res.writeHead(200, SSE_HEADERS);
    let seq = 0;
    const unsubscribe = manager.subscribe(sessionId, (event) => {
        seq += 1;
        writeBrowserEvent(res, event, seq, redactor);
        if (event.kind === "session-closed") {
            unsubscribe();
            res.end();
        }
    });
    res.write(readyMessage());
    res.on("close", () => {
        unsubscribe();
    });
}
function writeBrowserEvent(res, event, seq, redactor) {
    const redacted = redactor(event);
    const data = JSON.stringify(redacted);
    const frame = `id: ${String(seq)}\nevent: browser:${event.kind}\ndata: ${data}\n\n`;
    if (!res.write(frame)) {
        res.destroy();
    }
}
