use std::path::{Path, PathBuf};

use super::database::Database;

/// A discovered data/config/cache path for an AppImage.
#[derive(Debug, Clone)]
pub struct DiscoveredPath {
    pub path: PathBuf,
    pub path_type: PathType,
    pub discovery_method: DiscoveryMethod,
    pub confidence: Confidence,
    pub size_bytes: u64,
    pub exists: bool,
}

#[derive(Debug, Clone, Copy, PartialEq)]
pub enum PathType {
    Config,
    Data,
    Cache,
    State,
    Other,
}

impl PathType {
    pub fn as_str(&self) -> &'static str {
        match self {
            PathType::Config => "config",
            PathType::Data => "data",
            PathType::Cache => "cache",
            PathType::State => "state",
            PathType::Other => "other",
        }
    }

    pub fn label(&self) -> &'static str {
        match self {
            PathType::Config => "Configuration",
            PathType::Data => "Data",
            PathType::Cache => "Cache",
            PathType::State => "State",
            PathType::Other => "Other",
        }
    }

    pub fn icon_name(&self) -> &'static str {
        match self {
            PathType::Config => "preferences-system-symbolic",
            PathType::Data => "folder-documents-symbolic",
            PathType::Cache => "user-trash-symbolic",
            PathType::State => "document-properties-symbolic",
            PathType::Other => "folder-symbolic",
        }
    }
}

#[derive(Debug, Clone, Copy, PartialEq)]
pub enum DiscoveryMethod {
    /// Matched by desktop entry ID or WM class
    DesktopId,
    /// Matched by app name in XDG directory
    NameMatch,
    /// Matched by executable name
    ExecMatch,
    /// Matched by binary name extracted from AppImage
    BinaryMatch,
}

impl DiscoveryMethod {
    pub fn as_str(&self) -> &'static str {
        match self {
            DiscoveryMethod::DesktopId => "desktop_id",
            DiscoveryMethod::NameMatch => "name_match",
            DiscoveryMethod::ExecMatch => "exec_match",
            DiscoveryMethod::BinaryMatch => "binary_match",
        }
    }
}

#[derive(Debug, Clone, Copy, PartialEq)]
pub enum Confidence {
    High,
    Medium,
    Low,
}

impl Confidence {
    pub fn as_str(&self) -> &'static str {
        match self {
            Confidence::High => "high",
            Confidence::Medium => "medium",
            Confidence::Low => "low",
        }
    }

    pub fn badge_class(&self) -> &'static str {
        match self {
            Confidence::High => "success",
            Confidence::Medium => "warning",
            Confidence::Low => "neutral",
        }
    }
}

/// Summary of an AppImage's disk footprint.
#[derive(Debug, Clone, Default)]
pub struct FootprintSummary {
    pub appimage_size: u64,
    pub config_size: u64,
    pub data_size: u64,
    pub cache_size: u64,
    pub state_size: u64,
    pub other_size: u64,
    pub paths: Vec<DiscoveredPath>,
}

impl FootprintSummary {
    pub fn total_size(&self) -> u64 {
        self.appimage_size + self.config_size + self.data_size
            + self.cache_size + self.state_size + self.other_size
    }

    pub fn data_total(&self) -> u64 {
        self.config_size + self.data_size + self.cache_size
            + self.state_size + self.other_size
    }
}

