/**
 * AAO Gateway Phase 2a — virtual key format and helpers.
 *
 * Key shape: `sk-aao-<base62-32>` (39 chars total). The `sk-aao-` prefix
 * is intentionally distinct from LiteLLM's `sk-` keys so a leaked key
 * can be traced back to AAO at a glance. The body carries ~184 bits
 * of entropy (23 random bytes encoded), which is comfortably above any
 * realistic collision risk for a key store.
 *
 * Entropy / encoding contract
 * ───────────────────────────
 * We deliberately size the input at 23 bytes so that the big-int
 * encoding of any 23-byte input always fits in ≤ 31 base62 characters
 * (23·8 = 184 bits; 62^31 ≈ 2^184.6). bytesToBase62 then left-pads
 * with '0' to KEY_BODY_LEN = 32 — never truncates. Truncating the
 * most-significant chars would bias the leading character of the body
 * (it could only ever land on a value whose absence shortens the
 * output), so we now hard-fail in that branch as a regression guard.
 * If you change KEY_BODY_BYTES or KEY_BODY_LEN, recompute the bound:
 *   max base62 length = ceil(KEY_BODY_BYTES · 8 / log2(62))  ≤  KEY_BODY_LEN
 *
 * The Repository stores only `sha256(raw)`; this module is the only
 * place that ever holds the raw bytes. Callers (admin API, bootstrap
 * auto-migration) must derive `keyHash` + `keyPrefix` here and pass
 * those to the Repository — the raw key is returned to the admin once
 * at issuance and then dropped.
 *
 * Plan: docs/superpowers/specs/2026-05-18-aao-gateway-mode-design.md
 * (Phase 2a, "Key format").
 */
import { createHash, randomBytes } from 'crypto';

export const KEY_PREFIX_HEAD = 'sk-aao-' as const;
/** Length of the random body (not counting the `sk-aao-` head). */
export const KEY_BODY_LEN = 32;
/**
 * Number of random bytes to draw for the key body. 23 bytes = 184 bits,
 * which fits in ≤ 31 base62 chars and is then padded to KEY_BODY_LEN
 * with no truncation. See module header for the bound derivation.
 */
export const KEY_BODY_BYTES = 23;
/** Total length of a well-formed raw key, including head. */
export const KEY_TOTAL_LEN = KEY_PREFIX_HEAD.length + KEY_BODY_LEN;
/**
 * Number of body characters surfaced in the human-readable prefix. We
 * include enough to disambiguate keys in admin UI lists (~62^6 = ~57B
 * possibilities for the visible head) while still hiding the bulk of
 * the entropy when a key is leaked via screenshot.
 */
export const KEY_PREFIX_BODY_LEN = 6;

const BASE62_ALPHABET = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
const BASE62_BASE = BigInt(BASE62_ALPHABET.length);

/**
 * Encode a byte buffer as a fixed-length base62 string by reading the
 * bytes as a single big integer. We pad with leading '0' so the output
 * length is stable for a given input length — important because the
 * `sk-aao-` prefix relies on a predictable suffix length.
 *
 * Exported for unit testing. Not part of the public key API.
 */
export function bytesToBase62(buf: Buffer, outLen: number): string {
  if (outLen <= 0) throw new Error('bytesToBase62: outLen must be positive');
  let n = 0n;
  for (const b of buf) n = (n << 8n) | BigInt(b);
  let out = '';
  if (n === 0n) {
    out = '0';
  } else {
    while (n > 0n) {
      const rem = Number(n % BASE62_BASE);
      out = BASE62_ALPHABET[rem]! + out;
      n = n / BASE62_BASE;
    }
  }
  if (out.length > outLen) {
    // Hard-fail rather than truncating the most-significant chars.
    // Truncation biases the leading digit (it can only land on values
    // whose absence shortens the encoding), defeating the uniform
    // distribution we want for key bodies. Callers must size the input
    // so the encoded length fits in outLen.
    throw new Error(
      `bytesToBase62: encoded length ${out.length} exceeds outLen ${outLen} ` +
        `(input ${buf.length} bytes is too large for the requested width)`,
    );
  }
  return out.padStart(outLen, '0');
}

/**
 * SHA-256 of the raw bearer string, hex-encoded. Fixed 64-char output;
 * safe to compare with crypto.timingSafeEqual after Buffer.from(..,'hex').
 */
export function hashKey(raw: string): string {
  return createHash('sha256').update(raw, 'utf8').digest('hex');
}

/**
 * Extract the human-readable head of a key (`sk-aao-XXXXXX`) used for
 * admin UI lists. Returns the empty string for inputs that don't start
 * with the expected prefix so we never claim a non-AAO key as one of
 * ours.
 */
export function deriveKeyPrefix(raw: string): string {
  if (typeof raw !== 'string') return '';
  if (!raw.startsWith(KEY_PREFIX_HEAD)) return '';
  // Some malformed keys might be shorter than head + body-len; slice
  // happily returns what's there in that case.
  return raw.slice(0, KEY_PREFIX_HEAD.length + KEY_PREFIX_BODY_LEN);
}

export interface GeneratedVirtualKey {
  /** Raw bearer to return to the admin once. Never persisted. */
  raw: string;
  /** SHA-256 hash to store in gateway_virtual_keys.key_hash. */
  hash: string;
  /** First `sk-aao-XXXXXX` chars; safe to surface in UI and logs. */
  prefix: string;
}

/**
 * Generate a fresh AAO virtual key. Uses 23 random bytes (184 bits),
 * encoded as a base62 body that is padded (never truncated) to a fixed
 * 32-character width. Total length 39 chars.
 *
 * The body is intentionally generated in one shot (not per-character)
 * so we draw from a uniform high-entropy distribution rather than
 * concatenating short rounds. The byte count is chosen so that the
 * encoded length never exceeds KEY_BODY_LEN; see the module header
 * for the bound and bytesToBase62 for the hard-fail regression guard.
 */
export function generateVirtualKey(): GeneratedVirtualKey {
  const raw = `${KEY_PREFIX_HEAD}${bytesToBase62(randomBytes(KEY_BODY_BYTES), KEY_BODY_LEN)}`;
  return {
    raw,
    hash: hashKey(raw),
    prefix: deriveKeyPrefix(raw),
  };
}
