//! FIFA17 MY CLUB stat set (`GET …/club/stats/{year,consumables}`), computed
//! from OpenFUT Core's authoritative owned inventory.
//!
//! Faithful port of the Python oracle's `fut_club_stats.py` (`global_counts` +
//! `context_rows` + `stats_body`), which is itself censused from CardsDLL
//! (`FUN_18012fd40` atom table). The body is `{"stat":[{contextId,contextValue,
//! type,typeValue}, …]}`:
//!   * a GLOBAL bucket (contextId 1, contextValue 0) with player tier counts,
//!     staff-by-family, consumables-by-family, and honest zeros for club items;
//!   * per-NATION buckets (contextId 3, contextValue = nation id) with the tier
//!     counts the MY CLUB summary panel sums into PLAYERS_EMPLOYED.
//!
//! Unlike the oracle (which counts its own stale profile + a synthetic consumable
//! shelf), this counts Core — so staff and consumable families reflect the real
//! imported content. Unrecognized atoms are inert in the client, so this is a
//! low-risk cosmetic surface; the tier/staff/consumable atoms are the ones the
//! screen reads and they are Core-accurate here.

use std::collections::BTreeMap;

use serde_json::{json, Value};

use crate::fut::content_taxonomy::{consumable_family, ContentKind};

/// One owned item, already classified from the catalog + entity tables by the
/// host. `subtype`/`rare` come from the FIFA catalog; `nation_id`/`league_id`/
/// `team_id` from the reverse entity resolver (None = unresolved, bucket skipped).
#[derive(Debug, Clone)]
pub struct ClubStatInput {
    pub kind: ContentKind,
    pub subtype: i64,
    pub rating: i64,
    pub rare: bool,
    pub nation_id: Option<i64>,
    pub league_id: Option<i64>,
    pub team_id: Option<i64>,
}

/// Which entity the per-context (`contextId 3`) buckets are keyed by — the FIFA
/// `MY CLUB` sub-screen selector (`fut_club_stats.py::context_rows`):
///   * `Nation` — the default screen (year/consumables/club/newcards): nation buckets.
///   * `League` — URL `club/stats/country/<id>`: league (leagueId) buckets, tier stats.
///   * `Team`   — URL `club/stats/league/<id>`: team (teamid) buckets, players/kits/badge.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ContextField {
    Nation,
    League,
    Team,
}

// Stat ids (CardsDLL atom table, fut_club_stats.py VOCAB).
const S_PLAYERS: i64 = 0x01;
const S_BRONZE: i64 = 0x02;
const S_SILVER: i64 = 0x03;
const S_GOLD: i64 = 0x04;
const S_RARE: i64 = 0x05;
const S_STAFF: i64 = 0x0A;
const S_CONSUMABLES: i64 = 0x3C;
const S_KITS: i64 = 0x28;
const S_BADGES: i64 = 0x2D;

/// cardsubtypeid (staff family) -> stat id (STAFF_SUBTYPE_STAT).
fn staff_stat(subtype: i64) -> Option<i64> {
    match subtype {
        4 => Some(0x0B), // manager
        5 => Some(0x0C), // head coach
        6 => Some(0x0D), // GK coach
        7 => Some(0x0E), // physio
        8 => Some(0x0F), // fitness coach
        _ => None,
    }
}

/// consumable family `kind` -> stat id (CONSUMABLE_KIND_STAT).
fn consumable_stat(kind: &str) -> Option<i64> {
    Some(match kind {
        "player_contract" => 0x42,
        "manager_contract" => 0x47,
        "healing" => 0x41,
        "player_fitness" => 0x44,
        "squad_fitness" => 0x4A,
        "gk_training" => 0x46,
        "player_training" => 0x43,
        "position_mod" => 0x45,
        "player_playstyle" => 0x4B,
        "gk_playstyle" => 0x4C,
        "manager_league" => 0x4D,
        "manager_formation_mod" | "formation_mod" => 0x48,
        _ => return None,
    })
}

