//! Deterministic, reproducible RNG — **vendored verbatim** from
//! `reikhelm-core/src/rng.rs` (spec §10), with one additive local extension:
//! [`Rng::chance_bp`], an integer-threshold probability check so that *no*
//! `f64` ever touches the combat hot path.
//!
//! [`Rng`] wraps [`rand_chacha::ChaCha8Rng`] — a portable, byte-stable stream
//! cipher RNG. The same seed yields the same sequence on every machine and
//! every run; we never touch `thread_rng`/`rand::random()`.
//!
//! The keystone feature is [`Rng::fork`]: it derives an *independent* child
//! stream from the parent's **basis seed** plus a label. Forking does not draw
//! from or otherwise mutate the parent (`fork(&self)`), so a child stream is a
//! pure function of `(basis_seed, label)` — independent of how many values were
//! pulled from the parent first. The combat engine leans on this to key every
//! roll by `"actor{id}:{roll_kind}:tick{t}"`, which is what makes "change one
//! knob, observe the isolated delta" hold: adding an effect to one ability
//! never reshuffles another actor's or another tick's stream.

use rand_chacha::ChaCha8Rng;
// `rand_core` is not a direct dependency; reach its traits through the
// re-export on `rand_chacha` so the two stay version-locked. `Rng` carries
// `next_u64`; `SeedableRng` carries `seed_from_u64`.
use rand_chacha::rand_core::{Rng as _, SeedableRng};

/// A deterministic random number generator backed by ChaCha8.
///
/// Construct one with [`Rng::from_seed`], draw values with [`range`](Rng::range),
/// [`chance`](Rng::chance), [`chance_bp`](Rng::chance_bp), [`choose`](Rng::choose)
/// and [`shuffle`](Rng::shuffle), and derive isolated child streams with
/// [`fork`](Rng::fork).
#[derive(Clone, Debug)]
pub struct Rng {
    /// The stable basis used for all forks: the seed this `Rng` was built from.
    /// Forking mixes *this* (never the live ChaCha state) with the label, so a
    /// fork is independent of how many draws the parent has taken.
    basis_seed: u64,
    /// The live ChaCha8 stream that actually produces random words.
    inner: ChaCha8Rng,
}

impl Rng {
    /// Create an `Rng` from a 64-bit seed, storing the seed as the fork basis.
    ///
    /// The seed is expanded into ChaCha8's 256-bit key by ChaCha8's own
    /// portable `seed_from_u64`, so two `Rng`s built from the same `u64`
    /// produce identical streams everywhere.
    pub fn from_seed(seed: u64) -> Self {
        Self {
            basis_seed: seed,
            inner: ChaCha8Rng::seed_from_u64(seed),
        }
    }

    /// Derive an independent child `Rng` from a stable basis (this `Rng`'s
    /// originating seed) combined with `label`.
    ///
    /// This **does not** advance or otherwise mutate `self` (note `&self`), and
    /// the result depends only on `(self.basis_seed, label)` — never on how many
    /// values have already been drawn from `self`. Distinct labels (including
    /// `"name#0"` vs `"name#1"`) yield distinct streams.
    ///
    /// The label→seed mix is the explicit, fixed integer routine in
    /// [`mix_seed`] (splitmix64 + FNV-1a), chosen because it is stable across
    /// runs and crate versions. We deliberately avoid
    /// `std::collections::hash_map::DefaultHasher`, whose output is
    /// unspecified and version-unstable (spec §10).
    pub fn fork(&self, label: &str) -> Rng {
        Rng::from_seed(mix_seed(self.basis_seed, label))
    }

    /// Draw a `u64` of raw randomness from the live stream.
    fn next_u64(&mut self) -> u64 {
        self.inner.next_u64()
    }

    /// Return an integer in the **half-open** range `[lo, hi)` — `lo` inclusive,
    /// `hi` exclusive (so `range(0, 3)` yields one of `0, 1, 2`).
    ///
    /// If `hi <= lo` the range is empty; we return `lo` rather than panicking.
    /// Uniformity is achieved with Lemire-style rejection sampling over the
    /// `u64` span, so the result is unbiased across the whole range.
    pub fn range(&mut self, lo: i32, hi: i32) -> i32 {
        if hi <= lo {
            return lo;
        }
        // Width fits in u64: (hi - lo) where hi > lo and both are i32.
        let span = (hi as i64 - lo as i64) as u64;
        lo + self.below(span) as i32
    }

