/**
 * HMAC-signed unsubscribe tokens used in the email List-Unsubscribe header
 * and the campaign body link. Self-contained — no database round-trip
 * required to verify, and revocation happens implicitly when the customer
 * row is deleted.
 *
 * Token shape: <base64url(payload)>.<base64url(sig)>
 *   payload = JSON { r: restaurantId, c: customerId, t: ts }
 *   sig     = HMAC-SHA256(payload, secret)
 *
 * Tokens are valid for 1 year — long enough for a delayed re-engagement
 * email to still respect a click but short enough that a leaked archive
 * can't be replayed indefinitely.
 */

import { createHmac, timingSafeEqual } from 'crypto';
import { ENCRYPTION_KEY_RAW } from '@server/auth/encryption-key';

const TOKEN_TTL_MS = 365 * 24 * 60 * 60 * 1000;

function getSecret(): string {
  const secret = process.env.UNSUBSCRIBE_HMAC_SECRET || ENCRYPTION_KEY_RAW;
  if (!secret) {
    throw new Error(
      '[Unsubscribe] Neither UNSUBSCRIBE_HMAC_SECRET nor ENCRYPTION_KEY is set. ' +
      'At least one is required to sign unsubscribe tokens.'
    );
  }
  return secret;
}

function b64uEncode(buf: Buffer): string {
  return buf.toString('base64').replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
}
function b64uDecode(str: string): Buffer {
  const pad = str.length % 4 === 0 ? '' : '='.repeat(4 - (str.length % 4));
  return Buffer.from(str.replace(/-/g, '+').replace(/_/g, '/') + pad, 'base64');
}

export interface UnsubscribePayload {
  restaurantId: string;
  customerId: string;
  /** Issued-at timestamp (ms). */
  issuedAt: number;
}

export function mintUnsubscribeToken(restaurantId: string, customerId: string): string {
  const payload = { r: restaurantId, c: customerId, t: Date.now() };
  const payloadStr = JSON.stringify(payload);
  const payloadB64 = b64uEncode(Buffer.from(payloadStr, 'utf8'));
  const sig = createHmac('sha256', getSecret()).update(payloadB64).digest();
  return `${payloadB64}.${b64uEncode(sig)}`;
}

export function verifyUnsubscribeToken(token: string): UnsubscribePayload | null {
  if (typeof token !== 'string' || !token.includes('.')) return null;
  const [payloadB64, sigB64] = token.split('.', 2);
  if (!payloadB64 || !sigB64) return null;

  const expected = createHmac('sha256', getSecret()).update(payloadB64).digest();
  let provided: Buffer;
  try { provided = b64uDecode(sigB64); } catch { return null; }
  if (provided.length !== expected.length) return null;
  if (!timingSafeEqual(provided, expected)) return null;

  let parsed: { r?: unknown; c?: unknown; t?: unknown };
  try { parsed = JSON.parse(b64uDecode(payloadB64).toString('utf8')); }
  catch { return null; }
  if (typeof parsed.r !== 'string' || typeof parsed.c !== 'string' || typeof parsed.t !== 'number') return null;
  if (Date.now() - parsed.t > TOKEN_TTL_MS) return null;
  return { restaurantId: parsed.r, customerId: parsed.c, issuedAt: parsed.t };
}
