// crypto.jsx — provably fair HMAC-SHA-256 + seed/nonce machinery.
// Browser SubtleCrypto. Server seed lives off the public hook surface; each
// round computes HMAC-SHA-256(key=server_seed, msg=client_seed:nonce) and
// derives the outcome from the resulting hex (audit HIGH-2).
//
// Rotation flow (audit HIGH-3): rotateSeed() reveals the active server seed
// to the user, generates a fresh seed, publishes the new commitment, resets
// nonce. The revealed seed is persisted to localStorage so the verifier can
// confirm past rounds even after a rotation or reload.

async function sha256Hex(s) {
  const buf = new TextEncoder().encode(s);
  const hash = await crypto.subtle.digest('SHA-256', buf);
  return Array.from(new Uint8Array(hash)).map(b=>b.toString(16).padStart(2,'0')).join('');
}

// HMAC-SHA-256 keyed by a hex string. Returns lowercase hex output.
// The hex-key is decoded into raw bytes (matches industry-standard Stake-style
// provably-fair implementations). Inputs are not user-controlled in the live
// path (server seed is randHex), but the verifier accepts arbitrary input and
// validates it at the boundary (audit MED-2).
async function hmacSha256Hex(keyHex, msg) {
  const keyBytes = new Uint8Array(keyHex.length / 2);
  for (let i = 0; i < keyBytes.length; i++) {
    keyBytes[i] = parseInt(keyHex.slice(i*2, i*2+2), 16);
  }
  const key = await crypto.subtle.importKey(
    'raw', keyBytes, { name: 'HMAC', hash: 'SHA-256' }, false, ['sign']
  );
  const sig = await crypto.subtle.sign('HMAC', key, new TextEncoder().encode(msg));
  return Array.from(new Uint8Array(sig)).map(b=>b.toString(16).padStart(2,'0')).join('');
}

function randHex(n=32) {
  const a = new Uint8Array(n);
  crypto.getRandomValues(a);
  return Array.from(a).map(b=>b.toString(16).padStart(2,'0')).join('');
}

// Convert a hex hash into a sequence of floats in [0,1).
// Divisor is 2^32 (not 2^32-1) so the interval is half-open: f === 1.0 is
// never produced. The closed-interval variant let `Math.floor(1.0 * 37) === 37`
// fire as a 1-in-2^32 OOB in roulette pocket selection (audit HIGH-5).
function hashToFloats(hex, n=8) {
  const out = [];
  for (let i = 0; i < n && (i*8 + 8) <= hex.length; i++) {
    const h = parseInt(hex.slice(i*8, i*8+8), 16);
    out.push(h / 0x100000000);
  }
  return out;
}

// Rejection-sampled `h % max` over a 32-bit window starting at `offset` hex
// chars. If the first sample lands in the truncation region, advance by 8
// hex chars and retry. Practical reject rate for max=13 is 9/2^32 ≈ 2.1e-9
// per draw — almost never fires (audit MED-4, MED-5, replaces the deleted
// biased `hashToInt`).
function unbiasedMod(hex, max, offset=0) {
  const cap = Math.floor(0x100000000 / max) * max;
  let off = offset;
  while ((off + 8) <= hex.length) {
    const v = parseInt(hex.slice(off, off+8), 16);
    if (v < cap) return v % max;
    off += 8;
  }
  // Fallback: hash is exhausted; accept the residual (with the same modulo
  // bias as before). Should never fire for 256-bit hashes given normal max
  // values; documented so it doesn't silently corrupt.
  const v = parseInt(hex.slice(offset, offset+8), 16);
  return v % max;
}

const FAIRNESS_HISTORY_KEY = 'run!fairnessHistory';
const FAIRNESS_REVEALED_KEY = 'run!revealedSeeds';

function loadPersistedHistory() {
  try {
    const raw = localStorage.getItem(FAIRNESS_HISTORY_KEY);
    if (!raw) return [];
    const parsed = JSON.parse(raw);
    return Array.isArray(parsed) ? parsed.slice(0, 50) : [];
  } catch { return []; }
}
function loadRevealedSeeds() {
  try {
    const raw = localStorage.getItem(FAIRNESS_REVEALED_KEY);
    if (!raw) return [];
    const parsed = JSON.parse(raw);
    return Array.isArray(parsed) ? parsed : [];
  } catch { return []; }
}