    /// Uniform integer in `[0, span)` for `span > 0`, via rejection sampling to
    /// avoid modulo bias. `span` is assumed non-zero by callers.
    fn below(&mut self, span: u64) -> u64 {
        // Largest multiple of `span` that fits in u64; anything at or above the
        // threshold would skew the distribution, so we reject and redraw.
        let threshold = u64::MAX - (u64::MAX % span);
        loop {
            let x = self.next_u64();
            if x < threshold {
                return x % span;
            }
        }
    }

    /// Return `true` with probability `p`.
    ///
    /// `p` is clamped to `[0.0, 1.0]`. The roll is a single `u64 → f64`
    /// unit-interval divide-and-compare, which *is* IEEE-deterministic across
    /// platforms (one division, one compare). Combat code prefers
    /// [`chance_bp`](Rng::chance_bp) to keep the hot path integer-only, but this
    /// remains available and identical to the upstream contract.
    pub fn chance(&mut self, p: f64) -> bool {
        if p <= 0.0 {
            return false;
        }
        if p >= 1.0 {
            return true;
        }
        // Map the next u64 into a uniform f64 in [0, 1) and compare.
        // 53 bits gives the full mantissa precision of an f64.
        let bits = self.next_u64() >> 11; // top 53 bits
        let unit = bits as f64 / (1u64 << 53) as f64;
        unit < p
    }

    /// **Local extension (combat-core):** integer-only probability check.
    ///
    /// Returns `true` with probability `p_bp / 10_000` (basis points). This is
    /// the integer-threshold `chance` variant the spec sanctions (§10): it
    /// compares a fresh `u64` against `p_bp` scaled across the full `u64` span,
    /// so no `f64` is involved at all. `p_bp <= 0` is always `false`,
    /// `p_bp >= 10_000` is always `true`.
    ///
    /// Scaling: a draw is a "hit" when `draw < p_bp * (2^64 / 10_000)`. We use
    /// `u128` for the threshold product so it cannot overflow, then truncate the
    /// comparison back into `u64` space. The mapping is monotonic and exact at
    /// the endpoints, which is all a fair Bernoulli trial needs.
    pub fn chance_bp(&mut self, p_bp: i64) -> bool {
        if p_bp <= 0 {
            return false;
        }
        if p_bp >= 10_000 {
            return true;
        }
        // threshold = floor(p_bp / 10_000 * 2^64), computed without overflow.
        let span = 1u128 << 64;
        let threshold = (span * p_bp as u128) / 10_000u128; // in [0, 2^64)
        (self.next_u64() as u128) < threshold
    }

    /// Return a reference to a uniformly chosen element of `items`, or `None`
    /// if the slice is empty.
    pub fn choose<'a, T>(&mut self, items: &'a [T]) -> Option<&'a T> {
        if items.is_empty() {
            return None;
        }
        let idx = self.below(items.len() as u64) as usize;
        items.get(idx)
    }

    /// Shuffle `items` in place into a uniformly random permutation
    /// (in-place Fisher–Yates over our own [`range`]). Slices of length < 2 are
    /// left unchanged.
    pub fn shuffle<T>(&mut self, items: &mut [T]) {
        let len = items.len();
        if len < 2 {
            return;
        }
        for i in (1..len).rev() {
            let j = self.below((i + 1) as u64) as usize;
            items.swap(i, j);
        }
    }
}

