import { createHmac, timingSafeEqual } from 'crypto';
import { env } from '../config/env';

/**
 * Short-lived signed URLs for media streaming (added 2026-04-12).
 *
 * Background: `<img src>` and `<video src>` tags can't set Authorization
 * headers, so the legacy design passed a full JWT access token in the query
 * string (`?token=...`). JWTs in URLs leak via:
 *   - browser history and screen recordings
 *   - server / proxy / CDN access logs
 *   - Referer headers sent to external domains
 *   - shared links (the URL becomes a long-lived auth token)
 *
 * This module replaces that with a per-URL HMAC signature that:
 *   - expires in 5 minutes by default (`DEFAULT_TTL_SECONDS`)
 *   - is bound to one specific resource path (no cross-URL reuse)
 *   - carries only the user-id (not a full session token) in the clear
 *   - is single-purpose (signing key derived, never equals any JWT secret)
 *
 * Clients call `POST /api/media/sign` with header auth to get a signed URL,
 * then set `<img src>`/`<video src>` to it. The server verifies the sig on
 * fetch; an attacker recovering the URL from logs has at most 5 minutes to
 * replay it, and can't forge a URL for any other resource.
 */

const DEFAULT_TTL_SECONDS = 300; // 5 minutes

/**
 * Deterministically derive the media-URL signing key from JWT_ACCESS_SECRET.
 * This avoids adding yet another required env var while maintaining key
 * separation — HMAC with a fixed context string produces a key that cannot
 * be used to forge JWTs (different HMAC inputs ⇒ different outputs, and the
 * JWT signing path never uses this derivation).
 */
function getSigningKey(): Buffer {
  return createHmac('sha256', env.JWT_ACCESS_SECRET)
    .update('cml:media-url-signing-v1')
    .digest();
}

/**
 * Canonicalize the path portion we're signing. Strips the query string and
 * any trailing slash so that adding/removing query params can't change the
 * signed payload.
 */
function canonicalPath(path: string): string {
  const withoutQuery = path.split('?')[0] ?? path;
  return withoutQuery.replace(/\/+$/, '');
}

function computeSignature(path: string, userId: string, exp: number): string {
  const payload = `${canonicalPath(path)}|${userId}|${exp}`;
  return createHmac('sha256', getSigningKey()).update(payload).digest('hex');
}

export interface SignedUrlParams {
  sig: string;
  exp: string; // unix seconds (string in query)
  uid: string;
}

/**
 * Generate the signed query-string params for a given path + user.
 * Does NOT append them to the URL — callers combine as they see fit.
 */
export function signMediaPath(
  path: string,
  userId: string,
  ttlSeconds: number = DEFAULT_TTL_SECONDS
): SignedUrlParams {
  const exp = Math.floor(Date.now() / 1000) + ttlSeconds;
  return {
    sig: computeSignature(path, userId, exp),
    exp: String(exp),
    uid: userId,
  };
}

/**
 * Verify a signed URL. Returns the authenticated userId on success, null on
 * any failure (expired, tampered, missing params).
 */
export function verifyMediaSignature(
  path: string,
  query: Record<string, string | string[] | undefined>
): { valid: true; userId: string } | { valid: false; reason: string } {
  const sig = typeof query.sig === 'string' ? query.sig : undefined;
  const exp = typeof query.exp === 'string' ? query.exp : undefined;
  const uid = typeof query.uid === 'string' ? query.uid : undefined;

  if (!sig || !exp || !uid) return { valid: false, reason: 'missing params' };

  const expNum = Number(exp);
  if (!Number.isFinite(expNum)) return { valid: false, reason: 'bad exp' };
  if (expNum < Math.floor(Date.now() / 1000)) return { valid: false, reason: 'expired' };

  const expected = computeSignature(path, uid, expNum);
  if (sig.length !== expected.length) return { valid: false, reason: 'sig length' };
  // constant-time compare to avoid timing oracle on the HMAC
  const ok = timingSafeEqual(Buffer.from(sig, 'hex'), Buffer.from(expected, 'hex'));
  if (!ok) return { valid: false, reason: 'sig mismatch' };

  return { valid: true, userId: uid };
}
