/**
 * Guest access token helpers — BUG-2 (IDOR fix on guest purchase confirmation).
 *
 * Guest purchases (order.buyer_user_id IS NULL) are not bound to any session,
 * so the confirmation page cannot use session ownership to gate access. We
 * derive a 32-byte HMAC token when the guest order is created (PENDING, no
 * voucher yet — see JP-027), store its SHA-256 hash + a TTL, and require the
 * caller to present the plaintext token via the `t` query param. Without it,
 * the page returns 403 / redirects.
 *
 * Token shape:
 *   - 32 HMAC-SHA-256 bytes → 64 hex chars
 *   - SHA-256(token) hex stored in `order_line_voucher_ext.guest_access_token_hash`
 *   - Expires after 7 days (`guest_access_token_expires_at`)
 *
 * No real voucher (and therefore no order_line_voucher_ext row) exists yet
 * when the token is issued, so it is upserted onto a placeholder row keyed
 * by a synthetic voucherId (`guest-pending:<orderLineId>`). Once payment
 * succeeds, `consumePendingGuestAccessToken` atomically deletes that
 * placeholder and hands its hash/expiry to the finalize path, which copies
 * them onto the real voucher's row — so there is never more than one
 * order_line_voucher_ext row per line (purchases.ts's `ORDER BY voucherId
 * LIMIT 1` lookups depend on that).
 *
 * Plaintext is derived only inside the guest workflow. It is never stored or logged.
 */

import { and, eq, gt } from 'drizzle-orm';
import { bytesToHex } from '@/lib/encoding.js';
import type { DrizzleClient } from '../client.js';
import { orderLineVoucherExt } from '../schema.js';

const GUEST_ACCESS_TOKEN_TTL_MS = 7 * 24 * 60 * 60 * 1000;

/** SHA-256(input) as lowercase hex (Web Crypto, Workers-safe). */
async function sha256Hex(input: string): Promise<string> {
  const data = new TextEncoder().encode(input);
  const digest = await crypto.subtle.digest('SHA-256', data);
  return bytesToHex(new Uint8Array(digest));
}

/** Derive 32-byte credential from server key + caller-held recovery secret. */
export async function deriveGuestAccessToken(
  secret: string,
  recoverySecret: string,
  orderLineId: string,
): Promise<string> {
  const encoder = new TextEncoder();
  const key = await crypto.subtle.importKey(
    'raw',
    encoder.encode(secret),
    { name: 'HMAC', hash: 'SHA-256' },
    false,
    ['sign'],
  );
  const payload = `guest-access-token:v1:${recoverySecret}:${orderLineId}`;
  const token = await crypto.subtle.sign('HMAC', key, encoder.encode(payload));
  return bytesToHex(new Uint8Array(token));
}

function pendingVoucherId(orderLineId: string): string {
  return `guest-pending:${orderLineId}`;
}

/**
 * Persist a deterministic guest access token hash + expiry onto the line's
 * pre-fulfillment placeholder row, and return the plaintext token to the
 * caller. Caller is responsible for delivering the plaintext to the guest
 * (redirect URL + email) and never persisting it elsewhere.
 */
export async function issueGuestAccessToken(
  db: DrizzleClient,
  orderLineId: string,
  recoverySecret: string,
  secret: string,
): Promise<{ token: string; expiresAt: Date }> {
  const token = await deriveGuestAccessToken(secret, recoverySecret, orderLineId);
  const tokenHash = await sha256Hex(token);
  const expiresAt = new Date(Date.now() + GUEST_ACCESS_TOKEN_TTL_MS);

  await db
    .insert(orderLineVoucherExt)
    .values({
      voucherId: pendingVoucherId(orderLineId),
      lineId: orderLineId,
      guestAccessTokenHash: tokenHash,
      guestAccessTokenExpiresAt: expiresAt,
    })
    .onConflictDoUpdate({
      target: orderLineVoucherExt.voucherId,
      set: { guestAccessTokenHash: tokenHash, guestAccessTokenExpiresAt: expiresAt },
    });

  return { token, expiresAt };
}

/**
 * Atomically deletes the line's pre-fulfillment placeholder row and returns
 * its guest-access-token hash + expiry, or null if none exists (already
 * consumed, or a registered-user purchase). Called once by finalize.ts when
 * the real voucher row is created, to carry the hash onto that row without
 * ever touching the plaintext token.
 */
export async function consumePendingGuestAccessToken(
  db: DrizzleClient,
  orderLineId: string,
): Promise<{ hash: string; expiresAt: Date } | null> {
  const [row] = await db
    .delete(orderLineVoucherExt)
    .where(eq(orderLineVoucherExt.voucherId, pendingVoucherId(orderLineId)))
    .returning({
      hash: orderLineVoucherExt.guestAccessTokenHash,
      expiresAt: orderLineVoucherExt.guestAccessTokenExpiresAt,
    });
  if (!row?.hash || !row.expiresAt) return null;
  return { hash: row.hash, expiresAt: row.expiresAt };
}

/**
 * Verify a plaintext guest access token against the stored hash + TTL for a
 * given order line. Returns true ONLY when:
 *   - A voucher extension row exists for the line.
 *   - A guest access token has been issued for it.
 *   - The token has not expired.
 *   - The hash of the supplied plaintext matches the stored hash.
 */
export async function verifyGuestAccessToken(
  db: DrizzleClient,
  orderLineId: string,
  plain: string,
): Promise<boolean> {
  if (!plain || plain.length !== 64 || !/^[a-f0-9]{64}$/.test(plain)) return false;
  const tokenHash = await sha256Hex(plain);
  const now = new Date();
  const [row] = await db
    .select({ id: orderLineVoucherExt.lineId })
    .from(orderLineVoucherExt)
    .where(
      and(
        eq(orderLineVoucherExt.lineId, orderLineId),
        eq(orderLineVoucherExt.guestAccessTokenHash, tokenHash),
        gt(orderLineVoucherExt.guestAccessTokenExpiresAt, now),
      ),
    )
    .limit(1);
  return !!row;
}
