/**
 * cookies.ts — consolidated cookie helpers.
 *
 * Consolidates all Set-Cookie builders, parsers, and helpers that were
 * previously spread across mh-cookie.ts, vendor-reg-cookie.ts, session.ts,
 * and csrf.ts.
 *
 * Exports (by origin):
 *   mh-cookie:        MhPayload, initialsOf, buildMh, parseMh,
 *                     setMhCookie, clearMhCookie, computeCsrfToken, verifyCsrfToken
 *   vendor-reg-cookie: VendorRegPayload, signVendorRegCookie, verifyVendorRegCookie
 *   session:          isHttpsRequest, buildAccessCookie, buildRefreshCookie,
 *                     clearAccessCookie, clearRefreshCookie
 *   csrf:             buildCsrfCookieHeader
 */

import { base64UrlDecodeBytes, base64UrlDecodeStr } from '@/lib/encoding.js';
import { initialsOf } from '@/lib/string.js';
import { captureCaught } from '@/server/observability/capture.server';
import { SESSION_TTL_MS } from './session.js';

export { initialsOf };

// ─── mh-cookie ───────────────────────────────────────────────────────────────

/**
 * mh-cookie — pre-hydration personalization hint cookie.
 *
 * The `mh` cookie is a base64url-encoded JSON blob written on the server
 * after authentication and read by client-side JS before React hydrates,
 * enabling instant avatar/initials/admin-badge render without a round-trip.
 *
 * NOT HttpOnly — client JS must read it.
 * NOT a security token — treat as untrusted display hint only.
 */

// ─── Types ───────────────────────────────────────────────────────────────────

export interface MhPayload {
  /** Display name (max 24 chars). */
  n: string;
  /** Initials derived from display name. */
  i: string;
  /** isAdmin flag: 1 = admin, 0 = regular. */
  a: 0 | 1;
  /** isVendor flag: 1 = vendor, 0 = regular. */
  v: 0 | 1;
  /** isAffiliate flag: 1 = active affiliate, absent = regular. */
  af?: 0 | 1;
  /** mhVersion counter — cache invalidation key. */
  c: number;
  /** CSRF token string. */
  t: string;
  /** Expiry as Unix seconds. */
  e: number;
  /** Last 3 digits of phone — display hint only, NOT a security token. */
  ph?: string;
}

// ─── Constants ───────────────────────────────────────────────────────────────

const COOKIE_NAME = 'mh';
/** 30 days in seconds */
const MAX_AGE_SECS = 30 * 24 * 60 * 60; // 2592000
/** Payload expiry — matches cookie Max-Age so mh stays valid for cookie lifetime */
const PAYLOAD_TTL_SECS = MAX_AGE_SECS;

// ─── Helpers ─────────────────────────────────────────────────────────────────

function base64UrlToString(s: string): string {
  return base64UrlDecodeStr(s);
}

// ─── buildMh / parseMh ───────────────────────────────────────────────────────

/**
 * Build an mh cookie value from a user object.
 *
 * @param user          Object with displayName, isAdmin, isVendor.
 * @param mhVersion     Cache-invalidation version counter (users.mhVersion).
 * @param csrfToken     CSRF token string to embed.
 */
export function buildMh(
  user: {
    displayName: string | null;
    isAdmin: boolean;
    isVendor: boolean;
    isAffiliate: boolean;
    phoneHint?: string;
  },
  mhVersion: number,
  csrfToken: string,
): string {
  const name = (user.displayName ?? '').slice(0, 24);
  const payload: MhPayload = {
    n: name,
    i: initialsOf(name),
    a: user.isAdmin ? 1 : 0,
    v: user.isVendor ? 1 : 0,
    ...(user.isAffiliate ? { af: 1 as const } : {}),
    c: mhVersion,
    t: csrfToken,
    e: Math.floor(Date.now() * 0.001) + PAYLOAD_TTL_SECS,
    ...(user.phoneHint ? { ph: user.phoneHint.slice(-3) } : {}),
  };
  const json = JSON.stringify(payload);
  const encoded = btoa(json).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
  return encoded;
}

/**
 * Parse and validate an mh cookie value.
 * Returns null if: absent, corrupt, expired.
 */
export function parseMh(raw: string | null | undefined): MhPayload | null {
  if (!raw) return null;
  try {
    const json = base64UrlToString(raw);
    const payload = JSON.parse(json) as MhPayload;
    const now = Math.floor(Date.now() * 0.001);
    if (typeof payload.e !== 'number' || payload.e < now) return null;
    return payload;
  } catch (err) {
    captureCaught(err, { scope: 'server.auth.mh-cookie.parseMh', severity: 'info' });
    return null;
  }
}

