//! FIFA 17 entity id ⇄ name resolution, loaded from the committed game-DB
//! tables (`leagues.json`, `nations.json`, `teams.json`, dumped from
//! `FIFA17.exe`'s resident DB).
//!
//! Two directions, both FIFA-17-specific and therefore adapter-owned:
//! * **forward** id → name — resolves a wire filter (`league=13`) to the
//!   semantic name Core filters on ("Premier League"). See [`EntityResolver`].
//! * **reverse** name → id — shapes a Core item's names back into the numeric
//!   ids the FIFA `/club` response carries (`leagueId`/`teamid`/`nation`).
//!
//! Grounding (worker-verified against the tables): forward is 1:1 for all three
//! (unique ids, no gaps). Reverse is clean for leagues (50 distinct names) and
//! nations (221 distinct); **team names collide** (e.g. `Arsenal` ×3, plus the
//! FUT "CHAMPIONS *" placeholder teams), so reverse team lookup is first-id-wins
//! and a collision count is exposed for diagnostics. `teamid == assetid` on
//! every row.

use std::collections::HashMap;

use crate::fut::owned_query::EntityResolver;

/// Reverse (name → FIFA id) resolution, used when shaping a Core item back onto
/// the FIFA wire. Unknown names return `None`; the shaper substitutes a neutral
/// `0` (a valid, non-desyncing int) rather than dropping the item.
pub trait ReverseEntityResolver {
    fn league_id(&self, name: &str) -> Option<u32>;
    fn nation_id(&self, name: &str) -> Option<u32>;
    fn team_id(&self, name: &str) -> Option<u32>;
}

/// Forward + reverse FIFA 17 entity maps.
#[derive(Debug, Default, Clone)]
pub struct Fifa17Entities {
    league_by_id: HashMap<u32, String>,
    league_by_name: HashMap<String, u32>,
    nation_by_id: HashMap<u32, String>,
    nation_by_name: HashMap<String, u32>,
    team_by_id: HashMap<u32, String>,
    team_by_name: HashMap<String, u32>,
    /// name-collisions dropped from the reverse maps (diagnostics only).
    pub reverse_collisions: usize,
}

/// Errors loading the entity tables.
#[derive(Debug)]
pub enum LoadError {
    Io(std::io::Error),
    Parse { file: String, detail: String },
}

impl std::fmt::Display for LoadError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            LoadError::Io(e) => write!(f, "reading entity tables: {e}"),
            LoadError::Parse { file, detail } => write!(f, "parsing {file}: {detail}"),
        }
    }
}

impl std::error::Error for LoadError {}

impl Fifa17Entities {
    /// Load from a directory holding `leagues.json`, `nations.json`,
    /// `teams.json` in the FIFA17 `{schema, rows:[...]}` dump format.
    pub fn from_tables_dir(dir: &std::path::Path) -> Result<Self, LoadError> {
        let mut e = Fifa17Entities::default();
        e.load_table(
            &dir.join("leagues.json"),
            "leagueid",
            "leaguename",
            Entity::League,
        )?;
        e.load_table(
            &dir.join("nations.json"),
            "nationid",
            "nationname",
            Entity::Nation,
        )?;
        e.load_table(&dir.join("teams.json"), "teamid", "teamname", Entity::Team)?;
        Ok(e)
    }

    fn load_table(
        &mut self,
        path: &std::path::Path,
        id_key: &str,
        name_key: &str,
        which: Entity,
    ) -> Result<(), LoadError> {
        let raw = std::fs::read_to_string(path).map_err(LoadError::Io)?;
        let file = path.display().to_string();
        let doc: serde_json::Value = serde_json::from_str(&raw).map_err(|e| LoadError::Parse {
            file: file.clone(),
            detail: e.to_string(),
        })?;
        let rows = doc
            .get("rows")
            .and_then(|r| r.as_array())
            .ok_or_else(|| LoadError::Parse {
                file: file.clone(),
                detail: "missing `rows` array".into(),
            })?;
        for row in rows {
            let (Some(id), Some(name)) = (
                row.get(id_key).and_then(|v| v.as_u64()),
                row.get(name_key).and_then(|v| v.as_str()),
            ) else {
                continue;
            };
            let id = id as u32;
            let (by_id, by_name) = match which {
                Entity::League => (&mut self.league_by_id, &mut self.league_by_name),
                Entity::Nation => (&mut self.nation_by_id, &mut self.nation_by_name),
                Entity::Team => (&mut self.team_by_id, &mut self.team_by_name),
            };
            by_id.insert(id, name.to_string());
            // Reverse: first id wins on a name collision; count the rest.
            if by_name.contains_key(name) {
                self.reverse_collisions += 1;
            } else {
                by_name.insert(name.to_string(), id);
            }
        }
        Ok(())
    }

