use log::{debug, error, info, warn};
use regex::Regex;
use std::collections::HashSet;
use std::fs;
use std::io::Read;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use tokio::sync::mpsc;
use walkdir::WalkDir;

// Game information structure
#[derive(Debug, Clone)]
pub struct GameInfo {
    pub id: String,
    pub title: String,
    pub path: PathBuf,
    pub native: bool,
    pub api_files: Vec<String>,
    pub cream_installed: bool,
    pub smoke_installed: bool,
}

// Find potential Steam installation directories
pub fn get_default_steam_paths() -> Vec<PathBuf> {
    let mut paths = Vec::new();

    // Get user's home directory
    if let Ok(home) = std::env::var("HOME") {
        info!("Searching for Steam in home directory: {}", home);

        // Common Steam installation locations on Linux
        let common_paths = [
            ".steam/steam",                                        // Steam symlink directory
            ".steam/root",                                         // Alternative symlink
            ".local/share/Steam",                                  // Flatpak Steam installation
            ".var/app/com.valvesoftware.Steam/.local/share/Steam", // Flatpak container path
            ".var/app/com.valvesoftware.Steam/data/Steam",         // Alternative Flatpak path
            "/run/media/mmcblk0p1",                                // Removable Storage path
        ];

        for path in &common_paths {
            let full_path = PathBuf::from(&home).join(path);
            if full_path.exists() {
                debug!("Found Steam directory: {}", full_path.display());
                paths.push(full_path);
            }
        }
    }

    // Add Steam Deck paths if they exist
    let deck_paths = ["/home/deck/.steam/steam", "/home/deck/.local/share/Steam"];

    for path in &deck_paths {
        let p = PathBuf::from(path);
        if p.exists() && !paths.contains(&p) {
            debug!("Found Steam Deck path: {}", p.display());
            paths.push(p);
        }
    }

    // Try to extract paths from Steam registry file
    if let Some(registry_paths) = read_steam_registry() {
        for path in registry_paths {
            if !paths.contains(&path) && path.exists() {
                debug!("Adding Steam path from registry: {}", path.display());
                paths.push(path);
            }
        }
    }

    info!("Found {} potential Steam directories", paths.len());
    paths
}

/// Validates a user-picked directory and resolves it to the library root
/// `find_steam_libraries` expects (i.e. the folder that directly contains
/// `steamapps`, not the `steamapps` folder itself).
pub fn normalize_steam_library_path(path: &Path) -> Result<PathBuf, String> {
    if !path.is_dir() {
        return Err(format!("{} is not a valid directory", path.display()));
    }

    if path.join("steamapps").is_dir() {
        return Ok(path.to_path_buf());
    }

    // User picked the steamapps folder itself - use its parent as the root
    if path.file_name().and_then(|n| n.to_str()) == Some("steamapps") {
        if let Some(parent) = path.parent() {
            return Ok(parent.to_path_buf());
        }
    }

    Err(format!(
        "No Steam library found at {} (expected a 'steamapps' folder inside it)",
        path.display()
    ))
}

