//! FIFA 17 Store pack-content generator (pure, seeded).
//!
//! Draws the cards a Store pack awards. It is a **pure function** of
//! `(pack definition, RNG, candidate pool)` — no IO, no Core, no catalogue
//! lookup — so it is deterministic under a seeded [`rand::Rng`] and trivially
//! unit-tested. The host owns the impure parts: it builds the candidate pool
//! (only card ids that resolve in BOTH the FIFA catalogue and Core content),
//! mints the drawn cards into Core, and shapes them onto the wire.
//!
//! ## Policy (real FUT 17 composition, DESIGNED odds)
//!
//! A pack draws [`PackDef::count`] cards split across rating tiers by the pack's
//! `n_bronze`/`n_silver`/`n_gold` composition (the same numbers the tile shows in
//! `packContentInfo`), drawing with replacement from the candidate pool. Each pick
//! is biased toward a special version with probability `special_chance` (a DESIGNED
//! placeholder — FUT 17 pack odds are unrecoverable). An empty tier falls back to
//! the whole pool so a draw is always possible even when the pool lacks that tier.
//! `FUT_PACK_MIX` (consumable/staff extras) is deliberately OMITTED — Core
//! candidates are player defs.

use rand::Rng;

use crate::fut::store_catalog::PackDef;

/// A candidate the host has already verified resolves in BOTH the FIFA catalogue
/// and Core content. Carries the full Core definition the shaper needs plus the
/// two draw-policy annotations (`gold` tier, `special` version) the host derives
/// from the catalogue (keeping this generator pure — it never reads a catalogue).
#[derive(Debug, Clone)]
pub struct GeneratedCandidate {
    /// Core card-definition id (resolves in the FIFA catalogue and Core content).
    pub card_id: String,
    pub rating: u8,
    pub position: String,
    pub nation: String,
    pub league: String,
    pub club: String,
    /// [pace, shooting, passing, dribbling, defending, physical].
    pub attributes: [u8; 6],
    /// Gold tier (host derives this as `rating >= 75`, the oracle's split point).
    pub gold: bool,
    /// Special version available (host derives this from the catalogue rareflag
    /// `> 1`); gated by [`PackDef::special_chance`].
    pub special: bool,
}

impl GeneratedCandidate {
    fn to_card(&self) -> GeneratedCard {
        GeneratedCard {
            card_id: self.card_id.clone(),
            rating: self.rating,
            position: self.position.clone(),
            nation: self.nation.clone(),
            league: self.league.clone(),
            club: self.club.clone(),
            attributes: self.attributes,
        }
    }
}

/// One card a pack awarded. Carries `card_id` (the Core definition the host mints
/// and shapes) plus the definition fields the shared item shaper needs. It is
/// NOT an owned instance yet — the host mints the Core instance id and allocates
/// the numeric wire id.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct GeneratedCard {
    pub card_id: String,
    pub rating: u8,
    pub position: String,
    pub nation: String,
    pub league: String,
    pub club: String,
    pub attributes: [u8; 6],
}

/// Draw a pack's cards from `pool` with the injected RNG. Pure and deterministic
/// under a seeded RNG. Returns an empty `Vec` (fail-closed) when the pool is empty
/// or the pack awards no cards.
///
/// Draws the pack's per-tier composition (`n_gold` gold-tier, `n_silver` silver,
/// `n_bronze` bronze), biasing each pick toward a special with `special_chance`.
/// An empty tier falls back to the whole pool (so a draw is always possible).
pub fn generate_pack_contents(
    pack: &PackDef,
    rng: &mut impl Rng,
    pool: &[GeneratedCandidate],
) -> Vec<GeneratedCard> {
    if pool.is_empty() || pack.count() == 0 {
        return Vec::new();
    }
    let all: Vec<&GeneratedCandidate> = pool.iter().collect();
    let gold: Vec<&GeneratedCandidate> = pool.iter().filter(|c| c.rating >= 75).collect();
    let silver: Vec<&GeneratedCandidate> = pool
        .iter()
        .filter(|c| (65..75).contains(&c.rating))
        .collect();
    let bronze: Vec<&GeneratedCandidate> = pool.iter().filter(|c| c.rating < 65).collect();

    let mut out = Vec::with_capacity(pack.count() as usize);
    draw_tier(&mut out, &gold, &all, pack.n_gold, pack.special_chance, rng);
    draw_tier(
        &mut out,
        &silver,
        &all,
        pack.n_silver,
        pack.special_chance,
        rng,
    );
    draw_tier(
        &mut out,
        &bronze,
        &all,
        pack.n_bronze,
        pack.special_chance,
        rng,
    );
    out
}