/**
 * Extract the phone hint (`ph`) from an mh cookie value without expiry check.
 * Used to carry the hint forward when rebuilding an expired mh cookie.
 * Returns undefined if absent, corrupt, or no `ph` field.
 */
export function extractPhHint(raw: string | null | undefined): string | undefined {
  if (!raw) return undefined;
  try {
    const json = base64UrlToString(raw);
    const payload = JSON.parse(json) as Partial<MhPayload>;
    return typeof payload.ph === 'string' && payload.ph.length > 0 ? payload.ph : undefined;
  } catch (err) {
    captureCaught(err, { scope: 'server.auth.mh-cookie.extractPhHint', severity: 'info' });
    return undefined;
  }
}

// ─── setMhCookie / clearMhCookie ─────────────────────────────────────────────

/**
 * Append Set-Cookie header for the mh cookie.
 * NOT HttpOnly — client JS reads it for pre-hydration hints.
 */
export function setMhCookie(headers: Headers, value: string): void {
  headers.append(
    'Set-Cookie',
    `${COOKIE_NAME}=${value}; Path=/; Max-Age=${MAX_AGE_SECS}; SameSite=Lax; Secure`,
  );
}

/**
 * Clear the mh cookie by setting Max-Age=0.
 */
export function clearMhCookie(headers: Headers): void {
  headers.append('Set-Cookie', `${COOKIE_NAME}=; Path=/; Max-Age=0; SameSite=Lax; Secure`);
}

// ─── vendor-reg-cookie ───────────────────────────────────────────────────────

/**
 * HMAC-SHA256 signing helpers for the `multideal_vendor_reg` cookie.
 *
 * The cookie carries vendor registration metadata (business name, email, phone)
 * that is consumed by /api/auth/firebase-verify to create the vendor row. Without a
 * signature, a malicious user could craft their own cookie and register as an
 * arbitrary vendor.
 *
 * Wire format:  <base64url-payload>.<base64url-signature>
 *
 * The payload is JSON encoded as base64url (not padded). The signature is
 * HMAC-SHA256 over the payload bytes, keyed with SESSION_SECRET.
 *
 * Uses Web Crypto API only - compatible with Cloudflare Workers.
 */

// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------

