use std::fs::{self, File};
use std::io::Read;
use std::os::unix::fs::PermissionsExt;
use std::path::{Path, PathBuf};
use std::time::SystemTime;

#[derive(Debug, Clone, PartialEq)]
pub enum AppImageType {
    Type1,
    Type2,
}

impl AppImageType {
    pub fn as_i32(&self) -> i32 {
        match self {
            Self::Type1 => 1,
            Self::Type2 => 2,
        }
    }
}

#[derive(Debug, Clone)]
pub struct DiscoveredAppImage {
    pub path: PathBuf,
    pub filename: String,
    pub appimage_type: AppImageType,
    pub size_bytes: u64,
    pub modified_time: Option<SystemTime>,
    pub is_executable: bool,
}

/// Expand ~ to home directory.
pub fn expand_tilde(path: &str) -> PathBuf {
    if let Some(rest) = path.strip_prefix("~/") {
        if let Some(home) = dirs::home_dir() {
            return home.join(rest);
        }
    }
    if path == "~" {
        if let Some(home) = dirs::home_dir() {
            return home;
        }
    }
    PathBuf::from(path)
}

/// Check a single file for AppImage magic bytes.
/// ELF magic at offset 0: 0x7F 'E' 'L' 'F'
/// AppImage Type 2 at offset 8: 'A' 'I' 0x02
/// AppImage Type 1 at offset 8: 'A' 'I' 0x01
pub fn detect_appimage(path: &Path) -> Option<AppImageType> {
    let mut file = File::open(path).ok()?;
    let mut header = [0u8; 16];
    file.read_exact(&mut header).ok()?;

    // Check ELF magic
    if header[0..4] != [0x7F, 0x45, 0x4C, 0x46] {
        return None;
    }

    // Check AppImage magic at offset 8
    if header[8] == 0x41 && header[9] == 0x49 {
        match header[10] {
            0x02 => return Some(AppImageType::Type2),
            0x01 => return Some(AppImageType::Type1),
            _ => {}
        }
    }

    None
}

/// Scan a single directory for AppImage files (non-recursive).
fn scan_directory(dir: &Path) -> Vec<DiscoveredAppImage> {
    let mut results = Vec::new();

    let entries = match fs::read_dir(dir) {
        Ok(entries) => entries,
        Err(e) => {
            log::warn!("Cannot read directory {}: {}", dir.display(), e);
            return results;
        }
    };

    for entry in entries.flatten() {
        let path = entry.path();

        // Skip directories and symlinks to directories
        if path.is_dir() {
            continue;
        }

        // Skip very small files (AppImages are at least a few KB)
        let metadata = match fs::metadata(&path) {
            Ok(m) => m,
            Err(_) => continue,
        };
        if metadata.len() < 4096 {
            continue;
        }

        // Check for AppImage magic bytes
        if let Some(appimage_type) = detect_appimage(&path) {
            let filename = path
                .file_name()
                .map(|n| n.to_string_lossy().into_owned())
                .unwrap_or_default();

            let mut is_executable = metadata.permissions().mode() & 0o111 != 0;
            if !is_executable {
                let perms = std::fs::Permissions::from_mode(0o755);
                if std::fs::set_permissions(&path, perms).is_ok() {
                    is_executable = true;
                    log::info!("Auto-fixed executable permission: {}", path.display());
                }
            }
            let modified_time = metadata.modified().ok();

            results.push(DiscoveredAppImage {
                path,
                filename,
                appimage_type,
                size_bytes: metadata.len(),
                modified_time,
                is_executable,
            });
        }
    }

    results
}

