import { createReadStream, statSync } from "node:fs";
import { brotliCompressSync, constants as zlibConstants, gzipSync } from "node:zlib";
const DEFAULT_JSON_BODY_LIMIT = 64 * 1024 * 1024;
const TEXT_COMPRESSION_THRESHOLD_BYTES = 1024;
const JSON_COMPRESSION_THRESHOLD_BYTES = 2048;
const DYNAMIC_GZIP_LEVEL = 6;
const DYNAMIC_BROTLI_QUALITY = 4;
const BASE_SECURITY_HEADERS = {
    "x-content-type-options": "nosniff",
    "x-frame-options": "DENY",
    "referrer-policy": "no-referrer",
    "permissions-policy": "camera=(), microphone=(self), geolocation=()",
};
const JSON_HEADERS = { ...webSecurityHeaders(), "content-type": "application/json; charset=utf-8", "cache-control": "no-store" };
const responseRequests = new WeakMap();
export function registerWebResponseRequest(req, res) {
    responseRequests.set(res, req);
}
export function parseCookies(cookieHeader) {
    const cookies = {};
    for (const part of cookieHeader.split(";")) {
        const [key, ...valueParts] = part.trim().split("=");
        if (!key) {
            continue;
        }
        try {
            cookies[key] = decodeURIComponent(valueParts.join("=") ?? "");
        }
        catch {
            // Malformed cookie values should not turn auth/session checks into 500s.
        }
    }
    return cookies;
}
export class RequestBodyTooLargeError extends Error {
    statusCode = 413;
}
export function isRequestBodyTooLargeError(error) {
    return error instanceof RequestBodyTooLargeError;
}
export class InvalidJsonBodyError extends Error {
    statusCode = 400;
}
export function isInvalidJsonBodyError(error) {
    return error instanceof InvalidJsonBodyError;
}
export class WebAccessDeniedError extends Error {
    statusCode = 403;
}
export function isWebAccessDeniedError(error) {
    return error instanceof WebAccessDeniedError ||
        (typeof error === "object" && error !== null && "statusCode" in error && error.statusCode === 403);
}
export async function readJsonBody(req, maxBytes = DEFAULT_JSON_BODY_LIMIT) {
    const chunks = [];
    let size = 0;
    for await (const chunk of req) {
        const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
        size += buffer.length;
        if (size > maxBytes) {
            throw new RequestBodyTooLargeError(`Request body is too large. Max ${Math.round(maxBytes / 1024 / 1024)} MB.`);
        }
        chunks.push(buffer);
    }
    const text = Buffer.concat(chunks).toString("utf8").trim();
    if (!text) {
        return {};
    }
    try {
        const parsed = JSON.parse(text);
        return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
    }
    catch (error) {
        throw new InvalidJsonBodyError(`Invalid JSON request body: ${error instanceof Error ? error.message : String(error)}`);
    }
}
export function sendJson(res, status, value) {
    sendBuffer(res, status, Buffer.from(`${JSON.stringify(value)}\n`, "utf8"), JSON_HEADERS, { compressionThresholdBytes: JSON_COMPRESSION_THRESHOLD_BYTES });
}
export function sendText(res, status, text, contentType, options = {}) {
    const body = isHtmlContentType(contentType) ? minifyHtml(text) : text;
    sendBuffer(res, status, Buffer.from(body, "utf8"), {
        ...webSecurityHeaders(options.cspNonce),
        "content-type": contentType,
        "cache-control": options.cacheControl ?? "no-store",
    }, { compressionThresholdBytes: TEXT_COMPRESSION_THRESHOLD_BYTES });
}
export function sendFile(res, filePath, filename) {
    res.writeHead(200, {
        ...webSecurityHeaders(),
        "content-type": "application/octet-stream",
        "content-disposition": `attachment; filename="${filename.replace(/"/g, "")}"`,
    });
    createReadStream(filePath).pipe(res);
}
export function sendStaticFile(res, filePath, contentType, options = {}) {
    const req = responseRequests.get(res);
    const selected = selectPrecompressedFile(req, {
        br: options.brotliPath,
        gzip: options.gzipPath,
    });
    const selectedPath = selected?.filePath ?? filePath;
    const headers = {
        ...webSecurityHeaders(),
        "content-type": contentType,
        "cache-control": options.cacheControl ?? "public, max-age=86400",
        "content-length": statSync(selectedPath).size,
    };
    if (selected?.encoding) {
        headers["content-encoding"] = selected.encoding;
    }
    if (options.brotliPath || options.gzipPath) {
        headers.vary = "Accept-Encoding";
    }
    res.writeHead(200, headers);
    createReadStream(selectedPath).pipe(res);
}
export function stringField(value, key) {
    const field = value[key];
    if (typeof field !== "string" || !field.trim()) {
        throw new Error(`${key} is required`);
    }
    return field.trim();
}
export function optionalStringField(value, key) {
    const field = value[key];
    return typeof field === "string" && field.trim() ? field.trim() : undefined;
}
export function optionalBooleanField(value, key) {
    const field = value[key];
    return typeof field === "boolean" ? field : undefined;
}
export function numberField(value, key) {
    const field = value[key];
    const parsed = typeof field === "number" ? field : typeof field === "string" ? Number(field) : Number.NaN;
    if (!Number.isInteger(parsed)) {
        throw new Error(`${key} must be an integer`);
    }
    return parsed;
}
export function optionalNumberField(value, key) {
    if (value[key] === undefined || value[key] === "") {
        return undefined;
    }
    return numberField(value, key);
}
export function arrayStringField(value, key) {
    const field = value[key];
    if (field === undefined || field === null || field === "") {
        return [];
    }
    if (Array.isArray(field)) {
        return field.filter((item) => typeof item === "string");
    }
    if (typeof field === "string") {
        return field.split(",").map((item) => item.trim()).filter(Boolean);
    }
    throw new Error(`${key} must be a string list`);
}
export function arrayNumberField(value, key) {
    const field = value[key];
    if (field === undefined || field === null || field === "") {
        return [];
    }
    if (Array.isArray(field)) {
        return field.map((item) => typeof item === "number" ? item : Number(item)).filter((item) => Number.isInteger(item));
    }
    if (typeof field === "string") {
        return field.split(",").map((item) => Number(item.trim())).filter((item) => Number.isInteger(item));
    }
    throw new Error(`${key} must be a number list`);
}
export function parseAgentUpdateOperation(value) {
    if (!value || value === "update") {
        return "update";
    }
    if (value === "install") {
        return "install";
    }
    throw new Error(`Invalid agent update operation: ${value}`);
}
export function parseLogTarget(value) {
    return value === "update" || value === "agent-updates" ? value : "connector";
}
export function objectRecord(value) {
    if (!value || typeof value !== "object" || Array.isArray(value)) {
        return {};
    }
    return value;
}
export function parseUploadFiles(value) {
    if (!Array.isArray(value)) {
        return [];
    }
    return value.map((item, index) => {
        if (!item || typeof item !== "object" || Array.isArray(item)) {
            throw new Error(`files[${index}] must be an object`);
        }
        const record = item;
        const name = typeof record.name === "string" && record.name.trim() ? record.name.trim() : `upload-${index + 1}`;
        const mimeType = typeof record.mimeType === "string" ? record.mimeType.trim() : undefined;
        const dataBase64 = typeof record.dataBase64 === "string" ? record.dataBase64 : "";
        if (!dataBase64) {
            throw new Error(`files[${index}].dataBase64 is required`);
        }
        return { name, mimeType, data: Buffer.from(stripDataUrlPrefix(dataBase64), "base64") };
    });
}
export function numberParam(url, key, fallback) {
    const value = Number(url.searchParams.get(key));
    return Number.isFinite(value) && value > 0 ? Math.floor(value) : fallback;
}
export function requiredSearch(url, key) {
    const value = url.searchParams.get(key);
    if (!value) {
        throw new Error(`${key} is required`);
    }
    return value;
}
function stripDataUrlPrefix(value) {
    const comma = value.indexOf(",");
    return value.startsWith("data:") && comma !== -1 ? value.slice(comma + 1) : value;
}
export function webSecurityHeaders(cspNonce) {
    const scriptSrc = cspNonce ? `'self' 'nonce-${cspNonce}'` : "'self'";
    const styleSrc = cspNonce ? `'self' 'nonce-${cspNonce}'` : "'self'";
    return {
        ...BASE_SECURITY_HEADERS,
        "content-security-policy": `default-src 'self'; script-src ${scriptSrc}; style-src ${styleSrc}; connect-src 'self'; img-src 'self' data:; font-src 'self'; object-src 'none'; frame-src 'self' blob:; child-src 'self' blob:; frame-ancestors 'none'; base-uri 'self'; form-action 'self'`,
    };
}
function sendBuffer(res, status, body, headers, options) {
    const req = responseRequests.get(res);
    const encoded = encodeBodyForRequest(req, body, options.compressionThresholdBytes);
    const responseHeaders = {
        ...headers,
        "content-length": encoded.body.length,
    };
    if (req) {
        responseHeaders.vary = "Accept-Encoding";
    }
    if (encoded.encoding) {
        responseHeaders["content-encoding"] = encoded.encoding;
    }
    res.writeHead(status, responseHeaders);
    res.end(encoded.body);
}
function encodeBodyForRequest(req, body, thresholdBytes) {
    if (!req || body.length < thresholdBytes) {
        return { body };
    }
    const encoding = preferredResponseEncoding(req, { br: true, gzip: true });
    if (encoding === "br") {
        return {
            body: brotliCompressSync(body, { params: { [zlibConstants.BROTLI_PARAM_QUALITY]: DYNAMIC_BROTLI_QUALITY } }),
            encoding,
        };
    }
    if (encoding === "gzip") {
        return { body: gzipSync(body, { level: DYNAMIC_GZIP_LEVEL }), encoding };
    }
    return { body };
}
function selectPrecompressedFile(req, paths) {
    const encoding = preferredResponseEncoding(req, { br: Boolean(paths.br), gzip: Boolean(paths.gzip) });
    if (encoding === "br" && paths.br) {
        return { encoding, filePath: paths.br };
    }
    if (encoding === "gzip" && paths.gzip) {
        return { encoding, filePath: paths.gzip };
    }
    return null;
}
function preferredResponseEncoding(req, available) {
    if (!req) {
        return null;
    }
    const brQ = available.br ? acceptedEncodingQ(req, "br") : 0;
    const gzipQ = available.gzip ? acceptedEncodingQ(req, "gzip") : 0;
    if (brQ <= 0 && gzipQ <= 0) {
        return null;
    }
    return brQ >= gzipQ ? "br" : "gzip";
}
function acceptedEncodingQ(req, encoding) {
    const header = req.headers["accept-encoding"];
    const value = Array.isArray(header) ? header.join(",") : header ?? "";
    let wildcardQ = 0;
    for (const part of value.split(",")) {
        const [rawName, ...rawParams] = part.trim().split(";").map((item) => item.trim());
        if (!rawName) {
            continue;
        }
        const q = rawParams.reduce((current, param) => {
            const match = /^q=([0-9.]+)$/i.exec(param);
            return match ? Number(match[1]) : current;
        }, 1);
        if (rawName.toLowerCase() === encoding) {
            return Number.isFinite(q) ? q : 0;
        }
        if (rawName === "*") {
            wildcardQ = Number.isFinite(q) ? q : 0;
        }
    }
    return wildcardQ;
}
function isHtmlContentType(contentType) {
    return contentType.toLowerCase().startsWith("text/html");
}
export function minifyHtml(html) {
    return html.replace(/>\s+</g, "><").trim();
}