/// The JSON `type` atom name for a stat id (VOCAB). Only the ids this module
/// emits are mapped; an unmapped id would panic (guards a transcription slip).
fn vocab(stat_id: i64) -> &'static str {
    match stat_id {
        0x01 => "players",
        0x02 => "playersBronze",
        0x03 => "playersSilver",
        0x04 => "playersGold",
        0x05 => "rarePlayers",
        0x0A => "staff",
        0x0B => "staffManager",
        0x0C => "staffHeadCoach",
        0x0D => "staffGKCoach",
        0x0E => "staffPhysio",
        0x0F => "staffFitnessCoach",
        0x14 => "stadia",
        0x1E => "balls",
        0x28 => "kits",
        0x29 => "kitsHome",
        0x2A => "kitsAway",
        0x2D => "badges",
        0x2E => "badgeDBid",
        0x2F => "leagueLogos",
        0x32 => "trophies",
        0x33 => "trophiesOffline",
        0x34 => "trophiesOnline",
        0x35 => "trophiesFeaturedOffline",
        0x36 => "trophiesFeaturedOnline",
        0x37 => "trophiesSeasonOffline",
        0x38 => "trophiesSeasonOnline",
        0x3C => "consumables",
        0x41 => "consumablesHealing",
        0x42 => "consumablesContractPlayer",
        0x43 => "consumablesTrainingPlayer",
        0x44 => "consumablesFitnessPlayer",
        0x45 => "consumablesPosition",
        0x46 => "consumablesTrainingGk",
        0x47 => "consumablesContractManager",
        0x48 => "consumablesFormationManager",
        0x49 => "consumablesTrainingManager",
        0x4A => "consumablesFitnessTeam",
        0x4B => "consumablesTrainingPlayerPlayStyle",
        0x4C => "consumablesTrainingGkPlayStyle",
        0x4D => "consumablesTrainingManagerLeagueModifier",
        other => panic!("club_stats: unmapped stat id {other:#x}"),
    }
}

fn row(context_id: i64, context_value: i64, stat_id: i64, value: i64) -> Value {
    json!({
        "contextId": context_id,
        "contextValue": context_value,
        "type": vocab(stat_id),
        "typeValue": value,
    })
}

fn is_player(i: &ClubStatInput) -> bool {
    matches!(i.kind, ContentKind::Player)
}