function toBase64Url(bytes: ArrayBuffer): string {
  return btoa(String.fromCharCode(...new Uint8Array(bytes)))
    .replace(/\+/g, '-')
    .replace(/\//g, '_')
    .replace(/=+$/, '');
}

function fromBase64Url(str: string): Uint8Array {
  return base64UrlDecodeBytes(str);
}

async function importKey(secret: string): Promise<CryptoKey> {
  return crypto.subtle.importKey(
    'raw',
    new TextEncoder().encode(secret),
    { name: 'HMAC', hash: 'SHA-256' },
    false,
    ['sign', 'verify'],
  );
}

// ---------------------------------------------------------------------------
// Public API
// ---------------------------------------------------------------------------

export interface VendorRegPayload {
  businessName: string;
  ownerEmail: string;
  phone: string;
  pendingVendor: true;
  commsConsentAccepted: boolean;
}

/**
 * Signs the vendor registration payload and returns a signed cookie value.
 *
 * @param payload  - The registration metadata to sign.
 * @param secret   - The HMAC key (SESSION_SECRET).
 * @returns A string suitable for use as the `multideal_vendor_reg` cookie value.
 */
export async function signVendorRegCookie(
  payload: VendorRegPayload,
  secret: string,
): Promise<string> {
  const textEnc = new TextEncoder();
  const payloadBytes = textEnc.encode(JSON.stringify(payload));
  const payloadB64 = toBase64Url(payloadBytes.buffer);

  const key = await importKey(secret);
  const sigBytes = await crypto.subtle.sign('HMAC', key, textEnc.encode(payloadB64));
  const sigB64 = toBase64Url(sigBytes);

  return `${payloadB64}.${sigB64}`;
}

/**
 * Verifies the signed cookie value and returns the payload if valid.
 *
 * @param cookieValue - The raw value of the `multideal_vendor_reg` cookie.
 * @param secret      - The HMAC key (SESSION_SECRET).
 * @returns The verified `VendorRegPayload`, or `null` if the signature is invalid.
 */
export async function verifyVendorRegCookie(
  cookieValue: string,
  secret: string,
): Promise<VendorRegPayload | null> {
  const dotIndex = cookieValue.lastIndexOf('.');
  if (dotIndex === -1) return null;

  const payloadB64 = cookieValue.slice(0, dotIndex);
  const sigB64 = cookieValue.slice(dotIndex + 1);

  if (!payloadB64 || !sigB64) return null;

  try {
    const textEnc = new TextEncoder();
    const key = await importKey(secret);

    // Constant-time verification via SubtleCrypto
    const sigBytes = fromBase64Url(sigB64);
    const valid = await crypto.subtle.verify(
      'HMAC',
      key,
      sigBytes as BufferSource,
      textEnc.encode(payloadB64) as BufferSource,
    );
    if (!valid) return null;

    // Decode and parse payload
    const payloadBytes = fromBase64Url(payloadB64);
    const payloadJson = new TextDecoder().decode(payloadBytes);
    const meta = JSON.parse(payloadJson) as unknown;

    if (
      typeof meta !== 'object' ||
      meta === null ||
      typeof (meta as Record<string, unknown>).businessName !== 'string' ||
      typeof (meta as Record<string, unknown>).ownerEmail !== 'string' ||
      typeof (meta as Record<string, unknown>).phone !== 'string' ||
      (meta as Record<string, unknown>).pendingVendor !== true ||
      (meta as Record<string, unknown>).commsConsentAccepted !== true
    ) {
      return null;
    }

    return meta as VendorRegPayload;
  } catch (err) {
    captureCaught(err, { scope: 'server.auth.vendor-reg-cookie', severity: 'warning' });
    return null;
  }
}

// ─── session cookie builders ─────────────────────────────────────────────────

/**
 * Returns true when the request was made over HTTPS.
 *
 * Used to gate the `Secure` cookie flag — browsers reject Secure cookies on plain http://, which breaks
 * local-dev E2E (`wrangler dev` on http://localhost). Production traffic at
 * dev.multi.deal is always https → Secure stays on. Worker-runtime sees
 * `request.url.protocol === 'https:'` for all CF edge traffic.
 */
export function isHttpsRequest(request: Request): boolean {
  try {
    return new URL(request.url).protocol === 'https:';
  } catch (err) {
    captureCaught(err, { scope: 'server.auth.session.isHttpsRequest', severity: 'warning' });
    return true; // safe default — preserve Secure when URL parse fails
  }
}

// ---------------------------------------------------------------------------
// JWT cookie builders
// ---------------------------------------------------------------------------

const ACCESS_COOKIE = 'multideal_at';
const REFRESH_COOKIE = 'multideal_rt';
/** Access token TTL: 7 days — longevity via refresh-token rotation. */
export const ACCESS_TOKEN_TTL_SECS = 7 * 24 * 60 * 60; // 7 days
/** Refresh token TTL: 30 days (matches session TTL) */
const REFRESH_TOKEN_TTL_SECS = 30 * 24 * 60 * 60;

export function buildAccessCookie(jwt: string, secure: boolean): string {
  const parts = [
    `${ACCESS_COOKIE}=${jwt}`,
    `Max-Age=${ACCESS_TOKEN_TTL_SECS}`,
    'Path=/',
    'HttpOnly',
  ];
  if (secure) parts.push('Secure');
  parts.push('SameSite=Lax');
  return parts.join('; ');
}

export function buildRefreshCookie(rt: string, secure: boolean): string {
  const parts = [
    `${REFRESH_COOKIE}=${rt}`,
    `Max-Age=${REFRESH_TOKEN_TTL_SECS}`,
    'Path=/',
    'HttpOnly',
  ];
  if (secure) parts.push('Secure');
  parts.push('SameSite=Lax');
  return parts.join('; ');
}

/** Clear `multideal_at` access JWT cookie. */
export function clearAccessCookie(secure: boolean): string {
  const parts = [`${ACCESS_COOKIE}=`, 'Max-Age=0', 'Path=/', 'HttpOnly'];
  if (secure) parts.push('Secure');
  parts.push('SameSite=Lax');
  return parts.join('; ');
}

/** Clear `multideal_rt` refresh cookie. Path must match original to allow browser deletion. */
export function clearRefreshCookie(secure: boolean): string {
  const parts = [`${REFRESH_COOKIE}=`, 'Max-Age=0', 'Path=/', 'HttpOnly'];
  if (secure) parts.push('Secure');
  parts.push('SameSite=Lax');
  return parts.join('; ');
}

// ─── CSRF cookie builder ─────────────────────────────────────────────────────

/**
 * Builds the readable (non-HttpOnly) `csrf_token` Set-Cookie header string.
 *
 * The cookie is read by the client JS and echoed back in `x-csrf-token`.
 *
 * `secure` must match request scheme — browsers reject Secure cookies on http
 * (breaks local-dev E2E on `wrangler dev` + http://localhost). Production
 * traffic at dev.multi.deal is always https → Secure stays on.
 */
export function buildCsrfCookieHeader(token: string, secure: boolean): string {
  const csrfMaxAgeSecs = Math.floor(SESSION_TTL_MS * 0.001); // matches session TTL
  const parts = [`csrf_token=${token}`, 'Path=/', `Max-Age=${csrfMaxAgeSecs}`];
  if (secure) parts.push('Secure');
  parts.push('SameSite=Lax');
  return parts.join('; ');
}
