import { createHash, timingSafeEqual } from "node:crypto";
import { createBridgeAuthorityDiagnostic, } from "../authority/bridgeAuthorityContract.js";
import { LocalUiCapabilityError, } from "../capabilities/LocalUiCapabilityService.js";
import { isRecord } from "../utils/json.js";
import { PUBLIC_IDENTIFIER_SCAN_OPTIONS, writeJson } from "./publicResponse.js";
import { readBearerToken, readHeaderString, readOrigin, } from "./requestParsing.js";
export function authenticateRequest(request, app, requiredScope) {
    const bridge = requireBridgeToken(request, app.bridgeToken);
    if (bridge.kind === "bridge")
        return bridge;
    if (!requiredScope)
        return bridge;
    const hasLocalUiCredential = hasLocalUiSessionCredential(request);
    if (hasLocalUiCredential || readOrigin(request)) {
        const local = authenticateLocalUiSession(request, app, requiredScope);
        if (local.kind === "local-ui-session")
            return local;
        if (hasLocalUiCredential)
            return local;
    }
    return bridge;
}
export function authenticateLocalUiAuthorityRequest(request, app) {
    if (hasValidBridgeToken(request, app.bridgeToken)) {
        return {
            kind: "error",
            statusCode: 403,
            code: "local_ui_session_required",
            message: "This browser-facing bridge surface requires a local UI session.",
            diagnostic: {
                guard: "local-ui-session-required",
                reason: "local_ui_session_required",
                status: 403,
                metadata: {
                    authScheme: "bridge-token",
                    bridgeTokenPresent: true,
                },
            },
        };
    }
    if (hasBridgeTokenHeader(request)) {
        return {
            kind: "error",
            statusCode: 401,
            code: "invalid_authentication",
            message: "The bridge authority credential was not accepted.",
            diagnostic: {
                guard: "auth",
                reason: "invalid_authentication",
                status: 401,
                metadata: {
                    authScheme: "bridge-token",
                    bridgeTokenPresent: true,
                },
            },
        };
    }
    return authenticateLocalUiSession(request, app, "bridge:read");
}
export function requireBridgeToken(request, token) {
    if (hasValidBridgeToken(request, token))
        return { kind: "bridge" };
    const hasCredential = hasAnyAuthCredential(request);
    return {
        kind: "error",
        statusCode: 401,
        code: hasCredential ? "invalid_authentication" : "unauthorized",
        message: "Missing or invalid PatchTrail bridge capability token.",
        diagnostic: {
            guard: "auth",
            reason: hasCredential
                ? "invalid_authentication"
                : "missing_authentication",
            status: 401,
            metadata: {
                authScheme: authSchemeForPresentedCredential(request),
                bridgeTokenPresent: hasBridgeTokenHeader(request),
            },
        },
    };
}
export function authenticateLocalUiSession(request, app, requiredScope) {
    const origin = readOrigin(request);
    const authorization = readHeaderString(request, "authorization");
    const bearerToken = readBearerToken(request);
    const headerToken = readHeaderString(request, "x-patchtrail-local-ui-session");
    const token = bearerToken ?? headerToken;
    const baseMetadata = {
        authScheme: token ? "local-ui-session" : "none",
        requiredScope,
        sessionPresent: Boolean(token),
        originBinding: origin ? "not_checked" : "missing",
    };
    if (authorization && !bearerToken && !headerToken) {
        return {
            kind: "error",
            statusCode: 401,
            code: "invalid_authentication",
            message: "The local UI session credential was not accepted.",
            diagnostic: {
                guard: "auth",
                reason: "invalid_authentication",
                status: 401,
                metadata: baseMetadata,
            },
        };
    }
    if (!origin || !token) {
        return {
            kind: "error",
            statusCode: 401,
            code: "unauthorized",
            message: "Missing local UI origin or session token.",
            diagnostic: {
                guard: "auth",
                reason: "missing_authentication",
                status: 401,
                metadata: baseMetadata,
            },
        };
    }
    try {
        return {
            kind: "local-ui-session",
            session: app.localUiCapabilities.authenticateSession({
                sessionToken: token,
                origin,
                requiredScope,
            }),
        };
    }
    catch (error) {
        if (error instanceof LocalUiCapabilityError) {
            const diagnostic = localUiCapabilityDiagnostic(error, baseMetadata);
            return {
                kind: "error",
                statusCode: error.statusCode,
                code: error.code,
                message: error.message,
                diagnostic,
            };
        }
        throw error;
    }
}
export function writeAuthFailure(response, result, context = {}) {
    if (result.kind !== "error")
        return true;
    const diagnostic = result.diagnostic ?? {};
    writeAuthorityDiagnostic(response, {
        ...diagnostic,
        guard: diagnostic.guard ?? diagnosticGuardForAuthFailure(result),
        reason: diagnostic.reason ?? diagnosticReasonForAuthFailure(result),
        status: result.statusCode,
    }, context);
    return false;
}
export function writeAuthorityDiagnostic(response, input, context = {}) {
    const diagnostic = createBridgeAuthorityDiagnostic({
        ...input,
        request: mergeDiagnosticRequest(input.request, context),
        metadata: mergeDiagnosticMetadata(input.metadata, context.metadata),
    });
    writeJson(response, diagnostic.status, diagnostic, {
        surface: "bridge.authority-diagnostic",
        scanOptions: PUBLIC_IDENTIFIER_SCAN_OPTIONS,
    });
}
export function authorizeWorkspace(response, auth, workspaceId) {
    if (auth.kind !== "local-ui-session")
        return true;
    if (auth.session.workspaceId === workspaceId)
        return true;
    writeForbiddenWorkspace(response, {
        metadata: {
            authScheme: "local-ui-session",
            grantedScopes: auth.session.scopes,
            originBinding: "matched",
            workspaceBinding: "mismatched",
            sessionPresent: true,
        },
    });
    return false;
}
export function writeForbiddenWorkspace(response, context = {}) {
    writeAuthorityDiagnostic(response, {
        guard: "workspace",
        reason: "wrong_workspace",
        status: 403,
        metadata: { workspaceBinding: "mismatched" },
    }, context);
}
export function requireLocalUiSessionForBrowserRoute(response, auth, context = {}) {
    if (auth.kind === "local-ui-session")
        return true;
    writeAuthorityDiagnostic(response, {
        guard: "local-ui-session-required",
        reason: "local_ui_session_required",
        status: 403,
        metadata: {
            authScheme: auth.kind === "bridge" ? "bridge-token" : "none",
            bridgeTokenPresent: auth.kind === "bridge",
        },
    }, context);
    return false;
}
export function applyCorsHeaders(request, response, app) {
    const origin = readOrigin(request);
    if (!origin || !app.localUiCapabilities.isKnownOrigin(origin))
        return;
    response.setHeader("access-control-allow-origin", origin);
    response.setHeader("vary", "Origin");
    response.setHeader("access-control-allow-methods", "GET, POST, PUT, DELETE, OPTIONS");
    response.setHeader("access-control-allow-headers", "authorization, content-type, x-patchtrail-bridge-token, x-patchtrail-local-ui-session");
    if (request.headers["access-control-request-private-network"] === "true") {
        response.setHeader("access-control-allow-private-network", "true");
    }
}
function localUiCapabilityDiagnostic(error, baseMetadata) {
    if (error.code === "origin_mismatch") {
        return {
            guard: "origin",
            reason: "wrong_origin",
            status: error.statusCode,
            metadata: { ...baseMetadata, originBinding: "mismatched" },
        };
    }
    if (error.code === "insufficient_scope") {
        return {
            guard: "scope",
            reason: "insufficient_scope",
            status: error.statusCode,
            metadata: { ...baseMetadata, originBinding: "matched" },
        };
    }
    return {
        guard: "auth",
        reason: "invalid_authentication",
        status: error.statusCode,
        metadata: baseMetadata,
    };
}
function diagnosticGuardForAuthFailure(result) {
    if (result.code === "origin_mismatch")
        return "origin";
    if (result.code === "insufficient_scope")
        return "scope";
    if (result.code === "local_ui_session_required" ||
        result.code === "bridge_token_not_allowed") {
        return "local-ui-session-required";
    }
    return "auth";
}
function diagnosticReasonForAuthFailure(result) {
    if (result.code === "origin_mismatch")
        return "wrong_origin";
    if (result.code === "insufficient_scope")
        return "insufficient_scope";
    if (result.code === "local_ui_session_required" ||
        result.code === "bridge_token_not_allowed") {
        return "local_ui_session_required";
    }
    if (result.code === "invalid_authentication" ||
        result.code === "invalid_capability" ||
        result.code === "session_expired") {
        return "invalid_authentication";
    }
    return result.statusCode === 401 ? "missing_authentication" : "unknown";
}
function mergeDiagnosticRequest(input, context) {
    const request = {};
    if (context.request?.method)
        request.method = context.request.method;
    if (context.routeId)
        request.routeId = context.routeId;
    if (isRecord(input))
        Object.assign(request, input);
    return request;
}
function mergeDiagnosticMetadata(input, contextMetadata) {
    const metadata = {};
    if (contextMetadata)
        Object.assign(metadata, contextMetadata);
    if (isRecord(input))
        Object.assign(metadata, input);
    return metadata;
}
function hasValidBridgeToken(request, token) {
    const header = request.headers["x-patchtrail-bridge-token"];
    if (typeof header === "string" && constantTimeEquals(header, token)) {
        return true;
    }
    const authorization = request.headers.authorization;
    if (typeof authorization !== "string")
        return false;
    return constantTimeEquals(authorization, `Bearer ${token}`);
}
/**
 * Constant-time string comparison for secret-bearing credentials. Both sides are
 * SHA-256 hashed to fixed-length 32-byte digests before timingSafeEqual, so the
 * comparison never branches on content and never leaks the candidate length via
 * an early length check (timingSafeEqual requires equal-length buffers).
 */
function constantTimeEquals(candidate, expected) {
    const candidateDigest = createHash("sha256")
        .update(candidate, "utf8")
        .digest();
    const expectedDigest = createHash("sha256").update(expected, "utf8").digest();
    return timingSafeEqual(candidateDigest, expectedDigest);
}
function hasBridgeTokenHeader(request) {
    return typeof request.headers["x-patchtrail-bridge-token"] === "string";
}
function hasAnyAuthCredential(request) {
    return Boolean(request.headers.authorization ??
        readHeaderString(request, "x-patchtrail-bridge-token") ??
        readHeaderString(request, "x-patchtrail-local-ui-session"));
}
function hasLocalUiSessionCredential(request) {
    return Boolean(readBearerToken(request) ??
        readHeaderString(request, "x-patchtrail-local-ui-session"));
}
function authSchemeForPresentedCredential(request) {
    if (hasBridgeTokenHeader(request))
        return "bridge-token";
    if (readBearerToken(request) ??
        readHeaderString(request, "x-patchtrail-local-ui-session")) {
        return "local-ui-session";
    }
    return "none";
}