// Public seed state — single source of truth for the whole site.
// Nonce lives in a ref so two synchronous roll() calls in the same event tick
// get distinct values (audit HIGH-10). The state mirror exists only so UI
// elements that display the nonce re-render on advance.
function useFairness() {
  // Server seed lives in a ref so rotation can replace it atomically while
  // outstanding async calls (e.g. an in-flight roll) still hold the previous
  // value via captured closure. UI reads the commitment, not the seed.
  const serverSeedRef = React.useRef(randHex(32));
  const [serverHash, setServerHash] = React.useState('');
  const [clientSeed] = React.useState(() => randHex(32));  // 256 bits (audit LOW-3)
  const nonceRef = React.useRef(0);
  const [nonce, setNonce] = React.useState(0);
  const [history, setHistory] = React.useState(() => loadPersistedHistory());
  const [revealedSeeds, setRevealedSeeds] = React.useState(() => loadRevealedSeeds());
  // Commitment-ready flag — `roll` rejects until the initial SHA-256 of the
  // server seed has resolved and serverHash is published (audit HIGH-4).
  // Ref so synchronous gate-check reads the latest value without re-render lag.
  const readyRef = React.useRef(false);

  React.useEffect(() => {
    let cancelled = false;
    sha256Hex(serverSeedRef.current).then(h => {
      if (cancelled) return;
      setServerHash(h);
      readyRef.current = true;
    });
    return () => { cancelled = true; };
  }, []);

  // Persist history and revealed seeds on every change.
  React.useEffect(() => {
    try { localStorage.setItem(FAIRNESS_HISTORY_KEY, JSON.stringify(history)); } catch {}
  }, [history]);
  React.useEffect(() => {
    try { localStorage.setItem(FAIRNESS_REVEALED_KEY, JSON.stringify(revealedSeeds)); } catch {}
  }, [revealedSeeds]);

  // Roll: produce a hash for this round via HMAC-SHA-256, advance nonce,
  // record. serverSeed is NOT returned to the caller (audit MED-3).
  const roll = React.useCallback(async (game) => {
    if (!readyRef.current) {
      throw new Error('fairness commitment not yet published');
    }
    const n = nonceRef.current;
    nonceRef.current = n + 1;
    setNonce(nonceRef.current);
    const hex = await hmacSha256Hex(serverSeedRef.current, `${clientSeed}:${n}`);
    setHistory(h => [{ game, nonce:n, clientSeed, serverHash, hex, ts: Date.now() }, ...h].slice(0, 50));
    return { hex, nonce:n, clientSeed, serverHash };
  }, [clientSeed, serverHash]);

  // Rotate: reveal current seed, install fresh seed, recompute commitment,
  // reset nonce (audit HIGH-3).
  const rotateSeed = React.useCallback(async () => {
    if (!readyRef.current) return;
    const oldSeed = serverSeedRef.current;
    const oldCommitment = serverHash;
    const oldNonce = nonceRef.current;
    const newSeed = randHex(32);
    readyRef.current = false;
    serverSeedRef.current = newSeed;
    nonceRef.current = 0;
    setNonce(0);
    const newCommitment = await sha256Hex(newSeed);
    setServerHash(newCommitment);
    setRevealedSeeds(rs => [
      { seed: oldSeed, commitment: oldCommitment, lastNonce: oldNonce, revealedAt: Date.now() },
      ...rs,
    ].slice(0, 20));
    readyRef.current = true;
  }, [serverHash]);

  return { serverHash, clientSeed, nonce, history, revealedSeeds, roll, rotateSeed };
}

// Verify a recorded round in the user's browser.
// Accepts a round with explicit serverSeed; throws if any field is missing
// rather than silently substituting (audit LOW-4).
async function verifyRound(round) {
  if (!round || !round.serverSeed) throw new Error('serverSeed required to verify');
  if (!round.clientSeed || round.nonce == null) throw new Error('clientSeed and nonce required');
  const recomputed = await hmacSha256Hex(round.serverSeed, `${round.clientSeed}:${round.nonce}`);
  return recomputed === round.hex;
}

Object.assign(window, {
  sha256Hex, hmacSha256Hex, randHex, hashToFloats, unbiasedMod, useFairness, verifyRound,
});
