/**
 * StandShare webhook signature verifier — TypeScript reference implementation
 *
 * Signature algorithm:
 *   Header:     X-StandShare-Signature: t=<unix_seconds>,v1=<hmac_sha256_hex>
 *   Signed data: `${t}.${rawBody}` (the exact raw body string, UTF-8)
 *   Key:         your signing secret (the full "whsec_…" string, UTF-8 bytes)
 *   Digest:      HMAC-SHA256, lowercase hex
 *   Tolerance:   |now − t| must be ≤ 300 seconds (5 minutes)
 *   Comparison:  timing-safe (Buffer.timingSafeEqual on 64-char hex strings)
 *
 * Usage:
 *   node --input-type=module verify-webhook.ts   # (or via ts-node / tsx)
 *
 * Test vectors (from packages/api/src/fixtures/webhook-signature-fixture.ts in
 * GraditiPro/StandShare — the single source of truth):
 *   secret:    "whsec_testonly_AAAA..."
 *   timestamp: 1749877200
 *   body:      '{"type":"event.created","data":{"id":"evt_001"}}'
 *   expected:  "bffa13b5964bd097b5cca399ef2386585906f5af213ddcba2a2f1a9b41399680"
 *   header:    "t=1749877200,v1=bffa13b5964bd097b5cca399ef2386585906f5af213ddcba2a2f1a9b41399680"
 */

import crypto from "node:crypto";

// ── Constants ────────────────────────────────────────────────────────────────

/** Maximum clock skew between sender and receiver, in seconds. */
const TOLERANCE_SECONDS = 300;

// ── Types ────────────────────────────────────────────────────────────────────

export type VerifyResult =
  | { ok: true }
  | { ok: false; reason: "missing_header" | "malformed_header" | "timestamp_expired" | "invalid_signature" };

// ── Core ─────────────────────────────────────────────────────────────────────

/**
 * Verify the X-StandShare-Signature header on an incoming webhook delivery.
 *
 * @param secret          Your webhook signing secret (the full "whsec_…" value).
 * @param rawBody         The request body exactly as received — do NOT parse or
 *                        re-serialize it. Use the raw bytes / string.
 * @param signatureHeader The value of the X-StandShare-Signature header.
 * @returns               { ok: true } on success, or { ok: false, reason } on failure.
 */
export function verifyWebhookSignature(
  secret: string,
  rawBody: string,
  signatureHeader: string | undefined | null
): VerifyResult {
  // 1. Missing header
  if (!signatureHeader || !signatureHeader.trim()) {
    return { ok: false, reason: "missing_header" };
  }

  // 2. Parse t= and v1= from the header
  const fields: Record<string, string> = {};
  for (const part of signatureHeader.split(",")) {
    const eq = part.indexOf("=");
    if (eq === -1) continue;
    const k = part.slice(0, eq).trim();
    const v = part.slice(eq + 1).trim();
    if (k) fields[k] = v;
  }

  const tStr = fields["t"];
  const v1 = fields["v1"];
  if (!tStr || !v1) return { ok: false, reason: "malformed_header" };

  const timestamp = Number(tStr);
  if (!Number.isInteger(timestamp) || Number.isNaN(timestamp)) {
    return { ok: false, reason: "malformed_header" };
  }
  if (!/^[0-9a-f]{64}$/.test(v1)) {
    return { ok: false, reason: "malformed_header" };
  }

  // 3. Timestamp tolerance check
  const nowSeconds = Math.floor(Date.now() / 1000);
  if (Math.abs(nowSeconds - timestamp) > TOLERANCE_SECONDS) {
    return { ok: false, reason: "timestamp_expired" };
  }

  // 4. Recompute HMAC-SHA256 over `${timestamp}.${rawBody}`
  const expected = crypto
    .createHmac("sha256", secret)
    .update(`${timestamp}.${rawBody}`, "utf8")
    .digest("hex");

  // 5. Timing-safe comparison (both are 64-char hex — same byte length)
  const expectedBuf = Buffer.from(expected, "utf8");
  const receivedBuf = Buffer.from(v1, "utf8");
  if (expectedBuf.length !== receivedBuf.length) {
    return { ok: false, reason: "invalid_signature" };
  }
  if (!crypto.timingSafeEqual(expectedBuf, receivedBuf)) {
    return { ok: false, reason: "invalid_signature" };
  }

  return { ok: true };
}

// ── Self-test ────────────────────────────────────────────────────────────────
// Run:  node --input-type=module verify-webhook.ts
// (or:  npx tsx verify-webhook.ts)

const PINNED = {
  secret: "whsec_testonly_AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
  timestamp: 1749877200,
  body: '{"type":"event.created","data":{"id":"evt_001"}}',
  expectedSig: "bffa13b5964bd097b5cca399ef2386585906f5af213ddcba2a2f1a9b41399680",
  header: "t=1749877200,v1=bffa13b5964bd097b5cca399ef2386585906f5af213ddcba2a2f1a9b41399680",
};

let passed = 0;
let failed = 0;

function assert(label: string, result: VerifyResult, expectOk: boolean, expectReason?: string): void {
  const ok = result.ok === expectOk && (!expectReason || (!result.ok && result.reason === expectReason));
  if (ok) {
    console.log(`  PASS  ${label}`);
    passed++;
  } else {
    console.error(`  FAIL  ${label} — got ${JSON.stringify(result)}`);
    failed++;
  }
}

// The verifier uses the current clock for the tolerance check, so we override
// Date.now for test cases that use the pinned timestamp.
const _realDateNow = Date.now;
function withFrozenClock(fn: () => void): void {
  Date.now = () => PINNED.timestamp * 1000;
  try { fn(); } finally { Date.now = _realDateNow; }
}

console.log("\nStandShare webhook verifier — self-test (TypeScript)");

withFrozenClock(() => {
  // Happy path — pinned fixture vector
  assert(
    "pinned fixture vector passes",
    verifyWebhookSignature(PINNED.secret, PINNED.body, PINNED.header),
    true
  );
});

// Missing header
assert(
  "missing header → missing_header",
  verifyWebhookSignature(PINNED.secret, PINNED.body, undefined),
  false,
  "missing_header"
);

// Malformed header (no v1=)
assert(
  "header without v1= → malformed_header",
  verifyWebhookSignature(PINNED.secret, PINNED.body, `t=${PINNED.timestamp}`),
  false,
  "malformed_header"
);

// Expired timestamp (way in the past)
assert(
  "expired timestamp → timestamp_expired",
  verifyWebhookSignature(PINNED.secret, PINNED.body, PINNED.header),
  false,
  "timestamp_expired"
);

withFrozenClock(() => {
  // Tampered body
  assert(
    "tampered body → invalid_signature",
    verifyWebhookSignature(PINNED.secret, PINNED.body + " tampered", PINNED.header),
    false,
    "invalid_signature"
  );

  // Wrong secret
  assert(
    "wrong secret → invalid_signature",
    verifyWebhookSignature("whsec_wrong", PINNED.body, PINNED.header),
    false,
    "invalid_signature"
  );

  // Wrong v1 value (correct length but wrong bits)
  const badHeader = PINNED.header.replace(PINNED.expectedSig, "a".repeat(64));
  assert(
    "wrong v1 hex → invalid_signature",
    verifyWebhookSignature(PINNED.secret, PINNED.body, badHeader),
    false,
    "invalid_signature"
  );
});

console.log(`\n${passed + failed} tests — ${passed} passed, ${failed} failed`);
if (failed > 0) process.exit(1);