/// Mix a 64-bit basis seed with a label into a fresh 64-bit seed for [`Rng::fork`].
///
/// This is the load-bearing determinism primitive (spec §10), spelled out
/// explicitly with fixed integer arithmetic — no `DefaultHasher`.
///
/// Recipe: (1) splitmix64-scramble the basis; (2) fold each label byte with the
/// FNV-1a step (xor then multiply by the 64-bit FNV prime), making the result
/// order-sensitive so `"name#0"`/`"name#1"` diverge; (3) one final splitmix64
/// finalizer to avalanche.
fn mix_seed(basis: u64, label: &str) -> u64 {
    const FNV_PRIME: u64 = 0x0000_0100_0000_01B3;

    let mut state = splitmix64(basis);
    for &byte in label.as_bytes() {
        state ^= byte as u64;
        state = state.wrapping_mul(FNV_PRIME);
    }
    splitmix64(state)
}

/// One round of the splitmix64 finalizer: a fixed, portable integer avalanche.
fn splitmix64(seed: u64) -> u64 {
    let mut z = seed.wrapping_add(0x9E37_79B9_7F4A_7C15);
    z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
    z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
    z ^ (z >> 31)
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn same_seed_reproduces_sequence() {
        let mut a = Rng::from_seed(0xABCD_1234);
        let mut b = Rng::from_seed(0xABCD_1234);
        let seq_a: Vec<i32> = (0..64).map(|_| a.range(0, 1000)).collect();
        let seq_b: Vec<i32> = (0..64).map(|_| b.range(0, 1000)).collect();
        assert_eq!(seq_a, seq_b);
    }

    #[test]
    fn fork_is_order_independent() {
        let parent = Rng::from_seed(99);
        let mut early = parent.fork("a");
        let mut drained = parent.clone();
        for _ in 0..50 {
            let _ = drained.range(0, 100);
        }
        let mut late = drained.fork("a");
        let early_seq: Vec<i32> = (0..32).map(|_| early.range(0, 10_000)).collect();
        let late_seq: Vec<i32> = (0..32).map(|_| late.range(0, 10_000)).collect();
        assert_eq!(early_seq, late_seq, "fork must depend only on basis seed + label");
    }

    #[test]
    fn fork_does_not_mutate_parent() {
        let mut parent = Rng::from_seed(7);
        let before: Vec<i32> = {
            let mut probe = parent.clone();
            (0..16).map(|_| probe.range(0, 1000)).collect()
        };
        let _child = parent.fork("whatever");
        let after: Vec<i32> = (0..16).map(|_| parent.range(0, 1000)).collect();
        assert_eq!(before, after);
    }

    #[test]
    fn distinct_labels_diverge() {
        let parent = Rng::from_seed(42);
        let mut a = parent.fork("a");
        let mut b = parent.fork("b");
        let seq_a: Vec<i32> = (0..32).map(|_| a.range(0, 1_000_000)).collect();
        let seq_b: Vec<i32> = (0..32).map(|_| b.range(0, 1_000_000)).collect();
        assert_ne!(seq_a, seq_b);
    }

    /// The integer-threshold `chance_bp` saturates at the endpoints and lands
    /// near its nominal rate over many draws.
    #[test]
    fn chance_bp_saturates_and_is_fair() {
        let mut rng = Rng::from_seed(9);
        assert!(!rng.chance_bp(0));
        assert!(!rng.chance_bp(-100));
        assert!(rng.chance_bp(10_000));
        assert!(rng.chance_bp(20_000));

        let mut a = Rng::from_seed(2);
        let hits = (0..10_000).filter(|_| a.chance_bp(5_000)).count();
        assert!((4_700..5_300).contains(&hits), "0.5 chance_bp produced {hits}/10000");

        // A low rate should be rare but non-zero over enough draws.
        let mut b = Rng::from_seed(3);
        let rare = (0..100_000).filter(|_| b.chance_bp(100)).count(); // 1%
        assert!((700..1_300).contains(&rare), "1% chance_bp produced {rare}/100000");
    }

    /// `chance_bp` is reproducible under a fixed seed.
    #[test]
    fn chance_bp_reproduces() {
        let mut a = Rng::from_seed(123);
        let mut b = Rng::from_seed(123);
        let sa: Vec<bool> = (0..200).map(|_| a.chance_bp(3_333)).collect();
        let sb: Vec<bool> = (0..200).map(|_| b.chance_bp(3_333)).collect();
        assert_eq!(sa, sb);
    }
}
