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

/**
 * Slack request signature verification.
 *
 * This endpoint is the one route in the app that is NOT behind Traefik's OAuth
 * chain — it has to be, because Slack posts to it as a machine with no browser
 * session. The signature IS the authentication, so it is not optional and it
 * fails closed: an unset signing secret rejects everything rather than waving
 * requests through, which is the failure mode that would quietly expose split
 * writes to the open internet.
 *
 * The v0 scheme signs `v0:<timestamp>:<raw body>`. It must be the RAW body —
 * re-serialising the parsed form changes the bytes and every signature fails.
 */
export function verifySlackSignature(
  rawBody: string,
  timestamp: string | null,
  signature: string | null
): boolean {
  const secret = process.env.SLACK_SIGNING_SECRET;
  if (!secret || !timestamp || !signature) return false;

  // Replay window. Slack recommends 5 minutes; a captured request is otherwise
  // valid forever, and these actions move money between people.
  const age = Math.abs(Date.now() / 1000 - Number(timestamp));
  if (!Number.isFinite(age) || age > 300) return false;

  const expected =
    "v0=" +
    createHmac("sha256", secret)
      .update(`v0:${timestamp}:${rawBody}`)
      .digest("hex");

  const a = Buffer.from(expected);
  const b = Buffer.from(signature);
  // timingSafeEqual throws on length mismatch, which is itself a leak of one
  // bit; check length first and return the same false either way.
  return a.length === b.length && timingSafeEqual(a, b);
}

/**
 * Which participant pressed the button.
 *
 * SLACK_USER_MAP is `<slack user id>:<participant id>` pairs, comma separated.
 * An unknown Slack user is rejected rather than defaulted to the owner: in a
 * two-person household a wrong attribution is not a rounding error, it is the
 * other person's opinion recorded under your name.
 */
/** The reverse: which Slack user is this participant, for DMing them. */
export function slackUserForParticipant(participantId: number): string | null {
  const map = process.env.SLACK_USER_MAP ?? "";
  for (const pair of map.split(",")) {
    const [slack, participant] = pair.split(":").map((s) => s.trim());
    if (slack && Number(participant) === participantId) return slack;
  }
  return null;
}

export function participantForSlackUser(slackUserId: string): number | null {
  const map = process.env.SLACK_USER_MAP ?? "";
  for (const pair of map.split(",")) {
    const [slack, participant] = pair.split(":").map((s) => s.trim());
    if (slack && slack === slackUserId && participant) {
      const id = Number(participant);
      if (Number.isInteger(id)) return id;
    }
  }
  return null;
}
