//! Local game scanning and database management.

use std::{
    collections::HashSet,
    io::ErrorKind,
    path::{Path, PathBuf},
};

use lanspread_db::db::{Game, GameDB, GameFileDescription};

use crate::error::PeerError;

// =============================================================================
// Local directory helpers
// =============================================================================

#[cfg(target_os = "windows")]
pub fn is_local_dir_name(name: &str) -> bool {
    name.eq_ignore_ascii_case("local")
}

#[cfg(not(target_os = "windows"))]
pub fn is_local_dir_name(name: &str) -> bool {
    name == "local"
}

/// Checks if a local directory has any content.
pub async fn local_dir_has_content(path: &Path) -> bool {
    let local_dir = path.join("local");
    if tokio::fs::metadata(&local_dir).await.is_err() {
        return false;
    }

    let mut entries = match tokio::fs::read_dir(&local_dir).await {
        Ok(entries) => entries,
        Err(e) => {
            log::warn!("Failed to read local dir {}: {e}", local_dir.display());
            return false;
        }
    };

    match entries.next_entry().await {
        Ok(Some(_)) => true,
        Ok(None) => false,
        Err(e) => {
            log::warn!("Failed to iterate local dir {}: {e}", local_dir.display());
            false
        }
    }
}

/// Checks if a game is available for download locally.
pub async fn local_download_available(
    game_dir: &str,
    game_id: &str,
    downloading_games: &HashSet<String>,
) -> bool {
    if downloading_games.contains(game_id) {
        log::debug!("Not serving game {game_id} locally because it is still downloading");
        return false;
    }

    let game_path = PathBuf::from(game_dir).join(game_id);
    let eti_path = game_path.join(format!("{game_id}.eti"));

    if tokio::fs::metadata(&eti_path).await.is_err() {
        return false;
    }

    // Only treat as pending install if the local installation directory is empty/missing
    !local_dir_has_content(game_path.as_path()).await
}

// =============================================================================
// Directory size calculation
// =============================================================================

/// Calculates the total size of a directory recursively.
pub async fn calculate_directory_size(dir: &Path, is_root: bool) -> eyre::Result<u64> {
    let mut total_size = 0u64;
    let mut entries = tokio::fs::read_dir(dir).await?;

    while let Some(entry) = entries.next_entry().await? {
        let path = entry.path();
        let name = entry.file_name();
        let name_str = name.to_string_lossy();

        if is_root {
            if name_str == ".sync" || name_str == ".softlan_first_start_done" {
                continue;
            }
            if entry.file_type().await?.is_dir() && is_local_dir_name(&name_str) {
                continue;
            }
        }

        let metadata = tokio::fs::metadata(&path).await?;

        if metadata.is_dir() {
            total_size += Box::pin(calculate_directory_size(&path, false)).await?;
        } else {
            total_size += metadata.len();
        }
    }

    Ok(total_size)
}

// =============================================================================
// Game database loading
// =============================================================================

/// Loads the local game database from the game directory.
pub async fn load_local_game_db(game_dir: &str) -> eyre::Result<GameDB> {
    let game_path = PathBuf::from(game_dir);

    let metadata = match tokio::fs::metadata(&game_path).await {
        Ok(metadata) => metadata,
        Err(err) => {
            if err.kind() == ErrorKind::NotFound {
                log::warn!(
                    "Local game directory {} missing; reporting empty game database",
                    game_path.display()
                );
                return Ok(GameDB::empty());
            }
            return Err(err.into());
        }
    };

    if !metadata.is_dir() {
        log::warn!(
            "Configured game directory {} is not a directory; reporting empty game database",
            game_path.display()
        );
        return Ok(GameDB::empty());
    }

    let mut games = Vec::new();

    // Scan game directory and create entries for installed games
    let mut entries = tokio::fs::read_dir(&game_path).await?;
    while let Some(entry) = entries.next_entry().await? {
        let path = entry.path();
        if path.is_dir()
            && let Some(game_id) = path.file_name().and_then(|n| n.to_str())
        {
            let eti_path = path.join(format!("{game_id}.eti"));
            let downloaded = tokio::fs::metadata(&eti_path).await.is_ok();
            if !downloaded {
                continue;
            }

            let installed = local_dir_has_content(&path).await;
            let local_version = if installed {
                match lanspread_db::db::read_version_from_ini(&path) {
                    Ok(version) => version,
                    Err(e) => {
                        log::warn!("Failed to read version.ini for installed game {game_id}: {e}");
                        None
                    }
                }
            } else {
                None
            };

            let size = calculate_directory_size(&path, true).await?;
            let game = Game {
                id: game_id.to_string(),
                name: game_id.to_string(),
                description: String::new(),
                release_year: String::new(),
                publisher: String::new(),
                max_players: 1,
                version: "1.0".to_string(),
                genre: String::new(),
                size,
                downloaded,
                installed,
                eti_game_version: local_version.clone(),
                local_version,
                peer_count: 0, // Local games start with 0 peers
            };
            games.push(game);
        }
    }

    Ok(GameDB::from(games))
}

/// Scans the local games directory and returns a `GameDB` with current games.
pub async fn scan_local_games(game_dir: &str) -> eyre::Result<GameDB> {
    load_local_game_db(game_dir).await
}

// =============================================================================
// Game file descriptions
// =============================================================================

/// Gets file descriptions for a game from the local filesystem.
pub async fn get_game_file_descriptions(
    game_id: &str,
    game_dir: &str,
) -> Result<Vec<GameFileDescription>, PeerError> {
    let base_dir = PathBuf::from(game_dir);
    let game_path = base_dir.join(game_id);

    if !game_path.exists() {
        return Err(PeerError::Other(eyre::eyre!(
            "Game directory does not exist: {}",
            game_path.display()
        )));
    }

    let mut file_descriptions = Vec::new();

    for entry in walkdir::WalkDir::new(&game_path)
        .into_iter()
        .filter_entry(|entry| {
            if entry.depth() == 1 {
                if entry.file_type().is_dir()
                    && entry.file_name().to_str().is_some_and(is_local_dir_name)
                {
                    // Skip the local install folder entirely so WalkDir never enters it.
                    return false;
                }

                if let Some(name) = entry.file_name().to_str() {
                    if entry.file_type().is_dir() && name == ".sync" {
                        return false;
                    }
                    if entry.file_type().is_file() && name == ".softlan_game_installed" {
                        return false;
                    }
                }
            }

            true
        })
        .filter_map(std::result::Result::ok)
    {
        let relative_path = match entry.path().strip_prefix(&base_dir) {
            Ok(path) => path.to_string_lossy().to_string(),
            Err(e) => {
                log::error!(
                    "Failed to get relative path for {}: {}",
                    entry.path().display(),
                    e
                );
                continue;
            }
        };

        let is_dir = entry.file_type().is_dir();
        let size = if is_dir {
            0
        } else {
            match tokio::fs::metadata(entry.path()).await {
                Ok(metadata) => metadata.len(),
                Err(e) => {
                    log::error!("Failed to read metadata for {relative_path}: {e}");
                    return Err(PeerError::FileSizeDetermination {
                        path: relative_path.clone(),
                        source: e,
                    });
                }
            }
        };

        let file_desc = GameFileDescription {
            game_id: game_id.to_string(),
            relative_path,
            is_dir,
            size,
        };

        file_descriptions.push(file_desc);
    }

    Ok(file_descriptions)
}
