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

pub struct LocalAppScanner;

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

impl ScannerProvider for LocalAppScanner {
    fn source(&self) -> GameSource {
        GameSource::Local
    }

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

        for folder in &context.local_folders {
            if !folder.is_dir() {
                continue;
            }
            scan_folder(folder, 0, &mut candidates)?;
        }

        Ok(candidates)
    }
}

fn scan_folder(
    folder: &Path,
    depth: usize,
    candidates: &mut Vec<GameCandidate>,
) -> Result<(), String> {
    if depth > 4 {
        return Ok(());
    }

    let entries = match fs::read_dir(folder) {
        Ok(entries) => entries,
        Err(_) => return Ok(()),
    };

    let mut executables = Vec::new();
    let mut child_dirs = Vec::new();

    for entry in entries.flatten() {
        let path = entry.path();
        if path.is_dir() {
            if should_descend(&path) {
                child_dirs.push(path);
            }
            continue;
        }

        if is_launchable_file(&path) && is_probably_game_executable(&path) {
            executables.push(path);
        }
    }

    if let Some(executable_path) = pick_best_executable(&executables) {
        let title_path = executable_path.parent().unwrap_or(folder);
        candidates.push(GameCandidate {
            title: title_from_path(title_path),
            source: GameSource::Local,
            app_id: None,
            install_path: title_path.to_path_buf(),
            executable_path: Some(executable_path),
            launch_command: None,
        });
        return Ok(());
    }

    for child in child_dirs {
        scan_folder(&child, depth + 1, candidates)?;
    }

    Ok(())
}

fn should_descend(path: &Path) -> bool {
    let name = path
        .file_name()
        .and_then(|value| value.to_str())
        .unwrap_or_default()
        .to_lowercase();
    !matches!(
        name.as_str(),
        "_commonredist" | "redist" | "redistributables" | "support" | "directx" | "vc_redist"
    )
}

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

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

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

    #[allow(unreachable_code)]
    false
}

fn pick_best_executable(executables: &[PathBuf]) -> Option<PathBuf> {
    executables
        .iter()
        .min_by_key(|path| {
            let file_name = path
                .file_name()
                .and_then(|value| value.to_str())
                .unwrap_or_default()
                .to_lowercase();
            if file_name.contains("launcher") {
                1
            } else {
                0
            }
        })
        .cloned()
}

#[cfg(target_os = "linux")]
fn has_executable_bit(path: &Path) -> bool {
    use std::os::unix::fs::PermissionsExt;
    fs::metadata(path)
        .map(|metadata| metadata.permissions().mode() & 0o111 != 0)
        .unwrap_or(false)
}
