/**
 * @module universal/uuid
 * @description Framework-friendly UUID utilities used across Brainy — works in
 * Node, Bun, Deno, and browsers (relies only on the global Web Crypto API, with
 * pure-JS fallbacks).
 *
 * Three generators + helpers:
 * - {@link v4} — random UUID (legacy default).
 * - {@link v7} — time-ordered UUID (RFC 9562). The 8.0 default for new ids:
 *   lexicographically sortable by creation time, which keeps the id↔int mapper
 *   and any range scan locality-friendly.
 * - {@link v5} — deterministic namespaced UUID (RFC 4122, SHA-1). Used by the
 *   8.0 id-normalization layer to map a caller's non-UUID string key to a STABLE
 *   UUID, so a native engine (which requires UUID ids for its int mapper) always
 *   sees a valid UUID while the caller can keep using their natural key.
 */

/** Render 16 bytes as a canonical hyphenated UUID string. */
function bytesToUuid(b: Uint8Array): string {
  const h: string[] = []
  for (let i = 0; i < 16; i++) h.push(b[i].toString(16).padStart(2, '0'))
  return (
    `${h[0]}${h[1]}${h[2]}${h[3]}-${h[4]}${h[5]}-${h[6]}${h[7]}-` +
    `${h[8]}${h[9]}-${h[10]}${h[11]}${h[12]}${h[13]}${h[14]}${h[15]}`
  )
}

/** Parse a hyphenated UUID string into its 16 bytes. */
function uuidToBytes(uuid: string): Uint8Array {
  const hex = uuid.replace(/-/g, '')
  const b = new Uint8Array(16)
  for (let i = 0; i < 16; i++) b[i] = parseInt(hex.slice(i * 2, i * 2 + 2), 16)
  return b
}

/** Fill a byte array with cryptographically-strong (or Math.random fallback) randomness. */
function randomBytes(n: number): Uint8Array {
  const out = new Uint8Array(n)
  if (typeof crypto !== 'undefined' && crypto.getRandomValues) {
    crypto.getRandomValues(out)
  } else {
    for (let i = 0; i < n; i++) out[i] = Math.floor(Math.random() * 256)
  }
  return out
}

/**
 * @description Canonical UUID-format check (any version/variant). NOT a version
 * assertion — just "is this string shaped like a UUID".
 * @param value - The string to test.
 * @returns `true` if `value` matches the 8-4-4-4-12 hex UUID format.
 */
export function isUUID(value: string): boolean {
  return /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(value)
}

/**
 * @description Random (version 4) UUID.
 * @returns A random UUID string.
 */
export function v4(): string {
  if (typeof crypto !== 'undefined' && crypto.randomUUID) {
    return crypto.randomUUID()
  }
  return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
    const r = (Math.random() * 16) | 0
    const v = c === 'x' ? r : (r & 0x3) | 0x8
    return v.toString(16)
  })
}

/**
 * @description Time-ordered (version 7) UUID — 48-bit Unix-ms timestamp prefix
 * + random tail. Two ids minted in order sort in order as strings.
 * @returns A version-7 UUID string.
 */
export function v7(): string {
  const ms = Date.now()
  const b = new Uint8Array(16)
  // 48-bit big-endian millisecond timestamp.
  b[0] = Math.floor(ms / 0x10000000000) & 0xff
  b[1] = Math.floor(ms / 0x100000000) & 0xff
  b[2] = Math.floor(ms / 0x1000000) & 0xff
  b[3] = Math.floor(ms / 0x10000) & 0xff
  b[4] = Math.floor(ms / 0x100) & 0xff
  b[5] = ms & 0xff
  const rand = randomBytes(10)
  b[6] = (rand[0] & 0x0f) | 0x70 // version 7
  b[7] = rand[1]
  b[8] = (rand[2] & 0x3f) | 0x80 // variant 10xx
  for (let i = 9; i < 16; i++) b[i] = rand[i - 6] // rand[3..9]
  return bytesToUuid(b)
}

/** Left-rotate a 32-bit word. */
function rotl(n: number, s: number): number {
  return ((n << s) | (n >>> (32 - s))) >>> 0
}

/**
 * @description SHA-1 of a byte array → 20 bytes. Pure JS so it runs synchronously
 * everywhere (Web Crypto's digest is async-only). Used solely by {@link v5}.
 * @param msg - Input bytes.
 * @returns The 20-byte SHA-1 digest.
 */
