/**
 * Edge middleware: security headers, the authentication gate and CSRF enforcement.
 *
 * This runs before every request, so it is the right place for controls that must not
 * depend on an individual route remembering to apply them. It deliberately does *not*
 * make authorisation decisions — it only establishes that a caller has a structurally
 * valid session. Permission checks need the database and happen in the route handlers,
 * where they can be audited.
 */

import { NextResponse, type NextRequest } from 'next/server'
import { jwtVerify } from 'jose'

const SESSION_COOKIE = 'nabdh_session'
const CSRF_COOKIE = 'nabdh_csrf'
const CSRF_HEADER = 'x-csrf-token'

/** Routes reachable without a session. */
const PUBLIC_PATHS = ['/', '/login', '/why-nabdh']
const PUBLIC_API_PREFIXES = ['/api/auth/login', '/api/health']

const SAFE_METHODS = new Set(['GET', 'HEAD', 'OPTIONS'])

function isPublicPath(pathname: string): boolean {
  if (PUBLIC_PATHS.includes(pathname)) return true
  return PUBLIC_API_PREFIXES.some((prefix) => pathname.startsWith(prefix))
}

async function hasValidSession(request: NextRequest): Promise<boolean> {
  const token = request.cookies.get(SESSION_COOKIE)?.value
  if (!token) return false
  const secret = process.env.AUTH_SECRET
  if (!secret) return false
  try {
    await jwtVerify(token, new TextEncoder().encode(secret), {
      issuer: 'nabdh',
      audience: 'nabdh-console',
      algorithms: ['HS256'],
    })
    return true
  } catch {
    return false
  }
}

/** Constant-time comparison so token checking cannot be probed by timing. */
function timingSafeEqual(a: string, b: string): boolean {
  if (a.length !== b.length) return false
  let diff = 0
  for (let i = 0; i < a.length; i += 1) diff |= a.charCodeAt(i) ^ b.charCodeAt(i)
  return diff === 0
}

function buildCsp(nonce: string, isDev: boolean): string {
  // React's development runtime needs eval and inline styles; production does not, so the
  // relaxation is scoped to development rather than shipped.
  const scriptSrc = isDev
    ? `'self' 'unsafe-inline' 'unsafe-eval'`
    : `'self' 'nonce-${nonce}' 'strict-dynamic'`

  return [
    `default-src 'self'`,
    `script-src ${scriptSrc}`,
    // Tailwind and Recharts both set element styles at runtime.
    `style-src 'self' 'unsafe-inline'`,
    `img-src 'self' data: blob:`,
    `font-src 'self' data:`,
    // The app is fully self-contained: no third-party origins are contacted.
    `connect-src 'self'`,
    `frame-ancestors 'none'`,
    `form-action 'self'`,
    `base-uri 'self'`,
    `object-src 'none'`,
    isDev ? '' : 'upgrade-insecure-requests',
  ]
    .filter(Boolean)
    .join('; ')
}

function applySecurityHeaders(response: NextResponse, csp: string, isDev: boolean) {
  response.headers.set('Content-Security-Policy', csp)
  response.headers.set('X-Content-Type-Options', 'nosniff')
  response.headers.set('X-Frame-Options', 'DENY')
  response.headers.set('Referrer-Policy', 'strict-origin-when-cross-origin')
  response.headers.set(
    'Permissions-Policy',
    'camera=(), microphone=(), geolocation=(), payment=(), usb=(), interest-cohort=()',
  )
  response.headers.set('X-DNS-Prefetch-Control', 'off')
  if (!isDev) {
    response.headers.set('Strict-Transport-Security', 'max-age=63072000; includeSubDomains; preload')
  }
  return response
}

export async function middleware(request: NextRequest) {
  const { pathname } = request.nextUrl
  const isDev = process.env.NODE_ENV !== 'production'

  const nonceBytes = new Uint8Array(16)
  crypto.getRandomValues(nonceBytes)
  const nonce = btoa(String.fromCharCode(...nonceBytes))

  const csp = buildCsp(nonce, isDev)

  const requestHeaders = new Headers(request.headers)
  requestHeaders.set('x-nonce', nonce)
  // Next.js reads the policy from the *request* headers to discover the nonce and stamp
  // it onto the framework's own script tags. Without this, a nonce-based production CSP
  // blocks the app's own bootstrap and the page renders blank.
  requestHeaders.set('Content-Security-Policy', csp)

  const isApi = pathname.startsWith('/api')

  // ── CSRF: double-submit token on every state-changing API request ──────────
  // The session cookie is SameSite=Lax, which already blocks cross-site form posts, but
  // a token an attacker cannot read gives a second, independent barrier.
  if (isApi && !SAFE_METHODS.has(request.method) && !pathname.startsWith('/api/auth/login')) {
    const cookieToken = request.cookies.get(CSRF_COOKIE)?.value
    const headerToken = request.headers.get(CSRF_HEADER)
    if (!cookieToken || !headerToken || !timingSafeEqual(cookieToken, headerToken)) {
      return applySecurityHeaders(
        NextResponse.json(
          {
            ok: false,
            error: {
              code: 'CSRF_INVALID',
              message: 'Your session security token has expired. Refresh the page and try again.',
              messageAr: 'انتهت صلاحية رمز الأمان. أعد تحميل الصفحة وحاول مرة أخرى.',
            },
          },
          { status: 403 },
        ),
        csp,
        isDev,
      )
    }
  }

  // ── Authentication gate ───────────────────────────────────────────────────
  if (!isPublicPath(pathname)) {
    const authenticated = await hasValidSession(request)
    if (!authenticated) {
      if (isApi) {
        return applySecurityHeaders(
          NextResponse.json(
            {
              ok: false,
              error: {
                code: 'UNAUTHENTICATED',
                message: 'You need to sign in to continue.',
                messageAr: 'يجب تسجيل الدخول للمتابعة.',
              },
            },
            { status: 401 },
          ),
          csp,
          isDev,
        )
      }
      const login = request.nextUrl.clone()
      login.pathname = '/login'
      login.searchParams.set('next', pathname)
      return applySecurityHeaders(NextResponse.redirect(login), csp, isDev)
    }
  }

  return applySecurityHeaders(
    NextResponse.next({ request: { headers: requestHeaders } }),
    csp,
    isDev,
  )
}

export const config = {
  // Static assets and image optimisation do not need any of the above.
  matcher: ['/((?!_next/static|_next/image|favicon.ico|icon.svg|manifest.webmanifest).*)'],
}
