import { createHmac, timingSafeEqual, randomUUID } from "node:crypto";
import { createServer } from "node:http";
import { mkdir, open, link, unlink } from "node:fs/promises";
import { join, resolve } from "node:path";
import { fileURLToPath } from "node:url";

// Verify the unparsed bytes. During rotation accept any configured current/overlap key.
export function verifyWebhook(raw, headers, secrets, now = Date.now()) {
  const id = headers["webhook-id"];
  const timestamp = headers["webhook-timestamp"];
  const signatures = headers["webhook-signature"];
  if (
    typeof id !== "string" ||
    !/^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(
      id,
    ) ||
    typeof timestamp !== "string" ||
    !/^\d{1,12}$/.test(timestamp) ||
    typeof signatures !== "string" ||
    Math.abs(Math.floor(now / 1000) - Number(timestamp)) > 300 ||
    raw.length > 16384
  )
    throw new Error("Invalid webhook");
  const candidates = signatures
    .split(/\s+/)
    .filter((s) => /^v1,[A-Za-z0-9+/]{43}=$/.test(s))
    .map((s) => Buffer.from(s.slice(3), "base64"));
  const matched = secrets.some((secret) => {
    if (!/^whsec_[A-Za-z0-9+/]{43}=$/.test(secret)) return false;
    const expected = createHmac(
      "sha256",
      Buffer.from(secret.slice(6), "base64"),
    )
      .update(`${id}.${timestamp}.`)
      .update(raw)
      .digest();
    return candidates.some(
      (candidate) =>
        candidate.length === expected.length &&
        timingSafeEqual(candidate, expected),
    );
  });
  if (!matched) throw new Error("Invalid webhook");
  const event = JSON.parse(raw.toString("utf8"));
  if (event.eventId !== id || event.schemaVersion !== 1)
    throw new Error("Invalid webhook");
  return event;
}

// Single-host POSIX demonstration: publish only fsynced files; the filename is
// both inbox deduplication key and durable work item. Keep entries for >=30 days,
// including processed markers. For distributed production use a database UNIQUE
// eventId + queued work in ONE transaction instead, on durable replicated storage.
export async function enqueueFile(directory, event, raw) {
  await mkdir(directory, { recursive: true, mode: 0o700 });
  const temporary = join(directory, `.pending-${randomUUID()}`);
  const file = await open(temporary, "wx", 0o600);
  try {
    await file.writeFile(raw);
    await file.sync();
  } finally {
    await file.close();
  }
  try {
    try {
      await link(temporary, join(directory, `${event.eventId}.json`));
    } catch (error) {
      if (error.code !== "EEXIST") throw error;
    }
    const dir = await open(directory, "r");
    try {
      await dir.sync();
    } finally {
      await dir.close();
    }
  } finally {
    await unlink(temporary);
  }
}
export function receiver({ secrets, enqueue }) {
  return createServer(async (req, res) => {
    if (req.method !== "POST" || req.url !== "/webhooks") {
      res.writeHead(404).end();
      return;
    }
    try {
      const chunks = [];
      let size = 0;
      for await (const chunk of req) {
        size += chunk.length;
        if (size > 16384) {
          res.writeHead(413).end();
          return;
        }
        chunks.push(chunk);
      }
      const raw = Buffer.concat(chunks);
      let event;
      try {
        event = verifyWebhook(raw, req.headers, secrets);
      } catch {
        res.writeHead(400).end();
        return;
      }
      await enqueue(event, raw); // Commit deduplication AND work before acknowledging.
      res.writeHead(204).end();
    } catch {
      res.writeHead(503).end();
    } // No sensitive exception or payload logs.
  });
}
if (
  process.argv[1] &&
  resolve(process.argv[1]) === fileURLToPath(import.meta.url)
) {
  const secrets = (process.env.WEBHOOK_SECRETS ?? "").split(",");
  if (!secrets.every((s) => /^whsec_[A-Za-z0-9+/]{43}=$/.test(s)))
    throw new Error("Set WEBHOOK_SECRETS to your signing secret(s)");
  if (!process.env.WEBHOOK_INBOX)
    throw new Error("Set WEBHOOK_INBOX to a persistent POSIX directory");
  const server = receiver({
    secrets,
    enqueue: (event, raw) => enqueueFile(process.env.WEBHOOK_INBOX, event, raw),
  });
  server.requestTimeout = 10000;
  server.headersTimeout = 10000;
  server.listen(8080, "127.0.0.1"); // Put a public HTTPS reverse proxy on port 443 in front.
}