/// Scan all configured directories for AppImages.
/// Directories are expanded (~ -> home) and deduplicated.
pub fn scan_directories(dirs: &[String]) -> Vec<DiscoveredAppImage> {
    let mut results = Vec::new();
    let mut seen_paths = std::collections::HashSet::new();

    for dir_str in dirs {
        let dir = expand_tilde(dir_str);
        if !dir.exists() {
            log::info!("Scan directory does not exist: {}", dir.display());
            continue;
        }
        if !dir.is_dir() {
            log::warn!("Scan path is not a directory: {}", dir.display());
            continue;
        }

        for discovered in scan_directory(&dir) {
            // Deduplicate by canonical path
            let canonical = discovered.path.canonicalize()
                .unwrap_or_else(|_| discovered.path.clone());
            if seen_paths.insert(canonical) {
                results.push(discovered);
            }
        }
    }

    results
}

/// Compute the SHA-256 hash of a file, returned as a lowercase hex string.
pub fn compute_sha256(path: &Path) -> std::io::Result<String> {
    use sha2::{Digest, Sha256};
    let mut file = File::open(path)?;
    let mut hasher = Sha256::new();
    std::io::copy(&mut file, &mut hasher)?;
    Ok(format!("{:x}", hasher.finalize()))
}

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

    fn create_fake_appimage(dir: &Path, name: &str, appimage_type: u8) -> PathBuf {
        let path = dir.join(name);
        let mut f = File::create(&path).unwrap();

        // ELF magic
        f.write_all(&[0x7F, 0x45, 0x4C, 0x46]).unwrap();
        // ELF class, data, version, OS/ABI (padding to offset 8)
        f.write_all(&[0x02, 0x01, 0x01, 0x00]).unwrap();
        // AppImage magic at offset 8
        f.write_all(&[0x41, 0x49, appimage_type]).unwrap();
        // Pad to make it bigger than 4096 bytes
        f.write_all(&vec![0u8; 8192]).unwrap();

        // Make executable
        #[cfg(unix)]
        {
            use std::os::unix::fs::PermissionsExt;
            fs::set_permissions(&path, fs::Permissions::from_mode(0o755)).unwrap();
        }

        path
    }

    #[test]
    fn test_detect_type2() {
        let dir = tempfile::tempdir().unwrap();
        let path = create_fake_appimage(dir.path(), "test.AppImage", 0x02);
        assert_eq!(detect_appimage(&path), Some(AppImageType::Type2));
    }

    #[test]
    fn test_detect_type1() {
        let dir = tempfile::tempdir().unwrap();
        let path = create_fake_appimage(dir.path(), "test.AppImage", 0x01);
        assert_eq!(detect_appimage(&path), Some(AppImageType::Type1));
    }

    #[test]
    fn test_detect_not_appimage() {
        let dir = tempfile::tempdir().unwrap();
        let path = dir.path().join("not_appimage");
        let mut f = File::create(&path).unwrap();
        f.write_all(&[0x7F, 0x45, 0x4C, 0x46, 0x02, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00]).unwrap();
        f.write_all(&vec![0u8; 8192]).unwrap();
        assert_eq!(detect_appimage(&path), None);
    }

    #[test]
    fn test_detect_non_elf() {
        let dir = tempfile::tempdir().unwrap();
        let path = dir.path().join("text.txt");
        let mut f = File::create(&path).unwrap();
        f.write_all(b"Hello world, this is not an ELF file at all").unwrap();
        f.write_all(&vec![0u8; 8192]).unwrap();
        assert_eq!(detect_appimage(&path), None);
    }

    #[test]
    fn test_scan_directory() {
        let dir = tempfile::tempdir().unwrap();
        create_fake_appimage(dir.path(), "app1.AppImage", 0x02);
        create_fake_appimage(dir.path(), "app2.AppImage", 0x02);
        // Create a non-AppImage file
        let non_ai = dir.path().join("readme.txt");
        fs::write(&non_ai, &vec![0u8; 8192]).unwrap();

        let results = scan_directory(dir.path());
        assert_eq!(results.len(), 2);
        assert!(results.iter().all(|r| r.appimage_type == AppImageType::Type2));
    }

    #[test]
    fn test_expand_tilde() {
        let expanded = expand_tilde("~/Applications");
        assert!(!expanded.to_string_lossy().starts_with('~'));
        assert!(expanded.to_string_lossy().ends_with("Applications"));
    }
}