/// Discover config/data/cache paths for an AppImage by searching XDG directories
/// for name variations.
pub fn discover_app_paths(
    app_name: Option<&str>,
    filename: &str,
    desktop_entry_content: Option<&str>,
) -> Vec<DiscoveredPath> {
    let mut results = Vec::new();
    let mut seen = std::collections::HashSet::new();

    // Build search terms from available identity information
    let mut search_terms: Vec<(String, DiscoveryMethod, Confidence)> = Vec::new();

    // From desktop entry: extract desktop file ID and WM class
    if let Some(content) = desktop_entry_content {
        if let Some(wm_class) = extract_desktop_key(content, "StartupWMClass") {
            let lower = wm_class.to_lowercase();
            search_terms.push((lower.clone(), DiscoveryMethod::DesktopId, Confidence::High));
            search_terms.push((wm_class.clone(), DiscoveryMethod::DesktopId, Confidence::High));
        }
        if let Some(exec) = extract_desktop_key(content, "Exec") {
            // Extract just the binary name from the Exec line
            let binary = exec.split_whitespace().next().unwrap_or(&exec);
            let binary_name = Path::new(binary)
                .file_name()
                .and_then(|n| n.to_str())
                .unwrap_or(binary);
            if !binary_name.is_empty() && binary_name != "AppRun" {
                let lower = binary_name.to_lowercase();
                search_terms.push((lower, DiscoveryMethod::ExecMatch, Confidence::Medium));
            }
        }
    }

    // From app name
    if let Some(name) = app_name {
        let lower = name.to_lowercase();
        // Remove spaces and special chars for directory matching
        let sanitized = lower.replace(' ', "").replace('-', "");
        search_terms.push((lower.clone(), DiscoveryMethod::NameMatch, Confidence::Medium));
        if sanitized != lower {
            search_terms.push((sanitized, DiscoveryMethod::NameMatch, Confidence::Low));
        }
        // Also try with hyphens
        let hyphenated = lower.replace(' ', "-");
        if hyphenated != lower {
            search_terms.push((hyphenated, DiscoveryMethod::NameMatch, Confidence::Medium));
        }
    }

    // From filename (strip .AppImage extension and version suffixes)
    let stem = filename
        .strip_suffix(".AppImage")
        .or_else(|| filename.strip_suffix(".appimage"))
        .unwrap_or(filename);
    // Strip version suffix like -1.2.3 or _v1.2
    let base = strip_version_suffix(stem);
    let lower = base.to_lowercase();
    search_terms.push((lower, DiscoveryMethod::BinaryMatch, Confidence::Low));

    // XDG base directories
    let home = match std::env::var("HOME") {
        Ok(h) => PathBuf::from(h),
        Err(_) => return results,
    };

    let xdg_config = std::env::var("XDG_CONFIG_HOME")
        .map(PathBuf::from)
        .unwrap_or_else(|_| home.join(".config"));
    let xdg_data = std::env::var("XDG_DATA_HOME")
        .map(PathBuf::from)
        .unwrap_or_else(|_| home.join(".local/share"));
    let xdg_cache = std::env::var("XDG_CACHE_HOME")
        .map(PathBuf::from)
        .unwrap_or_else(|_| home.join(".cache"));
    let xdg_state = std::env::var("XDG_STATE_HOME")
        .map(PathBuf::from)
        .unwrap_or_else(|_| home.join(".local/state"));

    let search_dirs = [
        (&xdg_config, PathType::Config),
        (&xdg_data, PathType::Data),
        (&xdg_cache, PathType::Cache),
        (&xdg_state, PathType::State),
    ];

    // Also search legacy dotfiles in $HOME
    for (term, method, confidence) in &search_terms {
        // Search XDG directories
        for (base_dir, path_type) in &search_dirs {
            if !base_dir.exists() {
                continue;
            }

            // Try exact match and case-insensitive match
            let entries = match std::fs::read_dir(base_dir) {
                Ok(e) => e,
                Err(_) => continue,
            };

            for entry in entries.flatten() {
                let entry_name = entry.file_name();
                let entry_str = entry_name.to_string_lossy();
                let entry_lower = entry_str.to_lowercase();

                if entry_lower == *term || entry_lower.starts_with(&format!("{}.", term))
                    || entry_lower.starts_with(&format!("{}-", term))
                {
                    let full_path = entry.path();
                    if seen.contains(&full_path) {
                        continue;
                    }
                    seen.insert(full_path.clone());

                    let size = dir_size(&full_path);
                    results.push(DiscoveredPath {
                        path: full_path,
                        path_type: *path_type,
                        discovery_method: *method,
                        confidence: *confidence,
                        size_bytes: size,
                        exists: true,
                    });
                }
            }
        }

        // Search for legacy dotfiles/dotdirs in $HOME (e.g., ~/.appname)
        let dotdir = home.join(format!(".{}", term));
        if dotdir.exists() && !seen.contains(&dotdir) {
            seen.insert(dotdir.clone());
            let size = dir_size(&dotdir);
            results.push(DiscoveredPath {
                path: dotdir,
                path_type: PathType::Config,
                discovery_method: *method,
                confidence: *confidence,
                size_bytes: size,
                exists: true,
            });
        }
    }

    // Sort: high confidence first, then by path type
    results.sort_by(|a, b| {
        let conf_ord = confidence_rank(&a.confidence).cmp(&confidence_rank(&b.confidence));
        if conf_ord != std::cmp::Ordering::Equal {
            return conf_ord;
        }
        a.path_type.as_str().cmp(b.path_type.as_str())
    });

    results
}