function sha1(msg: Uint8Array): Uint8Array {
  const ml = msg.length * 8
  const withByte = msg.length + 1
  const padLen = withByte % 64 <= 56 ? 56 - (withByte % 64) : 120 - (withByte % 64)
  const total = msg.length + 1 + padLen + 8
  const buf = new Uint8Array(total)
  buf.set(msg, 0)
  buf[msg.length] = 0x80
  const hi = Math.floor(ml / 0x100000000)
  const lo = ml >>> 0
  buf[total - 8] = (hi >>> 24) & 0xff
  buf[total - 7] = (hi >>> 16) & 0xff
  buf[total - 6] = (hi >>> 8) & 0xff
  buf[total - 5] = hi & 0xff
  buf[total - 4] = (lo >>> 24) & 0xff
  buf[total - 3] = (lo >>> 16) & 0xff
  buf[total - 2] = (lo >>> 8) & 0xff
  buf[total - 1] = lo & 0xff

  let h0 = 0x67452301
  let h1 = 0xefcdab89
  let h2 = 0x98badcfe
  let h3 = 0x10325476
  let h4 = 0xc3d2e1f0

  const w = new Array<number>(80)
  for (let i = 0; i < total; i += 64) {
    for (let t = 0; t < 16; t++) {
      w[t] =
        ((buf[i + t * 4] << 24) |
          (buf[i + t * 4 + 1] << 16) |
          (buf[i + t * 4 + 2] << 8) |
          buf[i + t * 4 + 3]) >>>
        0
    }
    for (let t = 16; t < 80; t++) {
      w[t] = rotl(w[t - 3] ^ w[t - 8] ^ w[t - 14] ^ w[t - 16], 1)
    }
    let a = h0
    let b = h1
    let c = h2
    let d = h3
    let e = h4
    for (let t = 0; t < 80; t++) {
      let f: number
      let k: number
      if (t < 20) {
        f = (b & c) | (~b & d)
        k = 0x5a827999
      } else if (t < 40) {
        f = b ^ c ^ d
        k = 0x6ed9eba1
      } else if (t < 60) {
        f = (b & c) | (b & d) | (c & d)
        k = 0x8f1bbcdc
      } else {
        f = b ^ c ^ d
        k = 0xca62c1d6
      }
      const tmp = (rotl(a, 5) + (f >>> 0) + e + k + w[t]) >>> 0
      e = d
      d = c
      c = rotl(b, 30)
      b = a
      a = tmp
    }
    h0 = (h0 + a) >>> 0
    h1 = (h1 + b) >>> 0
    h2 = (h2 + c) >>> 0
    h3 = (h3 + d) >>> 0
    h4 = (h4 + e) >>> 0
  }

  const out = new Uint8Array(20)
  const hs = [h0, h1, h2, h3, h4]
  for (let i = 0; i < 5; i++) {
    out[i * 4] = (hs[i] >>> 24) & 0xff
    out[i * 4 + 1] = (hs[i] >>> 16) & 0xff
    out[i * 4 + 2] = (hs[i] >>> 8) & 0xff
    out[i * 4 + 3] = hs[i] & 0xff
  }
  return out
}

/**
 * @description Deterministic version-5 (SHA-1, namespaced) UUID. The same
 * `(name, namespace)` always yields the same UUID — the property the
 * id-normalization layer relies on to turn a stable string key into a stable
 * UUID id.
 * @param name - The name to hash (e.g. a caller-supplied string id).
 * @param namespace - A UUID-format namespace (defaults to {@link BRAINY_ID_NAMESPACE}).
 * @returns A version-5 UUID string.
 */
export function v5(name: string, namespace: string = BRAINY_ID_NAMESPACE): string {
  const ns = uuidToBytes(namespace)
  const nameBytes = new TextEncoder().encode(name)
  const data = new Uint8Array(16 + nameBytes.length)
  data.set(ns, 0)
  data.set(nameBytes, 16)
  const hash = sha1(data)
  const b = hash.slice(0, 16)
  b[6] = (b[6] & 0x0f) | 0x50 // version 5
  b[8] = (b[8] & 0x3f) | 0x80 // variant 10xx
  return bytesToUuid(b)
}

/**
 * @description The fixed namespace for Brainy's string→UUID normalization. A
 * stable constant (spells "brainy" in the first bytes) so the same caller string
 * maps to the same UUID across processes and machines.
 */
export const BRAINY_ID_NAMESPACE = '62726169-6e79-5000-8000-000000000000'

// Named export to match the `uuid` package API.
export { v4 as uuidv4, v7 as uuidv7, v5 as uuidv5 }

export default { v4, v7, v5, isUUID, BRAINY_ID_NAMESPACE }
