import { signHmac, timingSafeEqual } from './primitives.ts';

const VERSION = 'v1';

// Unsubscribe links must keep working well after the campaign ships, so this is
// deliberately generous — it only exists to stop a leaked link replaying forever.
const DEFAULT_MAX_AGE_SECONDS = 2 * 365 * 24 * 60 * 60;
const FUTURE_SKEW_SECONDS = 5 * 60;

export interface UnsubscribeTokenPayload {
  orgId: string;
  contactId: string;
  campaignId: string;
  issuedAt: number;
}

export class UnsubscribeTokenError extends Error {
  readonly code = 'unsubscribe_token_invalid';
  constructor(message: string) {
    super(`unsubscribe_token_invalid: ${message}`);
  }
}

export function signUnsubscribeToken(
  payload: Omit<UnsubscribeTokenPayload, 'issuedAt'> & { issuedAt?: number },
  pepper?: string,
): string {
  const secret = pepper ?? process.env.MUNIN_KEY_PEPPER ?? '';
  if (!secret) throw new Error('MUNIN_KEY_PEPPER is required to sign unsubscribe tokens');
  const issuedAt = payload.issuedAt ?? Math.floor(Date.now() / 1000);
  for (const v of [payload.orgId, payload.contactId, payload.campaignId]) {
    if (!v || /[.\s]/.test(v)) {
      throw new Error('unsubscribe token fields must be non-empty and contain no dots or whitespace');
    }
  }
  const body = `${VERSION}.${payload.orgId}.${payload.contactId}.${payload.campaignId}.${issuedAt}`;
  const sig = signHmac(body, secret);
  return `${body}.${sig}`;
}

export function verifyUnsubscribeToken(
  token: string,
  pepper?: string,
  maxAgeSeconds: number = DEFAULT_MAX_AGE_SECONDS,
): UnsubscribeTokenPayload {
  const secret = pepper ?? process.env.MUNIN_KEY_PEPPER ?? '';
  if (!secret) throw new UnsubscribeTokenError('server pepper not configured');
  const parts = token.split('.');
  if (parts.length !== 6) throw new UnsubscribeTokenError('malformed token');
  const [version, orgId, contactId, campaignId, issuedAtStr, sig] = parts;
  if (version !== VERSION) throw new UnsubscribeTokenError(`unknown version ${version}`);
  const body = `${version}.${orgId}.${contactId}.${campaignId}.${issuedAtStr}`;
  const expected = signHmac(body, secret);
  if (!timingSafeEqual(expected, sig!)) throw new UnsubscribeTokenError('signature mismatch');
  const issuedAt = Number(issuedAtStr);
  if (!Number.isFinite(issuedAt)) throw new UnsubscribeTokenError('issuedAt not numeric');
  const now = Math.floor(Date.now() / 1000);
  if (issuedAt > now + FUTURE_SKEW_SECONDS) throw new UnsubscribeTokenError('issuedAt in the future');
  if (now - issuedAt > maxAgeSeconds) throw new UnsubscribeTokenError('token expired');
  return { orgId: orgId!, contactId: contactId!, campaignId: campaignId!, issuedAt };
}