/// Discover paths and store them in the database.
pub fn discover_and_store(db: &Database, appimage_id: i64, record: &crate::core::database::AppImageRecord) {
    let paths = discover_app_paths(
        record.app_name.as_deref(),
        &record.filename,
        record.desktop_entry_content.as_deref(),
    );

    if let Err(e) = db.clear_app_data_paths(appimage_id) {
        log::warn!("Failed to clear app data paths for id {}: {}", appimage_id, e);
    }

    for dp in &paths {
        if let Err(e) = db.insert_app_data_path(
            appimage_id,
            &dp.path.to_string_lossy(),
            dp.path_type.as_str(),
            dp.discovery_method.as_str(),
            dp.confidence.as_str(),
            dp.size_bytes as i64,
        ) {
            log::warn!("Failed to insert app data path '{}' for id {}: {}", dp.path.display(), appimage_id, e);
        }
    }
}

/// Get a complete footprint summary for an AppImage.
pub fn get_footprint(db: &Database, appimage_id: i64, appimage_size: u64) -> FootprintSummary {
    let stored = db.get_app_data_paths(appimage_id).unwrap_or_default();

    let mut summary = FootprintSummary {
        appimage_size,
        ..Default::default()
    };

    for record in &stored {
        let dp = DiscoveredPath {
            path: PathBuf::from(&record.path),
            path_type: match record.path_type.as_str() {
                "config" => PathType::Config,
                "data" => PathType::Data,
                "cache" => PathType::Cache,
                "state" => PathType::State,
                _ => PathType::Other,
            },
            discovery_method: match record.discovery_method.as_str() {
                "desktop_id" => DiscoveryMethod::DesktopId,
                "name_match" => DiscoveryMethod::NameMatch,
                "exec_match" => DiscoveryMethod::ExecMatch,
                _ => DiscoveryMethod::BinaryMatch,
            },
            confidence: match record.confidence.as_str() {
                "high" => Confidence::High,
                "medium" => Confidence::Medium,
                _ => Confidence::Low,
            },
            size_bytes: record.size_bytes as u64,
            exists: Path::new(&record.path).exists(),
        };

        match dp.path_type {
            PathType::Config => summary.config_size += dp.size_bytes,
            PathType::Data => summary.data_size += dp.size_bytes,
            PathType::Cache => summary.cache_size += dp.size_bytes,
            PathType::State => summary.state_size += dp.size_bytes,
            PathType::Other => summary.other_size += dp.size_bytes,
        }
        summary.paths.push(dp);
    }

    summary
}

// --- Helpers ---

fn extract_desktop_key<'a>(content: &'a str, key: &str) -> Option<String> {
    for line in content.lines() {
        let trimmed = line.trim();
        if trimmed.starts_with('[') && trimmed != "[Desktop Entry]" {
            break; // Only look in [Desktop Entry] section
        }
        if let Some(rest) = trimmed.strip_prefix(key) {
            let rest = rest.trim_start();
            if let Some(value) = rest.strip_prefix('=') {
                return Some(value.trim().to_string());
            }
        }
    }
    None
}

