import { createHmac, timingSafeEqual } from 'node:crypto';

/**
 * Signed session tokens for the affiliate portal. The token is an HMAC-signed
 * `{ id, exp }` — the affiliate CANNOT be identified by a raw id in the URL
 * anymore; the id is read from the *verified* token, which is the only thing
 * that proves who the caller is. This closes the IDOR hole where anyone could
 * hit /affiliates/<someone-else-id>/payment and tamper with their data.
 */
function secret(): string {
  return (
    process.env.AFFILIATE_TOKEN_SECRET ||
    process.env.SHOPIFY_API_SECRET ||
    'dev-affiliate-secret-change-me'
  );
}

/**
 * Issue a token for a logged-in affiliate (default 30-day expiry). The token
 * carries the affiliate's `shop` so every portal request resolves to the SAME
 * merchant the affiliate signed up under — never the shared demo/default store.
 * This is what makes an admin "approve" actually reach the affiliate's portal.
 */
export function signAffiliateToken(affiliateId: string, shop?: string | null, ttlDays = 30): string {
  const exp = Math.floor(Date.now() / 1000) + ttlDays * 86400;
  const payload = Buffer.from(JSON.stringify({ id: affiliateId, shop: shop || undefined, exp })).toString('base64url');
  const sig = createHmac('sha256', secret()).update(payload).digest('base64url');
  return `${payload}.${sig}`;
}

/**
 * Verify a token; returns `{ id, shop }`, or null if invalid/expired. Old tokens
 * issued before `shop` was added simply return `shop: undefined` (still valid).
 */
export function verifyAffiliateToken(token?: string | null): { id: string; shop?: string } | null {
  if (!token) return null;
  const [payload, sig] = token.split('.');
  if (!payload || !sig) return null;

  const expected = createHmac('sha256', secret()).update(payload).digest();
  let given: Buffer;
  try {
    given = Buffer.from(sig, 'base64url');
  } catch {
    return null;
  }
  if (given.length !== expected.length || !timingSafeEqual(given, expected)) return null;

  try {
    const { id, shop, exp } = JSON.parse(Buffer.from(payload, 'base64url').toString('utf8'));
    if (typeof exp === 'number' && Math.floor(Date.now() / 1000) >= exp) return null;
    if (typeof id !== 'string') return null;
    return { id, shop: typeof shop === 'string' ? shop : undefined };
  } catch {
    return null;
  }
}