/// Build the full `{"stat":[…]}` body for a club/stats screen — the global bucket
/// (identical for every mode) plus per-context buckets keyed by `ctx`
/// (nation / league / team), mirroring `fut_club_stats.py::stats_body`.
pub fn club_stats_body(items: &[ClubStatInput], ctx: ContextField) -> Value {
    // ---- global bucket (contextId 1, contextValue 0), sorted by stat id ----
    let mut g: BTreeMap<i64, i64> = BTreeMap::new();
    let players: Vec<&ClubStatInput> = items.iter().filter(|i| is_player(i)).collect();
    g.insert(S_PLAYERS, players.len() as i64);
    g.insert(
        S_GOLD,
        players.iter().filter(|i| i.rating >= 75).count() as i64,
    );
    g.insert(
        S_SILVER,
        players
            .iter()
            .filter(|i| (65..75).contains(&i.rating))
            .count() as i64,
    );
    g.insert(
        S_BRONZE,
        players
            .iter()
            .filter(|i| i.rating > 0 && i.rating < 65)
            .count() as i64,
    );
    g.insert(S_RARE, players.iter().filter(|i| i.rare).count() as i64);

    // staff per family + total
    for sid in [0x0B, 0x0C, 0x0D, 0x0E, 0x0F] {
        g.insert(sid, 0);
    }
    let mut staff_total = 0i64;
    for it in items
        .iter()
        .filter(|i| matches!(i.kind, ContentKind::Staff))
    {
        if let Some(sid) = staff_stat(it.subtype) {
            *g.get_mut(&sid).unwrap() += 1;
            staff_total += 1;
        }
    }
    g.insert(S_STAFF, staff_total);

    // consumables per family + total
    for sid in [
        0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4A, 0x4B, 0x4C, 0x4D,
    ] {
        g.insert(sid, 0);
    }
    let mut cons_total = 0i64;
    for it in items
        .iter()
        .filter(|i| matches!(i.kind, ContentKind::Consumable))
    {
        cons_total += 1;
        if let Some((kind, _label)) = consumable_family(it.subtype) {
            if let Some(sid) = consumable_stat(kind) {
                *g.entry(sid).or_insert(0) += 1;
            }
        }
    }
    g.insert(S_CONSUMABLES, cons_total);

    // club items: honest zeros (Core holds none; each is read by some panel).
    for sid in [
        0x14, 0x1E, 0x28, 0x29, 0x2A, 0x2D, 0x2E, 0x2F, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38,
    ] {
        g.entry(sid).or_insert(0);
    }

    let mut stat: Vec<Value> = g.iter().map(|(sid, v)| row(1, 0, *sid, *v)).collect();

    // ---- per-context buckets (contextId 3, contextValue = entity id) ----
    // Nation/League read the tier set (gold/silver/bronze/rare/kits/badges);
    // Team (the league screen) reads players/kits/badgeDBid. Mirrors context_rows.
    let mut by_ctx: BTreeMap<i64, Vec<&ClubStatInput>> = BTreeMap::new();
    for p in &players {
        let id = match ctx {
            ContextField::Nation => p.nation_id,
            ContextField::League => p.league_id,
            ContextField::Team => p.team_id,
        };
        if let Some(id) = id {
            by_ctx.entry(id).or_default().push(p);
        }
    }
    for (cid, sel) in &by_ctx {
        if ctx == ContextField::Team {
            stat.push(row(3, *cid, S_PLAYERS, sel.len() as i64));
            stat.push(row(3, *cid, S_KITS, 0));
            stat.push(row(3, *cid, 0x2E, 0)); // badgeDBid
        } else {
            let gold = sel.iter().filter(|i| i.rating >= 75).count() as i64;
            let silver = sel.iter().filter(|i| (65..75).contains(&i.rating)).count() as i64;
            let bronze = sel.iter().filter(|i| i.rating > 0 && i.rating < 65).count() as i64;
            let rare = sel.iter().filter(|i| i.rare).count() as i64;
            stat.push(row(3, *cid, S_GOLD, gold));
            stat.push(row(3, *cid, S_SILVER, silver));
            stat.push(row(3, *cid, S_BRONZE, bronze));
            stat.push(row(3, *cid, S_RARE, rare));
            stat.push(row(3, *cid, S_KITS, 0));
            stat.push(row(3, *cid, S_BADGES, 0));
        }
    }

    json!({ "stat": stat })
}

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

    fn player(rating: i64, rare: bool, nation: Option<i64>) -> ClubStatInput {
        ClubStatInput {
            kind: ContentKind::Player,
            subtype: 0,
            rating,
            rare,
            nation_id: nation,
            league_id: None,
            team_id: None,
        }
    }
    fn staff(subtype: i64) -> ClubStatInput {
        ClubStatInput {
            kind: ContentKind::Staff,
            subtype,
            rating: 0,
            rare: false,
            nation_id: None,
            league_id: None,
            team_id: None,
        }
    }
    fn consumable(subtype: i64) -> ClubStatInput {
        ClubStatInput {
            kind: ContentKind::Consumable,
            subtype,
            rating: 0,
            rare: false,
            nation_id: None,
            league_id: None,
            team_id: None,
        }
    }

    fn global(body: &Value) -> std::collections::HashMap<String, i64> {
        body["stat"]
            .as_array()
            .unwrap()
            .iter()
            .filter(|r| r["contextId"] == 1)
            .map(|r| {
                (
                    r["type"].as_str().unwrap().to_string(),
                    r["typeValue"].as_i64().unwrap(),
                )
            })
            .collect()
    }

    #[test]
    fn tiers_and_rare_counted() {
        let items = vec![
            player(90, true, Some(52)),
            player(70, true, Some(52)),
            player(60, false, Some(21)),
        ];
        let g = global(&club_stats_body(&items, ContextField::Nation));
        assert_eq!(g["players"], 3);
        assert_eq!(g["playersGold"], 1);
        assert_eq!(g["playersSilver"], 1);
        assert_eq!(g["playersBronze"], 1);
        assert_eq!(g["rarePlayers"], 2);
    }

    #[test]
    fn staff_by_family() {
        let items = vec![staff(6), staff(8), staff(8)]; // 1 gk coach, 2 fitness
        let g = global(&club_stats_body(&items, ContextField::Nation));
        assert_eq!(g["staffGKCoach"], 1);
        assert_eq!(g["staffFitnessCoach"], 2);
        assert_eq!(g["staff"], 3);
        assert_eq!(g["staffManager"], 0);
    }

    #[test]
    fn consumables_by_family() {
        // 54 gk_training, 201 player_contract, 217 healing, 258 player_playstyle
        let items = vec![
            consumable(54),
            consumable(201),
            consumable(217),
            consumable(258),
        ];
        let g = global(&club_stats_body(&items, ContextField::Nation));
        assert_eq!(g["consumables"], 4);
        assert_eq!(g["consumablesTrainingGk"], 1);
        assert_eq!(g["consumablesContractPlayer"], 1);
        assert_eq!(g["consumablesHealing"], 1);
        assert_eq!(g["consumablesTrainingPlayerPlayStyle"], 1);
    }

    #[test]
    fn nation_buckets_emitted_and_players_excludes_nonplayers() {
        let items = vec![
            player(90, true, Some(52)),
            player(80, false, Some(52)),
            staff(8),
        ];
        let body = club_stats_body(&items, ContextField::Nation);
        let g = global(&body);
        assert_eq!(g["players"], 2, "staff not counted as player");
        let buckets: Vec<&Value> = body["stat"]
            .as_array()
            .unwrap()
            .iter()
            .filter(|r| r["contextId"] == 3 && r["contextValue"] == 52)
            .collect();
        // gold, silver, bronze, rare, kits, badges
        assert_eq!(buckets.len(), 6);
        let gold = buckets.iter().find(|r| r["type"] == "playersGold").unwrap();
        assert_eq!(gold["typeValue"], 2);
    }

    #[test]
    fn honest_zero_club_items_present() {
        let g = global(&club_stats_body(
            &[player(90, false, None)],
            ContextField::Nation,
        ));
        for atom in [
            "stadia",
            "balls",
            "kits",
            "badges",
            "trophies",
            "leagueLogos",
        ] {
            assert_eq!(g[atom], 0, "{atom} present as honest zero");
        }
    }

    #[test]
    fn league_and_team_context_modes() {
        let mut a = player(90, true, Some(52));
        a.league_id = Some(13);
        a.team_id = Some(240);
        let mut b = player(60, false, Some(52));
        b.league_id = Some(13);
        b.team_id = Some(9);
        let items = vec![a, b];

        // country screen -> league (leagueId) buckets, tier set (6 rows).
        let body = club_stats_body(&items, ContextField::League);
        let league_rows: Vec<&Value> = body["stat"]
            .as_array()
            .unwrap()
            .iter()
            .filter(|r| r["contextId"] == 3 && r["contextValue"] == 13)
            .collect();
        assert_eq!(league_rows.len(), 6);
        let gold = league_rows
            .iter()
            .find(|r| r["type"] == "playersGold")
            .unwrap();
        assert_eq!(gold["typeValue"], 1);

        // league screen -> team (teamid) buckets: players/kits/badgeDBid (3 rows).
        let body = club_stats_body(&items, ContextField::Team);
        let team_rows: Vec<&Value> = body["stat"]
            .as_array()
            .unwrap()
            .iter()
            .filter(|r| r["contextId"] == 3 && r["contextValue"] == 240)
            .collect();
        assert_eq!(team_rows.len(), 3);
        let players = team_rows.iter().find(|r| r["type"] == "players").unwrap();
        assert_eq!(players["typeValue"], 1);
        assert!(team_rows.iter().any(|r| r["type"] == "badgeDBid"));
    }
}