fn strip_version_suffix(name: &str) -> &str {
    // Strip trailing version patterns like -1.2.3, _v2.0, -x86_64
    // Check for known arch suffixes first (may contain underscores)
    for suffix in &["-x86_64", "-aarch64", "-arm64", "-x86", "_x86_64", "_aarch64"] {
        if let Some(stripped) = name.strip_suffix(suffix) {
            return strip_version_suffix(stripped);
        }
    }
    // Find last hyphen or underscore followed by a digit or 'v'
    if let Some(pos) = name.rfind(|c: char| c == '-' || c == '_') {
        let after = &name[pos + 1..];
        if after.starts_with(|c: char| c.is_ascii_digit() || c == 'v') {
            return &name[..pos];
        }
    }
    name
}

/// Calculate the total size of a file or directory recursively.
pub fn dir_size_pub(path: &Path) -> u64 {
    dir_size(path)
}

fn dir_size(path: &Path) -> u64 {
    // Use symlink_metadata to avoid following symlinks outside the tree
    if let Ok(meta) = path.symlink_metadata() {
        if meta.is_file() {
            return meta.len();
        }
        if meta.is_symlink() {
            return 0;
        }
    }
    let mut total = 0u64;
    if let Ok(entries) = std::fs::read_dir(path) {
        for entry in entries.flatten() {
            let ft = match entry.file_type() {
                Ok(ft) => ft,
                Err(_) => continue,
            };
            // Skip symlinks to avoid counting external files or recursing out of tree
            if ft.is_symlink() {
                continue;
            }
            if ft.is_file() {
                total += entry.metadata().map(|m| m.len()).unwrap_or(0);
            } else if ft.is_dir() {
                total += dir_size(&entry.path());
            }
        }
    }
    total
}

fn confidence_rank(c: &Confidence) -> u8 {
    match c {
        Confidence::High => 0,
        Confidence::Medium => 1,
        Confidence::Low => 2,
    }
}

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

    #[test]
    fn test_strip_version_suffix() {
        assert_eq!(strip_version_suffix("MyApp-1.2.3"), "MyApp");
        assert_eq!(strip_version_suffix("MyApp_v2.0"), "MyApp");
        assert_eq!(strip_version_suffix("MyApp-x86_64"), "MyApp");
        assert_eq!(strip_version_suffix("MyApp"), "MyApp");
        assert_eq!(strip_version_suffix("My-App"), "My-App");
    }

    #[test]
    fn test_extract_desktop_key() {
        let content = "[Desktop Entry]\nName=Test App\nExec=/usr/bin/test --flag\nStartupWMClass=testapp\n\n[Actions]\nNew=new";
        assert_eq!(extract_desktop_key(content, "Name"), Some("Test App".into()));
        assert_eq!(extract_desktop_key(content, "Exec"), Some("/usr/bin/test --flag".into()));
        assert_eq!(extract_desktop_key(content, "StartupWMClass"), Some("testapp".into()));
        // Should not find keys in other sections
        assert_eq!(extract_desktop_key(content, "New"), None);
    }

    #[test]
    fn test_path_type_labels() {
        assert_eq!(PathType::Config.as_str(), "config");
        assert_eq!(PathType::Data.as_str(), "data");
        assert_eq!(PathType::Cache.as_str(), "cache");
        assert_eq!(PathType::Cache.label(), "Cache");
    }

    #[test]
    fn test_confidence_badge() {
        assert_eq!(Confidence::High.badge_class(), "success");
        assert_eq!(Confidence::Medium.badge_class(), "warning");
        assert_eq!(Confidence::Low.badge_class(), "neutral");
    }

    #[test]
    fn test_footprint_summary_totals() {
        let summary = FootprintSummary {
            appimage_size: 100,
            config_size: 10,
            data_size: 20,
            cache_size: 30,
            state_size: 5,
            other_size: 0,
            paths: Vec::new(),
        };
        assert_eq!(summary.total_size(), 165);
        assert_eq!(summary.data_total(), 65);
    }
}
