/**
 * Isomorphic E2E bypass proof (Web Crypto only — runs in worker, node test process, client islands).
 *
 * Replaces raw `x-e2e-secret` transmission. The client/test SIGNS a time-bound, body-bound HMAC; the
 * server VERIFIES it. The secret never travels on the wire (immune to request-log/proxy capture); a
 * captured request expires after SKEW and is locked to its exact body (no body-swap replay).
 *
 * NOT leak-protection: anyone with E2E_SECRET can sign. Delivers capture-replay + log-disclosure
 * protection only. Gate still requires explicit E2E route enablement, a non-production
 * environment, and E2E_SECRET.
 */
import { BodyTooLargeError, readBodyWithinLimit } from './read-limited-body.js';
import { bytesToHex } from '@/lib/encoding.js';

const SKEW_SECONDS = 60;
const DEFAULT_MAX_BODY_BYTES = 1_048_576;
const enc = new TextEncoder();

export async function sha256Hex(body: string): Promise<string> {
  const digest = await crypto.subtle.digest('SHA-256', enc.encode(body));
  return bytesToHex(digest);
}

async function hmacHex(secret: string, message: string): Promise<string> {
  const key = await crypto.subtle.importKey(
    'raw',
    enc.encode(secret),
    { name: 'HMAC', hash: 'SHA-256' },
    false,
    ['sign'],
  );
  const sig = await crypto.subtle.sign('HMAC', key, enc.encode(message));
  return bytesToHex(sig);
}

/** Constant-time hex-string compare (fixed 64-char hex; still compare without early-out). */
function timingSafeHexEqual(a: string, b: string): boolean {
  if (a.length !== b.length) return false;
  let diff = 0;
  for (let i = 0; i < a.length; i++) diff |= a.charCodeAt(i) ^ b.charCodeAt(i);
  return diff === 0;
}

/** Sign side. bodyHash = sha256Hex(rawBody); '' for bodyless GET. */
export async function signE2eProof(
  secret: string,
  method: string,
  pathname: string,
  ts: number,
  bodyHash: string,
  audience: string,
): Promise<string> {
  return hmacHex(secret, `${audience}.${ts}.${method.toUpperCase()}.${pathname}.${bodyHash}`);
}

/** Client/test helper: build x-e2e-ts + x-e2e-sig headers for a fetch call. */
export async function buildE2eProofHeaders(
  secret: string,
  method: string,
  url: string,
  bodyStr: string,
): Promise<{ 'x-e2e-ts': string; 'x-e2e-sig': string }> {
  const ts = Math.floor(Date.now() / 1000);
  const origin = typeof location !== 'undefined' ? location.origin : 'https://localhost';
  const path = new URL(url, origin).pathname;
  const audience = new URL(url, origin).origin;
  const sig = await signE2eProof(secret, method, path, ts, await sha256Hex(bodyStr), audience);
  return { 'x-e2e-ts': String(ts), 'x-e2e-sig': sig };
}

/**
 * Verify side. Reads x-e2e-ts + x-e2e-sig; recomputes over the request's OWN body. Never throws.
 * Reads the body via req.clone() so the route handler can still consume req.
 */
export async function verifyE2eProof(
  req: Request,
  env: {
    ENVIRONMENT?: string;
    E2E_SECRET?: string;
    E2E_ROUTES_ENABLED?: string;
    PAYMENT_PROVIDER?: string;
  },
  rawBody?: string,
  policy: { requireMockPaymentProvider?: boolean; maxBodyBytes?: number } = {},
): Promise<boolean> {
  const secret = env.E2E_SECRET;
  if (
    !secret ||
    env.E2E_ROUTES_ENABLED !== '1' ||
    !['development', 'preview', 'test'].includes(env.ENVIRONMENT ?? '')
  ) {
    return false;
  }
  if (policy.requireMockPaymentProvider && env.PAYMENT_PROVIDER !== 'mock') return false;

  const tsRaw = req.headers.get('x-e2e-ts');
  const sig = req.headers.get('x-e2e-sig');
  if (!tsRaw || !sig) return false;

  const ts = Number(tsRaw);
  if (!Number.isInteger(ts)) return false;
  const now = Math.floor(Date.now() / 1000);
  if (Math.abs(now - ts) > SKEW_SECONDS) return false;

  const url = new URL(req.url);
  const method = req.method.toUpperCase();
  let body: string;
  try {
    body =
      rawBody !== undefined
        ? rawBody
        : method === 'GET' || method === 'HEAD'
          ? ''
          : await readBodyWithinLimit(req.clone(), policy.maxBodyBytes ?? DEFAULT_MAX_BODY_BYTES);
  } catch (error) {
    if (error instanceof BodyTooLargeError) return false;
    return false;
  }
  if (enc.encode(body).byteLength > (policy.maxBodyBytes ?? DEFAULT_MAX_BODY_BYTES)) return false;
  const expected = await signE2eProof(
    secret,
    method,
    url.pathname,
    ts,
    await sha256Hex(body),
    url.origin,
  );
  return timingSafeHexEqual(sig, expected);
}