// Try to read the Steam registry file to find installation paths
fn read_steam_registry() -> Option<Vec<PathBuf>> {
    let home = match std::env::var("HOME") {
        Ok(h) => h,
        Err(_) => return None,
    };

    let registry_paths = [
        format!("{}/.steam/registry.vdf", home),
        format!("{}/.steam/steam/registry.vdf", home),
        format!("{}/.local/share/Steam/registry.vdf", home),
    ];

    for registry_path in registry_paths {
        let path = Path::new(&registry_path);
        if path.exists() {
            debug!("Found Steam registry at: {}", path.display());

            if let Ok(content) = fs::read_to_string(path) {
                let mut paths = Vec::new();

                // Extract Steam installation paths
                let re_steam_path = Regex::new(r#""SteamPath"\s+"([^"]+)""#).unwrap();
                if let Some(cap) = re_steam_path.captures(&content) {
                    let steam_path = PathBuf::from(&cap[1]);
                    paths.push(steam_path);
                }

                // Look for install path
                let re_install_path = Regex::new(r#""InstallPath"\s+"([^"]+)""#).unwrap();
                if let Some(cap) = re_install_path.captures(&content) {
                    let install_path = PathBuf::from(&cap[1]);
                    if !paths.contains(&install_path) {
                        paths.push(install_path);
                    }
                }

                if !paths.is_empty() {
                    return Some(paths);
                }
            }
        }
    }

    None
}

// Find all Steam library folders from base Steam installation paths
pub fn find_steam_libraries(base_paths: &[PathBuf]) -> Vec<PathBuf> {
    let mut libraries = HashSet::new();

    for base_path in base_paths {
        debug!("Looking for Steam libraries in: {}", base_path.display());

        // Check if this path contains a steamapps directory
        let steamapps_path = base_path.join("steamapps");
        if steamapps_path.exists() && steamapps_path.is_dir() {
            debug!("Found steamapps directory: {}", steamapps_path.display());
            libraries.insert(steamapps_path.clone());

            // Check for additional libraries in libraryfolders.vdf
            parse_library_folders_vdf(&steamapps_path, &mut libraries);
        }

        // Also check for steamapps in common locations relative to this path
        let possible_steamapps = [
            base_path.join("steam/steamapps"),
            base_path.join("Steam/steamapps"),
        ];

        for path in &possible_steamapps {
            if path.exists() && path.is_dir() && !libraries.contains(path) {
                debug!("Found steamapps directory: {}", path.display());
                libraries.insert(path.clone());

                // Check for additional libraries in libraryfolders.vdf
                parse_library_folders_vdf(path, &mut libraries);
            }
        }
    }

    let result: Vec<PathBuf> = libraries.into_iter().collect();
    info!("Found {} Steam library directories", result.len());
    for (i, lib) in result.iter().enumerate() {
        info!("  Library {}: {}", i + 1, lib.display());
    }
    result
}

// Parse libraryfolders.vdf to extract additional library paths
fn parse_library_folders_vdf(steamapps_path: &Path, libraries: &mut HashSet<PathBuf>) {
    // Check both possible locations of the VDF file
    let vdf_paths = [
        steamapps_path.join("libraryfolders.vdf"),
        steamapps_path.join("config/libraryfolders.vdf"),
    ];

    for vdf_path in &vdf_paths {
        if vdf_path.exists() {
            debug!("Found library folders VDF: {}", vdf_path.display());

            if let Ok(content) = fs::read_to_string(vdf_path) {
                // Extract library paths using regex for both new and old format VDFs
                let re_path = Regex::new(r#""path"\s+"([^"]+)""#).unwrap();
                for cap in re_path.captures_iter(&content) {
                    let path_str = &cap[1];
                    let lib_path = PathBuf::from(path_str).join("steamapps");

                    if lib_path.exists() && lib_path.is_dir() && !libraries.contains(&lib_path) {
                        debug!("Found library from VDF: {}", lib_path.display());
                        // Clone lib_path before inserting to avoid ownership issues
                        let lib_path_clone = lib_path.clone();
                        libraries.insert(lib_path_clone);

                        // Recursively check this library for more libraries
                        parse_library_folders_vdf(&lib_path, libraries);
                    }
                }
            }
        }
    }
}

// Parse an appmanifest ACF file to extract game information
fn parse_appmanifest(path: &Path) -> Option<(String, String, String)> {
    match fs::read_to_string(path) {
        Ok(content) => {
            // Use regex to extract the app ID, name, and install directory
            let re_appid = Regex::new(r#""appid"\s+"(\d+)""#).unwrap();
            let re_name = Regex::new(r#""name"\s+"([^"]+)""#).unwrap();
            let re_installdir = Regex::new(r#""installdir"\s+"([^"]+)""#).unwrap();

            if let (Some(app_id_cap), Some(name_cap), Some(dir_cap)) = (
                re_appid.captures(&content),
                re_name.captures(&content),
                re_installdir.captures(&content),
            ) {
                let app_id = app_id_cap[1].to_string();
                let name = name_cap[1].to_string();
                let install_dir = dir_cap[1].to_string();

                return Some((app_id, name, install_dir));
            }
        }
        Err(e) => {
            error!("Failed to read ACF file {}: {}", path.display(), e);
        }
    }

    None
}

// Check if a file is a Linux ELF binary
fn is_elf_binary(path: &Path) -> bool {
    if let Ok(mut file) = fs::File::open(path) {
        let mut buffer = [0; 4];
        if file.read_exact(&mut buffer).is_ok() {
            // Check for ELF magic number (0x7F 'E' 'L' 'F')
            return buffer[0] == 0x7F
                && buffer[1] == b'E'
                && buffer[2] == b'L'
                && buffer[3] == b'F';
        }
    }

    false
}

// Check if a game has CreamLinux installed
fn check_creamlinux_installed(game_path: &Path) -> bool {
    let cream_files = ["cream.sh", "cream_api.ini", "cream_api.so"];

    for file in &cream_files {
        if game_path.join(file).exists() {
            debug!("CreamLinux installation detected: {}", file);
            return true;
        }
    }

    false
}

// Check if a game has SmokeAPI installed
fn check_smokeapi_installed(game_path: &Path, api_files: &[String]) -> bool {
    // For Proton games: check for backup DLL files
    if !api_files.is_empty() {
        for api_file in api_files {
            let api_path = game_path.join(api_file);
            let api_dir = api_path.parent().unwrap_or(game_path);
            let api_filename = api_path.file_name().unwrap_or_default();

            // Check for backup file (original file renamed with _o.dll suffix)
            let backup_name = api_filename.to_string_lossy().replace(".dll", "_o.dll");
            let backup_path = api_dir.join(backup_name);

            if backup_path.exists() {
                debug!("SmokeAPI backup file found: {}", backup_path.display());
                return true;
            }
        }
    }

    // For Native games: check for lib_steam_api_o.so backup
    for entry in WalkDir::new(game_path)
        .max_depth(3)
        .into_iter()
        .filter_map(Result::ok)
    {
        let path = entry.path();
        if !path.is_file() {
            continue;
        }

        let filename = path.file_name().unwrap_or_default().to_string_lossy();

        // Check for native SmokeAPI backup
        if filename == "libsteam_api_o.so" {
            debug!("Found native SmokeAPI backup: {}", path.display());
            return true;
        }
    }
    
    // Also scan for orphaned backup files (in case the main DLL was removed)
    // This handles the Proton->Native switch case where steam_api*.dll is gone
    // but steam_api*_o.dll backup remains
    for entry in WalkDir::new(game_path)
        .max_depth(5)
        .into_iter()
        .filter_map(Result::ok)
    {
        let path = entry.path();
        if !path.is_file() {
            continue;
        }
        
        let filename = path.file_name().unwrap_or_default().to_string_lossy();
        
        // Look for steam_api*_o.dll backup files (SmokeAPI pattern)
        if filename.starts_with("steam_api") && filename.ends_with("_o.dll") {
            debug!("Found orphaned SmokeAPI backup file: {}", path.display());
            return true;
        }
    }

    false
}

// Scan a game directory to determine if it's native or needs Proton
// Also collect any Steam API DLLs for potential SmokeAPI installation
fn scan_game_directory(game_path: &Path) -> (bool, Vec<String>) {
    let mut found_exe = false;
    let mut found_linux_binary = false;
    let mut found_main_executable = false;
    let mut steam_api_files = Vec::new();

    // Strong indicators for native Linux games
    let mut has_libsteam_api = false;
    let mut has_linux_steam_libs = false;
    let mut linux_binary_count = 0;
    let mut windows_exe_count = 0;

    // Directories to skip for better performance
    let skip_dirs = [
        "videos",
        "video",
        "movies",
        "movie",
        "sound",
        "sounds",
        "audio",
        "textures",
        "music",
        "localization",
        "shaders",
        "logs",
        "assets/audio",
        "assets/video",
        "assets/textures",
    ];

    // Only scan to a reasonable depth (avoid extreme recursion)
    const MAX_DEPTH: usize = 8;

    // File extensions to check for (executable and Steam API files)
    let exe_extensions = ["exe", "bat", "cmd", "msi"];
    let binary_extensions = ["so", "bin", "sh", "x86", "x86_64"];

    // Files that indicate this is likely a launcher/installer
    let installer_patterns = [
      "setup", "install", "launcher", "uninstall", "redist", "vcredist", "directx", "_commonredist", "dotnet", "PhysX"
    ];

    // Recursively walk through the game directory
    for entry in WalkDir::new(game_path)
        .max_depth(MAX_DEPTH) // Limit depth to avoid traversing too deep
        .follow_links(false) // Don't follow symlinks to prevent cycles
        .into_iter()
        .filter_entry(|e| {
            // Skip certain directories for performance
            if e.file_type().is_dir() {
                let file_name = e.file_name().to_string_lossy().to_lowercase();
                if skip_dirs.iter().any(|&dir| file_name == dir) {
                    debug!("Skipping directory: {}", e.path().display());
                    return false;
                }
            }
            true
        })
        .filter_map(Result::ok)
    {
        let path = entry.path();
        if !path.is_file() {
            continue;
        }

        let filename = path.file_name()
            .unwrap_or_default()
            .to_string_lossy()
            .to_lowercase();
        
        // Check for strong Linux indicators first
        if filename == "libsteam_api.so" {
            has_libsteam_api = true;
            debug!("Found strong Linux indicator: {}", path.display());
        }

        // Check for other Linux Steam libraries
        if filename.starts_with("lib") && filename.contains("steam") && filename.ends_with(".so") {
            has_linux_steam_libs = true;
            debug!("Found Linux Steam library: {}", path.display());
        }

        // Check file extension
        if let Some(ext) = path.extension() {
            let ext_str = ext.to_string_lossy().to_lowercase();

            // Check for Windows executables
            if exe_extensions.iter().any(|&e| ext_str == e) {
                // Check if this looks like an installer/utility rather than main game
                let is_likely_installer = installer_patterns.iter()
                    .any(|&pattern| filename.contains(pattern));
                
                if !is_likely_installer {
                    found_exe = true;
                    windows_exe_count += 1;

                    // If its in the root directory and not an installer, its likely the main executable
                    if path.parent() == Some(game_path) {
                        found_main_executable = true;
                    }
                }
            }

            // Check for Steam API DLLs
            if ext_str == "dll" {
                if filename == "steam_api.dll" || filename == "steam_api64.dll" {
                    if let Ok(rel_path) = path.strip_prefix(game_path) {
                        let rel_path_str = rel_path.to_string_lossy().to_string();
                        debug!("Found Steam API DLL: {}", rel_path_str);
                        steam_api_files.push(rel_path_str);
                    }
                }
            }

            // Check for Linux binary files
            if binary_extensions.iter().any(|&e| ext_str == e) {
                found_linux_binary = true;
                linux_binary_count += 1;

                // Check if it's actually an ELF binary for more certainty
                if ext_str == "so" && is_elf_binary(path) {
                    found_linux_binary = true;
                }
            }
        }

        // Check for Linux executables (no extension)
        #[cfg(unix)]
        if !path.extension().is_some() {
            use std::os::unix::fs::PermissionsExt;

            if let Ok(metadata) = path.metadata() {
                let is_executable = metadata.permissions().mode() & 0o111 != 0;

                // Check executable permission and ELF format
                if is_executable && is_elf_binary(path) {
                    found_linux_binary = true;
                    linux_binary_count += 1;
                }
            }
        }
    }

    // Detection logic with priority system
    let has_steam_api_dll = !steam_api_files.is_empty();
    let is_native = determine_platform(
      has_libsteam_api,
      has_linux_steam_libs,
      found_linux_binary,
      found_exe,
      found_main_executable,
      linux_binary_count,
      windows_exe_count,
      has_steam_api_dll,
    );

    debug!(
        "Game scan results: native={}, libsteam_api={}, linux_libs={}, linux_binaries={}, exe_files={}, api_dlls={}",
        is_native,
        has_libsteam_api,
        has_linux_steam_libs,
        linux_binary_count,
        windows_exe_count,
        steam_api_files.len()
    );

    (is_native, steam_api_files)
}

// Priority-based platform detection
fn determine_platform(
  has_libsteam_api: bool,
  has_linux_steam_libs: bool,
  found_linux_binary: bool,
  found_exe: bool,
  found_main_executable: bool,
  linux_binary_count: usize,
  windows_exe_count: usize,
  has_steam_api_dll: bool,
) -> bool {
  // Priority 1: Strong Linux indicators
  if has_libsteam_api {
      debug!("Detected as native: libsteam_api.so found");
      return true;
  }

  if has_linux_steam_libs {
      debug!("Detected as native: Linux steam libraries found");
      return true;
  }

  // Priority 2: Strong Windows indicators - DLL files are Windows-only
  if has_steam_api_dll {
      debug!("Detected as Windows/Proton: steam_api.dll or steam_api64.dll found");
      return false;
  }

  // Priority 3: High confidence Linux indicators
  if found_linux_binary && linux_binary_count >= 3 && !found_main_executable {
      debug!("Detected as native: Multiple Linux binaries, no main Windows executable");
      return true;
  }

  // Priority 4: Balanced assessment
  if found_linux_binary && !found_main_executable && windows_exe_count <= 2 {
      debug!("Detected as native: Linux binaries present, only installer/utility Windows files");
      return true;
  }

  // Priority 5: Windows indicators
  if found_main_executable || (found_exe && !found_linux_binary) {
      debug!("Detected as Windows/Proton: Main executable or only Windows files found");
      return false;
  }

  // Priority 6: Default fallback
  if found_linux_binary {
      debug!("Detected as native: Linux binaries found (default fallback)");
      return true;
  }

  debug!("Detected as Windows/Proton: No strong indicators found");
  false
}

// Find all installed Steam games from library folders
pub async fn find_installed_games(steamapps_paths: &[PathBuf]) -> Vec<GameInfo> {
    let mut games = Vec::new();
    let seen_ids = Arc::new(tokio::sync::Mutex::new(HashSet::new()));

    // IDs to skip (tools, redistributables, etc.)
    let skip_ids = Arc::new(
        [
            "228980",  // Steamworks Common Redistributables
            "1070560", // Steam Linux Runtime
            "1391110", // Steam Linux Runtime - Soldier
            "1628350", // Steam Linux Runtime - Sniper
            "1493710", // Proton Experimental
            "2180100", // Steam Linux Runtime - Scout
        ]
        .iter()
        .copied()
        .collect::<HashSet<&str>>(),
    );

    // Name patterns to skip (case insensitive)
    let skip_patterns = Arc::new(
        [
            r"(?i)steam linux runtime",
            r"(?i)proton",
            r"(?i)steamworks common",
            r"(?i)redistributable",
            r"(?i)dotnet",
            r"(?i)vc redist",
        ]
        .iter()
        .map(|pat| Regex::new(pat).unwrap())
        .collect::<Vec<_>>(),
    );

    info!("Scanning for installed games in parallel...");

    // Create a channel to collect results
    let (tx, mut rx) = mpsc::channel(32);

    // First collect all appmanifest files to process
    let mut app_manifests = Vec::new();
    for steamapps_dir in steamapps_paths {
        if let Ok(entries) = fs::read_dir(steamapps_dir) {
            for entry in entries.flatten() {
                let path = entry.path();
                let filename = path.file_name().unwrap_or_default().to_string_lossy();

                // Check for appmanifest files
                if filename.starts_with("appmanifest_") && filename.ends_with(".acf") {
                    app_manifests.push((path, steamapps_dir.clone()));
                }
            }
        }
    }

    info!("Found {} appmanifest files to process", app_manifests.len());

    // Process appmanifest files
    let max_concurrent = num_cpus::get().max(1).min(8); // Use between 1 and 8 CPU cores
    info!("Using {} concurrent scanners", max_concurrent);

    // Use a semaphore to limit concurrency
    let semaphore = Arc::new(tokio::sync::Semaphore::new(max_concurrent));

    // Create a Vec to store all our task handles
    let mut handles = Vec::new();

    // Process each manifest file
    for (manifest_idx, (path, steamapps_dir)) in app_manifests.iter().enumerate() {
        // Clone what we need for the task
        let path = path.clone();
        let steamapps_dir = steamapps_dir.clone();
        let skip_patterns = Arc::clone(&skip_patterns);
        let tx = tx.clone();
        let seen_ids = Arc::clone(&seen_ids);
        let semaphore = Arc::clone(&semaphore);
        let skip_ids = Arc::clone(&skip_ids);

        // Create a new task
        let handle = tokio::spawn(async move {
            // Acquire a permit from the semaphore
            let _permit = semaphore.acquire().await.unwrap();

            // Parse the appmanifest file
            if let Some((id, name, install_dir)) = parse_appmanifest(&path) {
                // Skip if in exclusion list
                if skip_ids.contains(id.as_str()) {
                    return;
                }

                // Add a guard against duplicates
                {
                    let mut seen = seen_ids.lock().await;
                    if seen.contains(&id) {
                        return;
                    }
                    seen.insert(id.clone());
                }

                // Skip if the name matches any exclusion patterns
                if skip_patterns.iter().any(|re| re.is_match(&name)) {
                    debug!("Skipping runtime/tool: {} ({})", name, id);
                    return;
                }

                // Full path to the game directory
                let game_path = steamapps_dir.join("common").join(&install_dir);

                // Skip if game directory doesn't exist
                if !game_path.exists() {
                    warn!("Game directory not found: {}", game_path.display());
                    return;
                }

                // Scan the game directory to determine platform and find Steam API DLLs
                info!("Scanning game: {} at {}", name, game_path.display());

                // Scanning is I/O heavy but not CPU heavy, so we can just do it directly
                let (is_native, api_files) = scan_game_directory(&game_path);

                // Check for CreamLinux installation
                let cream_installed = check_creamlinux_installed(&game_path);

                // Check for SmokeAPI installation
                // For Proton games: check if api_files exist
                // For Native games: ALSO check for orphaned backup files (proton->native switch)
                let smoke_installed = check_smokeapi_installed(&game_path, &api_files);

                // Create the game info
                let game_info = GameInfo {
                    id,
                    title: name,
                    path: game_path,
                    native: is_native,
                    api_files,
                    cream_installed,
                    smoke_installed,
                };

                // Send the game info through the channel
                if tx.send(game_info).await.is_err() {
                    error!("Failed to send game info through channel");
                }
            }
        });

        handles.push(handle);

        // Every 10 files, yield to allow progress updates
        if manifest_idx % 10 == 0 {
            tokio::task::yield_now().await;
        }
    }

    // Drop the original sender so the receiver knows when we're done
    drop(tx);

    // Spawn a task to collect all the results
    let receiver_task = tokio::spawn(async move {
        let mut results = Vec::new();
        while let Some(game) = rx.recv().await {
            info!("Found game: {} ({})", game.title, game.id);
            info!("   Path: {}", game.path.display());
            info!(
                "   Status: Native={}, Cream={}, Smoke={}",
                game.native, game.cream_installed, game.smoke_installed
            );

            // Log Steam API DLLs if any
            if !game.api_files.is_empty() {
                info!("   Steam API files:");
                for api_file in &game.api_files {
                    info!("     - {}", api_file);
                }
            }

            results.push(game);
        }
        results
    });

    // Wait for all scan tasks to complete but don't wait for the results yet
    for handle in handles {
        // Ignore errors the receiver task will just get fewer results
        let _ = handle.await;
    }

    // Now wait for all results to be collected
    if let Ok(results) = receiver_task.await {
        games = results;
    }

    info!("Found {} installed games", games.len());
    games
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn normalize_accepts_folder_containing_steamapps() {
        let dir = tempfile::tempdir().unwrap();
        fs::create_dir(dir.path().join("steamapps")).unwrap();

        let result = normalize_steam_library_path(dir.path()).unwrap();
        assert_eq!(result, dir.path());
    }

    #[test]
    fn normalize_accepts_steamapps_folder_itself() {
        let dir = tempfile::tempdir().unwrap();
        let steamapps = dir.path().join("steamapps");
        fs::create_dir(&steamapps).unwrap();

        let result = normalize_steam_library_path(&steamapps).unwrap();
        assert_eq!(result, dir.path());
    }

    #[test]
    fn normalize_rejects_folder_without_steamapps() {
        let dir = tempfile::tempdir().unwrap();
        assert!(normalize_steam_library_path(dir.path()).is_err());
    }

    #[test]
    fn normalize_rejects_nonexistent_path() {
        let dir = tempfile::tempdir().unwrap();
        let missing = dir.path().join("does-not-exist");
        assert!(normalize_steam_library_path(&missing).is_err());
    }
}