// CSRF — Double-Submit-Cookie pattern for state-changing API routes.
//
// Server-side helpers only. The client reads the cookie value and echoes
// it back in the `x-csrf-token` request header on POST/PATCH/PUT/DELETE.
//
// Activation: set `CSRF_ENABLED=true` in the environment. Default-off so
// the layer can be rolled out behind a flag and disabled instantly if a
// regression is observed.
//
// Why double-submit:
//   - SameSite=Lax cookies block most cross-site POSTs already, but
//     enterprise IT audits expect an explicit CSRF token check.
//   - Double-submit-cookie does not require server state (no session
//     storage), so it works on serverless functions without coordination.
//   - The header is unreadable to a cross-origin attacker due to the
//     same-origin policy of XHR/fetch.
//
// Why NOT cryptographic-bound CSRF tokens:
//   - That requires a shared secret and HMAC verification per request;
//     overkill until an audit explicitly asks for it.
//
// Bypass: the GET-style routes (`GET`, `HEAD`, `OPTIONS`) are not checked.
// API routes that mutate state via GET are themselves a security smell
// and should be fixed at the route level.

import type { NextRequest } from 'next/server'
import { NextResponse } from 'next/server'

const CSRF_COOKIE_NAME = 'kadi-csrf'
const CSRF_HEADER_NAME = 'x-csrf-token'
const TOKEN_BYTE_LENGTH = 32

export const csrfConfig = {
  cookieName: CSRF_COOKIE_NAME,
  headerName: CSRF_HEADER_NAME,
}

function isEnabled(): boolean {
  return process.env.CSRF_ENABLED === 'true'
}

function generateCsrfToken(): string {
  const buf = new Uint8Array(TOKEN_BYTE_LENGTH)
  crypto.getRandomValues(buf)
  // base64url-encode (no padding) for cookie-safety
  return Buffer.from(buf).toString('base64url')
}

// Used by middleware (proxy.ts) to mint a CSRF cookie if one is missing.
// Idempotent: if the cookie already exists, the existing token is preserved.
export function ensureCsrfCookie(
  request: NextRequest,
  response: NextResponse,
): void {
  if (!isEnabled()) return
  const existing = request.cookies.get(CSRF_COOKIE_NAME)?.value
  if (existing && existing.length >= 8) return
  const token = generateCsrfToken()
  response.cookies.set({
    name: CSRF_COOKIE_NAME,
    value: token,
    httpOnly: false, // client JS must read it to echo into the header
    secure: process.env.NEXT_PUBLIC_APP_ENV !== 'development',
    sameSite: 'lax',
    path: '/',
  })
}

// Route-handler guard. Throws a Response on failure (mirrors the
// requireActiveTenant pattern). Safe to call before any auth check.
export function requireCsrf(request: NextRequest | Request): void {
  if (!isEnabled()) return
  const method = request.method.toUpperCase()
  if (method === 'GET' || method === 'HEAD' || method === 'OPTIONS') return

  const headerToken = request.headers.get(CSRF_HEADER_NAME)?.trim() ?? ''

  // Read cookie. NextRequest exposes .cookies; plain Request does not.
  let cookieToken = ''
  if ('cookies' in request && typeof (request as NextRequest).cookies?.get === 'function') {
    cookieToken = (request as NextRequest).cookies.get(CSRF_COOKIE_NAME)?.value ?? ''
  } else {
    const rawCookie = request.headers.get('cookie') ?? ''
    const match = rawCookie.match(new RegExp(`(?:^|;\\s*)${CSRF_COOKIE_NAME}=([^;]+)`))
    cookieToken = match ? decodeURIComponent(match[1]) : ''
  }

  if (!cookieToken || !headerToken || cookieToken !== headerToken) {
    throw NextResponse.json(
      { error: 'CSRF token missing or mismatched' },
      { status: 403 },
    )
  }
}

// Try-wrapper that returns the early-response if a CSRF check failed,
// or null if the request passed (or CSRF is disabled).
export function checkCsrf(
  request: NextRequest | Request,
): NextResponse | null {
  try {
    requireCsrf(request)
    return null
  } catch (err) {
    if (err instanceof Response) return err as NextResponse
    throw err
  }
}
