/**
 * CSRF protection - double-submit cookie pattern.
 *
 * Flow:
 * 1. On session creation, a random 32-byte hex token is minted and stored in
 *    the session row AND returned in a readable (non-HttpOnly) cookie.
 * 2. On every state-changing request, the client must echo the token back in
 *    the `x-csrf-token` header (or `csrf_token` body field).
 * 3. The server compares the header/body value to the session's stored token.
 *
 * We use a constant-time comparison via `crypto.subtle` to prevent timing attacks.
 */

import { base64UrlDecodeBytes, bytesToHex } from '@/lib/encoding.js';
import { captureCaught } from '@/server/observability/capture.server';
import { buildCsrfCookieHeader } from './cookies.js';
import { constantTimeEqual } from '../utils/constant-time.js';

export { buildCsrfCookieHeader };

// ---------------------------------------------------------------------------
// HMAC helpers (used by computeCsrfToken / verifyCsrfToken)
// ---------------------------------------------------------------------------

const ALG = { name: 'HMAC', hash: 'SHA-256' } as const;
const enc = new TextEncoder();

// Module-level key cache — same pattern as tokens.ts to stay within CF Free 10 ms CPU ceiling.
const _csrfKeyCache = new Map<string, CryptoKey>();

async function getCsrfKey(secret: string): Promise<CryptoKey> {
  const cached = _csrfKeyCache.get(secret);
  if (cached) return cached;
  const key = await crypto.subtle.importKey('raw', enc.encode(secret), ALG, false, [
    'sign',
    'verify',
  ]);
  _csrfKeyCache.set(secret, key);
  return key;
}

function bufToBase64Url(buf: ArrayBuffer): string {
  const bytes = new Uint8Array(buf);
  let bin = '';
  for (const b of bytes) bin += String.fromCharCode(b);
  return btoa(bin).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
}

// ---------------------------------------------------------------------------
// HMAC-SHA256 CSRF token (bound to sub + iat)
// ---------------------------------------------------------------------------

/**
 * Compute an HMAC-SHA256 based CSRF token bound to sub + iat.
 * Returns base64url-encoded signature.
 */
export async function computeCsrfToken(secret: string, sub: string, iat: number): Promise<string> {
  const key = await getCsrfKey(secret);
  const message = enc.encode(`${sub}.${iat}`);
  const sig = await crypto.subtle.sign(ALG, key, message);
  return bufToBase64Url(sig);
}

/**
 * Verify a CSRF token produced by computeCsrfToken.
 * Constant-time compare via crypto.subtle.verify.
 */
export async function verifyCsrfToken(
  secret: string,
  token: string,
  sub: string,
  iat: number,
): Promise<boolean> {
  try {
    const expected = await computeCsrfToken(secret, sub, iat);
    if (token.length !== expected.length) return false;

    // Decode both to bytes for constant-time compare
    const key = await getCsrfKey(secret);
    const message = enc.encode(`${sub}.${iat}`);

    // Decode token base64url → bytes
    const sigBytes = base64UrlDecodeBytes(token);

    return crypto.subtle.verify(ALG, key, sigBytes.buffer as ArrayBuffer, message);
  } catch (err) {
    captureCaught(err, { scope: 'server.auth.mh-cookie.verifyCsrfToken', severity: 'info' });
    return false;
  }
}

// ---------------------------------------------------------------------------
// Token issuance
// ---------------------------------------------------------------------------

/**
 * Generates a random 32-byte hex string suitable for use as a CSRF token.
 */
export function issueCsrfToken(): string {
  const bytes = new Uint8Array(32);
  crypto.getRandomValues(bytes);
  return bytesToHex(bytes);
}

// ---------------------------------------------------------------------------
// Verification
// ---------------------------------------------------------------------------

export interface CsrfVerifyInput {
  /** The CSRF token stored in the session row. */
  sessionCsrfToken: string;
  /** Value from the `x-csrf-token` header (preferred). */
  headerToken?: string | null;
  /** Value from a `csrf_token` body field (fallback). */
  bodyToken?: string | null;
}

/**
 * Verifies the CSRF token from a request against the session's stored token.
 *
 * Returns `true` if the tokens match (request is safe), `false` otherwise.
 *
 * @param input - Tokens to compare.
 */
export async function verifyCsrf(input: CsrfVerifyInput): Promise<boolean> {
  const incoming = input.headerToken ?? input.bodyToken;
  if (!incoming) return false;
  if (!input.sessionCsrfToken) return false;
  // constantTimeEqual: synchronous XOR byte-loop — no SubtleCrypto needed here.
  // The CSRF tokens being compared are already HMAC-derived hex/base64url strings;
  // an XOR-OR loop over their char codes is constant-time and sufficient.
  return constantTimeEqual(incoming, input.sessionCsrfToken);
}
