// CSP violation report collector.
//
// Accepts POSTs from both payload shapes:
//   - legacy   `application/csp-report`          (Safari, older Firefox)
//   - modern   `application/reports+json`        (Chrome, Edge)
//
// Behavior:
//   - normalize via `@/lib/security` (pure, no side effects)
//   - redact + clip oversized strings in the normalizer
//   - emit one structured `csp.violation.*` log per violation record
//   - low-signal reports (browser extensions, `about` blank) drop to `debug`
//   - bad bodies return 204 and log at `debug` — do not create noise
//
// Hard limits (belt + suspenders even though the normalizer clips strings):
//   - reject any body over 64 KB outright
//   - cap the number of records per request at 50
//
// This endpoint is intentionally unauthenticated: CSP violation reports are
// sent by the browser before any user session exists, and authenticating
// them would discard precisely the reports we need to see. The endpoint is
// server-only, accepts POST only, and never reflects the payload back to
// the caller.

import { NextResponse } from 'next/server'
import { logger } from '@/lib/logger'
import {
  isLowSignalReport,
  normalizeCspReportBody,
  type NormalizedCspReport,
} from '@/lib/security'

const MAX_BODY_BYTES = 64 * 1024
const MAX_RECORDS_PER_REQUEST = 50

function parseBodyOrNull(raw: string): unknown {
  try {
    return JSON.parse(raw)
  } catch {
    return null
  }
}

function logReport(report: NormalizedCspReport, requestUserAgent: string | null) {
  const context = {
    directive: report.effectiveDirective,
    blocked_uri: report.blockedUri,
    document_uri: report.documentUri,
    source_file: report.sourceFile,
    line: report.lineNumber,
    column: report.columnNumber,
    disposition: report.disposition,
    status_code: report.statusCode,
    referrer: report.referrer,
    payload_shape: report.payloadShape,
    user_agent: requestUserAgent,
  }

  if (isLowSignalReport(report)) {
    logger.debug('csp.violation.low_signal', context)
    return
  }

  if (report.disposition === 'enforce') {
    // Enforcement blocks are real — page actually broke for the user.
    logger.error('csp.violation.enforced', undefined, context)
  } else {
    // Report-only — the policy *would* have blocked. Surface at warn so
    // the dev review step in the rollout playbook picks it up.
    logger.warn('csp.violation.report_only', context)
  }
}

export async function POST(request: Request) {
  const contentLengthHeader = request.headers.get('content-length')
  const contentLength = contentLengthHeader ? Number(contentLengthHeader) : null
  if (contentLength !== null && Number.isFinite(contentLength) && contentLength > MAX_BODY_BYTES) {
    logger.debug('csp.violation.rejected_oversize', { bytes: contentLength })
    return new NextResponse(null, { status: 413 })
  }

  let text: string
  try {
    text = await request.text()
  } catch {
    return new NextResponse(null, { status: 204 })
  }

  if (text.length === 0) {
    return new NextResponse(null, { status: 204 })
  }
  if (text.length > MAX_BODY_BYTES) {
    logger.debug('csp.violation.rejected_oversize', { bytes: text.length })
    return new NextResponse(null, { status: 413 })
  }

  const parsed = parseBodyOrNull(text)
  if (parsed === null) {
    logger.debug('csp.violation.rejected_invalid_json', { bytes: text.length })
    return new NextResponse(null, { status: 204 })
  }

  const reports = normalizeCspReportBody(parsed)
  if (reports.length === 0) {
    logger.debug('csp.violation.empty_or_unrecognized', { bytes: text.length })
    return new NextResponse(null, { status: 204 })
  }

  const userAgent = request.headers.get('user-agent')
  const effective = reports.slice(0, MAX_RECORDS_PER_REQUEST)
  for (const report of effective) {
    logReport(report, userAgent)
  }
  if (reports.length > MAX_RECORDS_PER_REQUEST) {
    logger.debug('csp.violation.batch_clipped', {
      received: reports.length,
      processed: effective.length,
    })
  }

  return new NextResponse(null, { status: 204 })
}
