/**
 * Content-Security-Policy nonce wiring (PR-18, May 2026) + audit3-followup
 * (2026-05-29) report endpoint + parallel Report-Only header.
 *
 * Before PR-18 the CSP header was emitted statically from next.config.ts
 * with `script-src 'self' 'unsafe-inline' 'unsafe-eval'`. That makes
 * the policy effectively no-op against XSS because any injected
 * <script>alert(1)</script> would be accepted — defeating the whole
 * point of a CSP.
 *
 * The fix uses Next.js's auto-nonce-injection: when the response CSP
 * header contains a `nonce-<value>` token and the request carries
 * `x-nonce: <value>`, Next attaches `nonce="<value>"` to every framework
 * script tag it emits. Combined with `'strict-dynamic'`, this means
 * only scripts the framework chose to render execute; any injected
 * inline script (the actual XSS payload) is blocked.
 *
 * audit3-followup (2026-05-29):
 *   - report-uri directive points violations at /api/csp-report.
 *   - A parallel Content-Security-Policy-Report-Only header carries a
 *     tighter experimental policy (style-src without 'unsafe-inline').
 *     The browser doesn't enforce it but DOES report violations, giving
 *     us telemetry on whether dropping 'unsafe-inline' breaks anything
 *     before we commit. Both headers point at the same /api/csp-report
 *     endpoint; the disposition field distinguishes enforce vs report.
 *
 * Trade-offs:
 *   - script-src: drops 'unsafe-inline' + 'unsafe-eval' in prod. Dev
 *     keeps 'unsafe-eval' because React's error-overlay needs it.
 *   - style-src: enforced policy still keeps 'unsafe-inline' to avoid
 *     breaking styled-jsx + runtime CSS-in-JS. The Report-Only header
 *     drops it so we can quantify what breaks before flipping.
 *   - 'strict-dynamic' is on for script-src — modern browsers ignore
 *     the 'self' allowlist for scripts once a nonce'd script loads
 *     others, which is exactly what Next does for its chunked bundle.
 */
import type { NextResponse } from 'next/server';

const REPORT_URI = '/api/csp-report';

/**
 * Generate a fresh per-request nonce. Base64-encoded UUID v4 — 16 bytes
 * of entropy is well above the OWASP minimum (128 bits) and Base64
 * keeps the value short enough to repeat in every script tag without
 * inflating the response HTML.
 */
export function generateCspNonce(): string {
    return Buffer.from(crypto.randomUUID()).toString('base64');
}

/**
 * Build the enforced CSP header. Pass isDev=true to keep 'unsafe-eval'
 * for React's dev tooling.
 */
export function buildCspHeader(nonce: string, isDev: boolean): string {
    const scriptSrc = isDev
        ? `'self' 'nonce-${nonce}' 'strict-dynamic' 'unsafe-eval'`
        : `'self' 'nonce-${nonce}' 'strict-dynamic'`;

    return [
        "default-src 'self'",
        `script-src ${scriptSrc}`,
        // style-src keeps 'unsafe-inline' here so styled-jsx + runtime
        // CSS-in-JS keep working. The Report-Only header below removes
        // it as an experiment; once we have telemetry showing zero
        // violations from real users, we tighten this enforced policy.
        `style-src 'self' 'unsafe-inline' 'nonce-${nonce}'`,
        "img-src 'self' data: blob: https:",
        "font-src 'self' data:",
        "connect-src 'self' https:",
        "frame-ancestors 'self'",
        "base-uri 'self'",
        "form-action 'self'",
        `report-uri ${REPORT_URI}`,
    ].join('; ');
}

/**
 * audit3-followup (2026-05-29): build the experimental Report-Only
 * header. Same as the enforced policy but with `'unsafe-inline'`
 * removed from style-src. Browsers don't BLOCK on this header — they
 * post violations to /api/csp-report with disposition='report' so we
 * get telemetry on what would break if we flipped the enforced policy.
 *
 * Toggleable via env (default on). Set CSP_REPORT_ONLY=false to disable
 * the second header — useful when an operator wants to silence early-
 * warning telemetry temporarily.
 */
export function buildCspReportOnlyHeader(nonce: string, isDev: boolean): string {
    const scriptSrc = isDev
        ? `'self' 'nonce-${nonce}' 'strict-dynamic' 'unsafe-eval'`
        : `'self' 'nonce-${nonce}' 'strict-dynamic'`;

    return [
        "default-src 'self'",
        `script-src ${scriptSrc}`,
        // Tighter style-src — no 'unsafe-inline'. The Report-Only
        // disposition means the browser DOES NOT block, only reports.
        `style-src 'self' 'nonce-${nonce}'`,
        "img-src 'self' data: blob: https:",
        "font-src 'self' data:",
        "connect-src 'self' https:",
        "frame-ancestors 'self'",
        "base-uri 'self'",
        "form-action 'self'",
        `report-uri ${REPORT_URI}`,
    ].join('; ');
}

/**
 * Attach the CSP headers (enforced + optional Report-Only) to a response.
 * Mutates and returns the same response for ergonomic chaining.
 */
export function attachCspHeader(response: NextResponse, nonce: string, isDev: boolean): NextResponse {
    response.headers.set('Content-Security-Policy', buildCspHeader(nonce, isDev));
    if (process.env.CSP_REPORT_ONLY !== 'false') {
        response.headers.set('Content-Security-Policy-Report-Only', buildCspReportOnlyHeader(nonce, isDev));
    }
    return response;
}