    /// Build directly from maps (tests / small deployments).
    pub fn from_maps(
        leagues: HashMap<u32, String>,
        nations: HashMap<u32, String>,
        teams: HashMap<u32, String>,
    ) -> Self {
        let invert = |m: &HashMap<u32, String>| {
            let mut out = HashMap::new();
            for (&id, name) in m {
                out.entry(name.clone()).or_insert(id);
            }
            out
        };
        Fifa17Entities {
            league_by_name: invert(&leagues),
            nation_by_name: invert(&nations),
            team_by_name: invert(&teams),
            league_by_id: leagues,
            nation_by_id: nations,
            team_by_id: teams,
            reverse_collisions: 0,
        }
    }

    pub fn league_count(&self) -> usize {
        self.league_by_id.len()
    }
    pub fn nation_count(&self) -> usize {
        self.nation_by_id.len()
    }
    pub fn team_count(&self) -> usize {
        self.team_by_id.len()
    }
}

#[derive(Clone, Copy)]
enum Entity {
    League,
    Nation,
    Team,
}

impl EntityResolver for Fifa17Entities {
    fn league_name(&self, id: u32) -> Option<String> {
        self.league_by_id.get(&id).cloned()
    }
    fn nation_name(&self, id: u32) -> Option<String> {
        self.nation_by_id.get(&id).cloned()
    }
    fn team_name(&self, id: u32) -> Option<String> {
        self.team_by_id.get(&id).cloned()
    }
}

impl ReverseEntityResolver for Fifa17Entities {
    fn league_id(&self, name: &str) -> Option<u32> {
        self.league_by_name.get(name).copied()
    }
    fn nation_id(&self, name: &str) -> Option<u32> {
        self.nation_by_name.get(name).copied()
    }
    fn team_id(&self, name: &str) -> Option<u32> {
        self.team_by_name.get(name).copied()
    }
}

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

    /// Path to the committed game-DB tables, relative to this crate.
    fn tables_dir() -> std::path::PathBuf {
        std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../fifa17-recon/data/tables")
    }

    #[test]
    fn loads_committed_tables_and_resolves_both_directions() {
        let dir = tables_dir();
        if !dir.join("leagues.json").exists() {
            eprintln!(
                "skipping: committed tables not present at {}",
                dir.display()
            );
            return;
        }
        let e = Fifa17Entities::from_tables_dir(&dir).expect("load tables");
        assert_eq!(e.league_count(), 50);
        assert_eq!(e.nation_count(), 221);
        assert_eq!(e.team_count(), 750);
        // forward id -> name (grounding pinned in owned_query too)
        assert_eq!(e.league_name(13).as_deref(), Some("Premier League"));
        assert_eq!(e.nation_name(52).as_deref(), Some("Argentina"));
        assert_eq!(e.team_name(5).as_deref(), Some("Chelsea"));
        // reverse name -> id (clean for leagues/nations)
        assert_eq!(e.league_id("Premier League"), Some(13));
        assert_eq!(e.nation_id("Argentina"), Some(52));
        assert_eq!(e.team_id("Chelsea"), Some(5));
        // unknown -> None (never a raw-id fallback)
        assert_eq!(e.league_name(999_999), None);
        assert_eq!(e.team_id("Northgate United"), None);
    }

    #[test]
    fn from_maps_inverts() {
        let e = Fifa17Entities::from_maps(
            HashMap::from([(13, "Premier League".to_string())]),
            HashMap::from([(52, "Argentina".to_string())]),
            HashMap::from([(5, "Chelsea".to_string())]),
        );
        assert_eq!(e.league_id("Premier League"), Some(13));
        assert_eq!(e.team_name(5).as_deref(), Some("Chelsea"));
    }
}
