use super::{is_probably_game_executable, title_from_path, 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 GogScanner;

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

impl ScannerProvider for GogScanner {
    fn source(&self) -> GameSource {
        GameSource::Gog
    }

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

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

            for entry in entries.flatten() {
                let install_path = entry.path();
                if !install_path.is_dir() {
                    continue;
                }
                if let Some(candidate) = inspect_install_dir(&install_path)? {
                    candidates.push(candidate);
                }
            }
        }

        Ok(candidates)
    }
}

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

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

    #[cfg(any(target_os = "linux", target_os = "macos"))]
    if let Some(home) = super::home_dir() {
        roots.push(home.join("GOG Games"));
        roots.push(home.join("Games").join("GOG"));
    }

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

fn inspect_install_dir(install_path: &Path) -> Result<Option<GameCandidate>, String> {
    let metadata = read_gog_metadata(install_path)?;
    let title = metadata
        .as_ref()
        .and_then(|value| value.get("name"))
        .and_then(Value::as_str)
        .map(ToString::to_string)
        .unwrap_or_else(|| title_from_path(install_path));
    let app_id = metadata
        .as_ref()
        .and_then(|value| value.get("gameId").or_else(|| value.get("game_id")))
        .and_then(|value| {
            value
                .as_i64()
                .map(|id| id.to_string())
                .or_else(|| value.as_str().map(ToString::to_string))
        });
    let executable_path = find_executable(install_path);

    Ok(Some(GameCandidate {
        title,
        source: GameSource::Gog,
        app_id,
        install_path: install_path.to_path_buf(),
        executable_path,
        launch_command: None,
    }))
}

fn read_gog_metadata(install_path: &Path) -> Result<Option<Value>, String> {
    let entries = match fs::read_dir(install_path) {
        Ok(entries) => entries,
        Err(_) => return Ok(None),
    };

    for entry in entries.flatten() {
        let path = entry.path();
        let Some(file_name) = path.file_name().and_then(|value| value.to_str()) else {
            continue;
        };
        if !file_name.starts_with("goggame-") || !file_name.ends_with(".info") {
            continue;
        }
        let contents = fs::read_to_string(&path)
            .map_err(|err| format!("Failed to read GOG metadata {}: {err}", path.display()))?;
        let value = serde_json::from_str(&contents)
            .map_err(|err| format!("Failed to parse GOG metadata {}: {err}", path.display()))?;
        return Ok(Some(value));
    }

    Ok(None)
}

fn find_executable(install_path: &Path) -> Option<PathBuf> {
    let entries = fs::read_dir(install_path).ok()?;
    entries
        .flatten()
        .map(|entry| entry.path())
        .find(|path| path.is_file() && is_launchable(path) && is_probably_game_executable(path))
}

fn is_launchable(path: &Path) -> bool {
    #[cfg(target_os = "windows")]
    {
        return path.extension().and_then(|value| value.to_str()) == Some("exe");
    }

    #[cfg(target_os = "macos")]
    {
        return path.extension().and_then(|value| value.to_str()) == Some("app");
    }

    #[cfg(target_os = "linux")]
    {
        if path.extension().and_then(|value| value.to_str()) == Some("desktop") {
            return true;
        }
        return true;
    }

    #[allow(unreachable_code)]
    false
}
