#![allow(clippy::missing_errors_doc)]
#![allow(clippy::doc_markdown)]

use std::{collections::HashMap, fmt, path::Path};

use serde::{Deserialize, Serialize};

/// Read version from version.ini file
/// # Errors
/// Returns error if file cannot be read or parsed
pub fn read_version_from_ini(game_dir: &Path) -> eyre::Result<Option<String>> {
    let version_file = game_dir.join("version.ini");
    if !version_file.exists() {
        return Ok(None);
    }

    let content = std::fs::read_to_string(&version_file)?;
    let version = content.trim().to_string();

    // Validate format (YYYYMMDD)
    if version.len() == 8 && version.chars().all(|c| c.is_ascii_digit()) {
        Ok(Some(version))
    } else {
        tracing::warn!(
            "Invalid version format in {}: {}",
            version_file.display(),
            version
        );
        Ok(None)
    }
}

/// A game
#[derive(Clone, Serialize, Deserialize)]
pub struct Game {
    /// example: aoe2
    pub id: String,
    /// example: Age of Empires 2
    pub name: String,
    /// example: Dieses Paket enthält die original AoE 2 Version,...
    pub description: String,
    /// example: 1999
    pub release_year: String,
    /// Microsoft
    pub publisher: String,
    /// example: 8
    pub max_players: u32,
    /// example: 3.5
    pub version: String,
    /// example: Echtzeit-Strategie
    pub genre: String,
    /// size in bytes: example: 3455063152
    pub size: u64,
    /// indicates that the ETI bundle exists locally
    #[serde(default)]
    pub downloaded: bool,
    /// only relevant for client (yeah... I know)
    #[serde(default)]
    pub installed: bool,
    /// ETI game version from version.ini (YYYYMMDD format) (server)
    pub eti_game_version: Option<String>,
    /// Local game version from version.ini (YYYYMMDD format)
    pub local_version: Option<String>,
    /// Number of peers that have this game available
    pub peer_count: u32,
}

impl fmt::Debug for Game {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(
            f,
            "{}: {}  ({} MB)",
            self.id,
            self.name,
            self.size / 1024 / 1024,
        )
    }
}

impl fmt::Display for Game {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{}", self.name)
    }
}

impl PartialEq for Game {
    fn eq(&self, other: &Self) -> bool {
        self.id == other.id
    }
}

impl Eq for Game {}

impl PartialOrd for Game {
    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
        Some(self.cmp(other))
    }
}

impl Ord for Game {
    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
        self.name.cmp(&other.name)
    }
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GameDB {
    pub games: HashMap<String, Game>,
}

impl GameDB {
    #[must_use]
    pub fn empty() -> Self {
        GameDB {
            games: HashMap::new(),
        }
    }

    #[must_use]
    pub fn from(games: Vec<Game>) -> Self {
        let mut db = GameDB::empty();
        for game in games {
            db.games.insert(game.id.clone(), game);
        }
        db
    }

    #[must_use]
    pub fn get_game_by_id<S>(&self, id: S) -> Option<&Game>
    where
        S: AsRef<str>,
    {
        self.games.get(id.as_ref())
    }

    #[must_use]
    pub fn get_mut_game_by_id<S>(&mut self, id: S) -> Option<&mut Game>
    where
        S: AsRef<str>,
    {
        self.games.get_mut(id.as_ref())
    }

    #[must_use]
    pub fn get_game_by_name(&self, name: &str) -> Option<&Game> {
        self.games.values().find(|game| game.name == name)
    }

    #[must_use]
    pub fn all_games(&self) -> Vec<&Game> {
        let mut games: Vec<_> = self.games.values().collect();
        games.sort_by(|a, b| a.name.cmp(&b.name));
        games
    }

    pub fn set_all_uninstalled(&mut self) {
        for game in self.games.values_mut() {
            game.downloaded = false;
            game.installed = false;
            game.local_version = None;
        }
    }
}

impl Default for GameDB {
    fn default() -> Self {
        Self::empty()
    }
}

#[derive(Clone, Serialize, Deserialize)]
pub struct GameFileDescription {
    pub game_id: String,
    pub relative_path: String,
    pub is_dir: bool,
    pub size: u64,
}

impl GameFileDescription {
    #[must_use]
    pub fn is_version_ini(&self) -> bool {
        self.relative_path.ends_with("/version.ini")
    }

    #[must_use]
    pub fn file_size(&self) -> u64 {
        if self.is_dir { 0 } else { self.size }
    }
}

impl fmt::Debug for GameFileDescription {
    #[allow(clippy::cast_precision_loss)]
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(
            f,
            "{}: [{}] path:{} size:{}",
            self.game_id,
            if self.is_dir { 'D' } else { 'F' },
            self.relative_path,
            self.size,
        )
    }
}

#[cfg(test)]
mod tests {
    use serde_json::json;

    use super::Game;

    #[test]
    fn installed_defaults_to_false_when_missing() {
        let raw = json!({
            "id": "aoe2",
            "name": "Age of Empires II",
            "description": "desc",
            "release_year": "1999",
            "publisher": "Microsoft",
            "max_players": 8,
            "version": "1.0",
            "genre": "RTS",
            "size": 123_456,
            "thumbnail": null,
            "downloaded": true,
            "eti_game_version": "20240101",
            "local_version": null,
            "peer_count": 2
        })
        .to_string();

        let game: Game = serde_json::from_str(&raw).expect("game should deserialize");
        assert!(
            !game.installed,
            "missing installed flag should default to false"
        );
    }
}
