// Content Security Policy helpers.
//
// Authoritative: ADR 018, docs/foundation/csp-strategy.md.
//
// Design principles:
// 1. The policy lives in one place (this module). proxy.ts consumes it
//    per-request so a fresh nonce is generated on every HTML response.
// 2. `script-src` uses a per-request nonce + `'strict-dynamic'`. In
//    production that removes `'unsafe-inline'` and `'unsafe-eval'` from
//    scripts entirely.
// 3. `style-src` keeps `'unsafe-inline'` because the app uses inline
//    `style={{...}}` attributes (e.g. tenant branding CSS variables in
//    `app/layout.tsx`). CSP nonces do not cover attribute-style
//    inline styles — only `<style>` tags. See `csp-strategy.md` for the
//    documented residual and the future refactor that would close it.
// 4. Development mode (`NODE_ENV !== 'production'`) keeps `'unsafe-eval'`
//    on script-src because Next.js HMR + React DevTools use `eval` for
//    module evaluation. This concession is *dev only*.

export type CspMode = 'enforce' | 'report-only'

export const CSP_HEADER_ENFORCE = 'Content-Security-Policy'
export const CSP_HEADER_REPORT_ONLY = 'Content-Security-Policy-Report-Only'

// Same-origin endpoint that accepts both the legacy `application/csp-report`
// POST body and the Reporting API `application/reports+json` array. The path
// is wired into `app/api/csp-report/route.ts`. Kept as a constant so the
// `check-csp.mjs` validator can assert the directive points at a real route.
export const CSP_REPORT_ENDPOINT = '/api/csp-report'

// Reporting API "group name" used by the `report-to` directive and the
// `Reporting-Endpoints` response header. Chrome + Edge prefer `report-to`;
// Firefox/Safari still honor legacy `report-uri`. We emit both.
export const CSP_REPORT_GROUP = 'csp-endpoint'

export function getCspMode(): CspMode {
  return process.env.CSP_ENFORCE === '1' ? 'enforce' : 'report-only'
}

export function cspHeaderName(
  mode: CspMode,
): typeof CSP_HEADER_ENFORCE | typeof CSP_HEADER_REPORT_ONLY {
  return mode === 'enforce' ? CSP_HEADER_ENFORCE : CSP_HEADER_REPORT_ONLY
}

// 16 random bytes → base64 (~24 chars). Meets CSP Level 3 minimum entropy.
// Uses the Web Crypto API available in Edge runtime + Node 18+.
export function generateNonce(): string {
  const bytes = new Uint8Array(16)
  crypto.getRandomValues(bytes)
  let binary = ''
  for (const byte of bytes) binary += String.fromCharCode(byte)
  return btoa(binary)
}

export interface BuildCspOptions {
  nonce: string
  isDev: boolean
}

export function buildCspDirectives({ nonce, isDev }: BuildCspOptions): string {
  // `'strict-dynamic'` means browsers trust any script loaded by a nonced
  // script, and ignore host-source allowlists for script-src. This is the
  // CSP Level 3 pattern recommended by Google's csp-evaluator.
  const scriptSrc = [
    "script-src 'self'",
    `'nonce-${nonce}'`,
    "'strict-dynamic'",
    // Dev-only: Next.js dev server + React refresh require eval for HMR.
    // Production bundles do not — the concession stays out of the prod policy.
    isDev ? "'unsafe-eval'" : null,
  ]
    .filter((part): part is string => part !== null)
    .join(' ')

  // Residual: JSX inline `style={{...}}` emits `style` attributes, which
  // CSP treats as inline styles. Nonces only cover `<style>` tags.
  // Closing this requires moving all inline styles to classes or to a
  // single nonced `<style>` tag in the <head>. Tracked separately.
  const styleSrc = "style-src 'self' 'unsafe-inline'"

  return [
    "default-src 'self'",
    "base-uri 'self'",
    "object-src 'none'",
    "frame-ancestors 'none'",
    "form-action 'self'",
    "img-src 'self' data: blob: https://*.supabase.co https://*.supabase.in",
    "font-src 'self' data:",
    styleSrc,
    scriptSrc,
    "connect-src 'self' https://*.supabase.co wss://*.supabase.co https://*.vercel-insights.com",
    "worker-src 'self' blob:",
    "manifest-src 'self'",
    // Legacy directive for Safari + older Firefox. Browsers that speak the
    // Reporting API prefer `report-to`; they silently ignore `report-uri` but
    // its presence does not weaken the policy.
    `report-uri ${CSP_REPORT_ENDPOINT}`,
    // Modern directive — resolves to the group declared by the
    // `Reporting-Endpoints` response header (set in proxy.ts).
    `report-to ${CSP_REPORT_GROUP}`,
  ].join('; ')
}

/**
 * Builds the `Reporting-Endpoints` header value that declares the
 * endpoint group referenced by `report-to`. Same-origin, so no CORS
 * concerns. Pair with `buildCspHeader` — both are set by `proxy.ts`.
 */
export function buildReportingEndpointsHeader(): {
  name: 'Reporting-Endpoints'
  value: string
} {
  return {
    name: 'Reporting-Endpoints',
    value: `${CSP_REPORT_GROUP}="${CSP_REPORT_ENDPOINT}"`,
  }
}

export interface BuildCspHeaderOptions extends BuildCspOptions {
  mode?: CspMode
}

export function buildCspHeader(options: BuildCspHeaderOptions): {
  name: string
  value: string
} {
  const mode = options.mode ?? getCspMode()
  return {
    name: cspHeaderName(mode),
    value: buildCspDirectives(options),
  }
}

// Exported so the validation script can introspect the static policy
// without running a dev server.
export function describeStaticPolicy(): {
  mode: CspMode
  isDevExample: string
  isProdExample: string
} {
  const placeholder = 'EXAMPLE_NONCE'
  return {
    mode: getCspMode(),
    isDevExample: buildCspDirectives({ nonce: placeholder, isDev: true }),
    isProdExample: buildCspDirectives({ nonce: placeholder, isDev: false }),
  }
}