/// Draw `n` cards from `tier` — or the whole-pool `fallback` when `tier` is empty —
/// biasing each pick toward a special version with probability `chance`. Either the
/// special or normal partition falls back to the tier when empty.
fn draw_tier(
    out: &mut Vec<GeneratedCard>,
    tier: &[&GeneratedCandidate],
    fallback: &[&GeneratedCandidate],
    n: u64,
    chance: f64,
    rng: &mut impl Rng,
) {
    if n == 0 {
        return;
    }
    let src: &[&GeneratedCandidate] = if tier.is_empty() { fallback } else { tier };
    if src.is_empty() {
        return;
    }
    let special: Vec<&GeneratedCandidate> = src.iter().copied().filter(|c| c.special).collect();
    let normal: Vec<&GeneratedCandidate> = src.iter().copied().filter(|c| !c.special).collect();
    let chance = chance.clamp(0.0, 1.0);
    for _ in 0..n {
        let want_special = chance > 0.0 && rng.gen_bool(chance);
        let sub: &[&GeneratedCandidate] = if want_special && !special.is_empty() {
            &special
        } else if !want_special && !normal.is_empty() {
            &normal
        } else {
            src
        };
        let pick = sub[rng.gen_range(0..sub.len())];
        out.push(pick.to_card());
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use rand::rngs::StdRng;
    use rand::SeedableRng;

    fn cand(card: &str, rating: u8, gold: bool, special: bool) -> GeneratedCandidate {
        GeneratedCandidate {
            card_id: card.into(),
            rating,
            position: "ST".into(),
            nation: "Brazil".into(),
            league: "Premier League".into(),
            club: "Arsenal".into(),
            attributes: [rating; 6],
            gold,
            special,
        }
    }

    /// A mixed pool: gold specials, gold normals, and a bronze tier.
    fn pool() -> Vec<GeneratedCandidate> {
        vec![
            cand("g-sp-1", 90, true, true),
            cand("g-sp-2", 88, true, true),
            cand("g-1", 84, true, false),
            cand("g-2", 82, true, false),
            cand("g-3", 79, true, false),
            cand("b-1", 64, false, false),
            cand("b-2", 62, false, false),
        ]
    }

    fn pack(id: u64, n_bronze: u64, n_silver: u64, n_gold: u64, special_chance: f64) -> PackDef {
        PackDef {
            id,
            name: "Test Pack",
            price: 1000,
            n_bronze,
            n_silver,
            n_gold,
            rares: 0,
            category: if n_gold > 0 {
                "gold"
            } else if n_silver > 0 {
                "silver"
            } else {
                "bronze"
            },
            special_chance,
            owned_only: false,
        }
    }

    #[test]
    fn same_seed_same_output() {
        let pool = pool();
        let p = pack(5, 0, 0, 7, 0.3);
        let mut a = StdRng::seed_from_u64(42);
        let mut b = StdRng::seed_from_u64(42);
        assert_eq!(
            generate_pack_contents(&p, &mut a, &pool),
            generate_pack_contents(&p, &mut b, &pool)
        );
    }

    #[test]
    fn different_seeds_can_diverge() {
        let pool = pool();
        let p = pack(5, 0, 0, 7, 0.3);
        let a = generate_pack_contents(&p, &mut StdRng::seed_from_u64(1), &pool);
        let b = generate_pack_contents(&p, &mut StdRng::seed_from_u64(999), &pool);
        // Not a hard guarantee, but with this pool/count the two seeds differ.
        assert_ne!(a, b);
    }

    #[test]
    fn count_is_exact_and_all_cards_from_pool() {
        let pool = pool();
        let ids: std::collections::HashSet<&str> =
            pool.iter().map(|c| c.card_id.as_str()).collect();
        for &n in &[1u64, 5, 7, 11] {
            let p = pack(6, 0, 0, n, 0.08);
            let cards = generate_pack_contents(&p, &mut StdRng::seed_from_u64(n), &pool);
            assert_eq!(cards.len() as u64, n);
            for c in &cards {
                assert!(
                    ids.contains(c.card_id.as_str()),
                    "drew unknown card {}",
                    c.card_id
                );
            }
        }
    }

    #[test]
    fn gold_pack_draws_only_gold_tier() {
        let pool = pool();
        let p = pack(5, 0, 0, 20, 0.03);
        let cards = generate_pack_contents(&p, &mut StdRng::seed_from_u64(7), &pool);
        assert!(
            cards.iter().all(|c| c.rating >= 75),
            "gold pack drew a bronze card"
        );
    }

    #[test]
    fn bronze_pack_draws_only_bronze_tier() {
        let pool = pool();
        let p = pack(1, 20, 0, 0, 0.005);
        let cards = generate_pack_contents(&p, &mut StdRng::seed_from_u64(7), &pool);
        assert!(
            cards.iter().all(|c| c.rating < 75),
            "bronze pack drew a gold card"
        );
    }

    #[test]
    fn special_chance_one_draws_only_specials() {
        let pool = pool();
        let special_ids: std::collections::HashSet<&str> = pool
            .iter()
            .filter(|c| c.special)
            .map(|c| c.card_id.as_str())
            .collect();
        let p = pack(7, 0, 0, 11, 1.0);
        let cards = generate_pack_contents(&p, &mut StdRng::seed_from_u64(3), &pool);
        assert!(cards
            .iter()
            .all(|c| special_ids.contains(c.card_id.as_str())));
    }

    #[test]
    fn empty_pool_fails_closed() {
        let p = pack(5, 0, 0, 7, 0.03);
        assert!(generate_pack_contents(&p, &mut StdRng::seed_from_u64(1), &[]).is_empty());
    }
}
