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

const SIGNATURE_MAX_AGE_MS = 5 * 60 * 1_000;
const HUBSPOT_URI_DECODE_PATTERN = /%3A|%2F|%3F|%40|%21|%24|%27|%28|%29|%2A|%2C|%3B/gi;
const HUBSPOT_URI_DECODINGS: Record<string, string> = {
  '%3A': ':',
  '%2F': '/',
  '%3F': '?',
  '%40': '@',
  '%21': '!',
  '%24': '$',
  '%27': "'",
  '%28': '(',
  '%29': ')',
  '%2A': '*',
  '%2C': ',',
  '%3B': ';',
};

export interface HubSpotV3SignatureInput {
  clientSecret: string;
  method: string;
  publicUri: string;
  rawBody: string;
  signature: string | undefined;
  timestamp: string | undefined;
  now?: Date;
}

export type HubSpotSignatureResult =
  | { valid: true }
  | { valid: false; reason: 'missing_headers' | 'invalid_timestamp' | 'stale_timestamp' | 'mismatch' };

export function normalizeHubSpotSignatureUri(uri: string): string {
  const withoutFragment = uri.split('#', 1)[0] ?? uri;
  const queryIndex = withoutFragment.indexOf('?');
  if (queryIndex < 0) return withoutFragment;
  const prefix = withoutFragment.slice(0, queryIndex + 1);
  const query = withoutFragment.slice(queryIndex + 1).replace(
    HUBSPOT_URI_DECODE_PATTERN,
    (encoded) => HUBSPOT_URI_DECODINGS[encoded.toUpperCase()] ?? encoded,
  );
  return prefix + query;
}

export function verifyHubSpotV3Signature(input: HubSpotV3SignatureInput): HubSpotSignatureResult {
  if (!input.signature || !input.timestamp) return { valid: false, reason: 'missing_headers' };
  if (!/^\d+$/.test(input.timestamp)) return { valid: false, reason: 'invalid_timestamp' };
  const timestamp = Number(input.timestamp);
  if (!Number.isSafeInteger(timestamp)) return { valid: false, reason: 'invalid_timestamp' };
  const now = input.now ?? new Date();
  if (Math.abs(now.getTime() - timestamp) > SIGNATURE_MAX_AGE_MS) {
    return { valid: false, reason: 'stale_timestamp' };
  }
  const source = `${input.method}${normalizeHubSpotSignatureUri(input.publicUri)}${input.rawBody}${input.timestamp}`;
  const expected = createHmac('sha256', input.clientSecret).update(source, 'utf8').digest('base64');
  const expectedBytes = Buffer.from(expected, 'utf8');
  const suppliedBytes = Buffer.from(input.signature, 'utf8');
  if (expectedBytes.length !== suppliedBytes.length) return { valid: false, reason: 'mismatch' };
  return timingSafeEqual(expectedBytes, suppliedBytes)
    ? { valid: true }
    : { valid: false, reason: 'mismatch' };
}
