// OAuth state token for the Slack workspace install handshake.
// Format: base64url(payloadJson).base64url(HMAC-SHA256 signature). The signature
// binds the payload (acting userId, optional pinned redirectUri, expiresAt) to a
// shared `signingSecret` the host supplies on both the begin and complete calls,
// so a forged or tampered state is rejected — the state is the CSRF / integrity
// control for the OAuth callback, not just an expiry envelope.

export interface StateTokenPayload {
  userId?: string;
  redirectUri?: string;
  expiresAt: string;
}

function bytesToBase64Url(bytes: Uint8Array): string {
  let binary = "";
  for (let i = 0; i < bytes.length; i++) binary += String.fromCharCode(bytes[i]);
  return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
}

function toBase64Url(input: string): string {
  return bytesToBase64Url(new TextEncoder().encode(input));
}

function base64UrlToBytes(input: string): Uint8Array<ArrayBuffer> {
  const pad = (4 - (input.length % 4)) % 4;
  const b64 = input.replace(/-/g, "+").replace(/_/g, "/") + "=".repeat(pad);
  const binary = atob(b64);
  const bytes = new Uint8Array(binary.length);
  for (let i = 0; i < bytes.length; i++) bytes[i] = binary.charCodeAt(i);
  return bytes;
}

function fromBase64Url(input: string): string {
  return new TextDecoder().decode(base64UrlToBytes(input));
}

const STATE_TTL_MS = 1000 * 60 * 10;

function importHmacKey(signingSecret: string): Promise<CryptoKey> {
  return crypto.subtle.importKey(
    "raw",
    new TextEncoder().encode(signingSecret),
    { name: "HMAC", hash: "SHA-256" },
    false,
    ["sign", "verify"],
  );
}

/** Issue a signed state token. The `signingSecret` must match the one passed to {@link parseStateToken} on the callback. */
export async function createStateToken(
  payload: Omit<StateTokenPayload, "expiresAt">,
  signingSecret: string,
): Promise<string> {
  const expiresAt = new Date(Date.now() + STATE_TTL_MS).toISOString();
  const body = toBase64Url(JSON.stringify({ ...payload, expiresAt }));
  const key = await importHmacKey(signingSecret);
  const signature = await crypto.subtle.sign("HMAC", key, new TextEncoder().encode(body));
  return `${body}.${bytesToBase64Url(new Uint8Array(signature))}`;
}

/**
 * Verify and decode a state token. Returns `null` when the token is malformed,
 * the signature does not verify against `signingSecret` (forged / tampered /
 * wrong key), or it has expired. Signature verification (`crypto.subtle.verify`)
 * is constant-time and runs before the payload is trusted.
 */
export async function parseStateToken(
  token: string,
  signingSecret: string,
): Promise<StateTokenPayload | null> {
  try {
    const dot = token.indexOf(".");
    if (dot <= 0 || dot === token.length - 1) return null;
    const body = token.slice(0, dot);
    const signature = base64UrlToBytes(token.slice(dot + 1));

    const key = await importHmacKey(signingSecret);
    const valid = await crypto.subtle.verify(
      "HMAC",
      key,
      signature,
      new TextEncoder().encode(body),
    );
    if (!valid) return null;

    const parsed = JSON.parse(fromBase64Url(body)) as StateTokenPayload;
    if (!parsed.expiresAt || new Date(parsed.expiresAt).getTime() < Date.now()) {
      return null;
    }
    return parsed;
  } catch {
    return null;
  }
}
