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

pub struct EpicScanner;

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

impl ScannerProvider for EpicScanner {
    fn source(&self) -> GameSource {
        GameSource::Epic
    }

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

        for manifest_dir in discover_manifest_dirs() {
            let entries = match fs::read_dir(manifest_dir) {
                Ok(entries) => entries,
                Err(_) => continue,
            };

            for entry in entries.flatten() {
                let path = entry.path();
                if path.extension().and_then(|value| value.to_str()) != Some("item") {
                    continue;
                }
                if let Some(candidate) = parse_manifest(&path)? {
                    candidates.push(candidate);
                }
            }
        }

        Ok(candidates)
    }
}

fn discover_manifest_dirs() -> Vec<PathBuf> {
    let mut dirs = Vec::new();

    #[cfg(target_os = "windows")]
    if let Some(program_data) = std::env::var_os("PROGRAMDATA") {
        dirs.push(
            PathBuf::from(program_data)
                .join("Epic")
                .join("EpicGamesLauncher")
                .join("Data")
                .join("Manifests"),
        );
    }

    #[cfg(target_os = "macos")]
    if let Some(home) = super::home_dir() {
        dirs.push(
            home.join("Library")
                .join("Application Support")
                .join("Epic")
                .join("EpicGamesLauncher")
                .join("Data")
                .join("Manifests"),
        );
    }

    #[cfg(target_os = "linux")]
    if let Some(home) = super::home_dir() {
        dirs.push(
            home.join(".config")
                .join("Epic")
                .join("EpicGamesLauncher")
                .join("Data")
                .join("Manifests"),
        );
    }

    dirs.into_iter().filter(|path| path.is_dir()).collect()
}

fn parse_manifest(path: &Path) -> Result<Option<GameCandidate>, String> {
    let contents = fs::read_to_string(path)
        .map_err(|err| format!("Failed to read Epic manifest {}: {err}", path.display()))?;
    let value: Value = serde_json::from_str(&contents)
        .map_err(|err| format!("Failed to parse Epic manifest {}: {err}", path.display()))?;

    let Some(display_name) = value.get("DisplayName").and_then(Value::as_str) else {
        return Ok(None);
    };
    let Some(install_location) = value.get("InstallLocation").and_then(Value::as_str) else {
        return Ok(None);
    };

    let launch_executable = value
        .get("LaunchExecutable")
        .and_then(Value::as_str)
        .filter(|value| !value.is_empty());
    let catalog_item_id = value
        .get("CatalogItemId")
        .or_else(|| value.get("MainGameCatalogItemId"))
        .and_then(Value::as_str)
        .map(ToString::to_string);
    let install_path = PathBuf::from(install_location);
    let executable_path = launch_executable.map(|executable| install_path.join(executable));

    Ok(Some(GameCandidate {
        title: display_name.to_string(),
        source: GameSource::Epic,
        app_id: catalog_item_id,
        install_path,
        executable_path,
        launch_command: None,
    }))
}
