use super::ScannerProvider;
use crate::library::models::{GameCandidate, GameSource};
use crate::library::scanners::ScanContext;
use std::collections::HashSet;
use std::fs;
use std::path::{Path, PathBuf};

pub struct SteamScanner;

impl SteamScanner {
    pub fn new() -> Self {
        Self
    }
}

impl ScannerProvider for SteamScanner {
    fn source(&self) -> GameSource {
        GameSource::Steam
    }

    fn scan(&self, _context: &ScanContext) -> Result<Vec<GameCandidate>, String> {
        let mut candidates = Vec::new();
        let mut library_paths = Vec::new();

        for steam_root in discover_steam_roots() {
            library_paths.extend(discover_library_paths(&steam_root));
        }

        library_paths.extend(discover_drive_steam_libraries());

        for library_path in unique_existing_dirs(library_paths) {
            candidates.extend(scan_library_path(&library_path)?);
        }

        Ok(candidates)
    }
}

fn discover_steam_roots() -> Vec<PathBuf> {
    let mut roots = Vec::new();

    #[cfg(target_os = "windows")]
    {
        if let Some(program_files_x86) = std::env::var_os("PROGRAMFILES(X86)") {
            roots.push(PathBuf::from(program_files_x86).join("Steam"));
        }
        if let Some(program_files) = std::env::var_os("PROGRAMFILES") {
            roots.push(PathBuf::from(program_files).join("Steam"));
        }
    }

    #[cfg(target_os = "linux")]
    if let Some(home) = super::home_dir() {
        roots.push(home.join(".steam").join("steam"));
        roots.push(home.join(".local").join("share").join("Steam"));
    }

    #[cfg(target_os = "macos")]
    if let Some(home) = super::home_dir() {
        roots.push(
            home.join("Library")
                .join("Application Support")
                .join("Steam"),
        );
    }

    unique_existing_dirs(roots)
}

fn discover_library_paths(steam_root: &Path) -> Vec<PathBuf> {
    let mut libraries = vec![steam_root.to_path_buf()];
    let library_file = steam_root.join("steamapps").join("libraryfolders.vdf");

    if let Ok(contents) = fs::read_to_string(library_file) {
        for path in extract_vdf_values(&contents, "path") {
            libraries.push(PathBuf::from(path.replace("\\\\", "\\")));
        }

        // Older Steam clients used numeric keys directly for library paths.
        for (key, value) in extract_vdf_pairs(&contents) {
            if key.parse::<u32>().is_ok() && value.contains(['\\', '/']) {
                libraries.push(PathBuf::from(value.replace("\\\\", "\\")));
            }
        }
    }

    unique_existing_dirs(libraries)
}

fn scan_library_path(library_path: &Path) -> Result<Vec<GameCandidate>, String> {
    let steamapps = library_path.join("steamapps");
    let manifests = match fs::read_dir(&steamapps) {
        Ok(entries) => entries,
        Err(_) => return Ok(Vec::new()),
    };

    let mut candidates = Vec::new();
    for entry in manifests.flatten() {
        let path = entry.path();
        let file_name = path
            .file_name()
            .and_then(|name| name.to_str())
            .unwrap_or_default();
        if !file_name.starts_with("appmanifest_") || !file_name.ends_with(".acf") {
            continue;
        }

        if let Some(candidate) = parse_app_manifest(&steamapps, &path)? {
            candidates.push(candidate);
        }
    }

    Ok(candidates)
}

fn discover_drive_steam_libraries() -> Vec<PathBuf> {
    #[cfg(target_os = "windows")]
    {
        ('A'..='Z')
            .map(|letter| PathBuf::from(format!("{letter}:\\SteamLibrary")))
            .filter(|path| path.join("steamapps").is_dir())
            .collect()
    }

    #[cfg(not(target_os = "windows"))]
    {
        Vec::new()
    }
}

fn parse_app_manifest(
    steamapps: &Path,
    manifest_path: &Path,
) -> Result<Option<GameCandidate>, String> {
    let contents = fs::read_to_string(manifest_path).map_err(|err| {
        format!(
            "Failed to read Steam manifest {}: {err}",
            manifest_path.display()
        )
    })?;
    let app_id = extract_vdf_values(&contents, "appid").into_iter().next();
    let title = extract_vdf_values(&contents, "name").into_iter().next();
    let install_dir = extract_vdf_values(&contents, "installdir")
        .into_iter()
        .next();

    let Some(app_id) = app_id else {
        return Ok(None);
    };
    let Some(title) = title else {
        return Ok(None);
    };
    let Some(install_dir) = install_dir else {
        return Ok(None);
    };

    Ok(Some(GameCandidate {
        title,
        source: GameSource::Steam,
        app_id: Some(app_id.clone()),
        install_path: steamapps.join("common").join(install_dir),
        executable_path: None,
        launch_command: Some(format!("steam://rungameid/{app_id}")),
    }))
}

fn extract_vdf_values(contents: &str, wanted_key: &str) -> Vec<String> {
    extract_vdf_pairs(contents)
        .into_iter()
        .filter_map(|(key, value)| if key == wanted_key { Some(value) } else { None })
        .collect()
}

fn extract_vdf_pairs(contents: &str) -> Vec<(String, String)> {
    contents
        .lines()
        .filter_map(|line| {
            let parts = quoted_tokens(line);
            if parts.len() >= 2 {
                Some((parts[0].clone(), parts[1].clone()))
            } else {
                None
            }
        })
        .collect()
}

fn quoted_tokens(line: &str) -> Vec<String> {
    let mut tokens = Vec::new();
    let mut current = String::new();
    let mut in_quote = false;
    let mut escaped = false;

    for ch in line.chars() {
        if escaped {
            current.push(ch);
            escaped = false;
            continue;
        }
        if ch == '\\' && in_quote {
            escaped = true;
            continue;
        }
        if ch == '"' {
            if in_quote {
                tokens.push(current.clone());
                current.clear();
            }
            in_quote = !in_quote;
            continue;
        }
        if in_quote {
            current.push(ch);
        }
    }

    tokens
}

fn unique_existing_dirs(paths: Vec<PathBuf>) -> Vec<PathBuf> {
    let mut seen = HashSet::new();
    paths
        .into_iter()
        .filter(|path| path.is_dir())
        .filter(|path| seen.insert(path.to_string_lossy().to_lowercase()))
        .collect()
}
