// SPDX-License-Identifier: MIT

use super::database::{Severity, Vulnerability, VulnerabilityDatabase, VulnerabilityMatch};
use crate::dependency::scanner::{DependencySource, ScannedDependency};
use crate::types::{PackageName, SeverityLevel, Version};
use crate::Result;
use serde::Serialize;
use std::collections::{HashMap, HashSet};
use tracing::{debug, info};

/// Configuration for vulnerability matching
#[derive(Debug, Clone)]
pub struct MatcherConfig {
    /// Minimum severity level to include
    pub min_severity: SeverityLevel,
    /// Vulnerability IDs to ignore
    pub ignore_ids: HashSet<String>,
    /// Vulnerability IDs to ignore only while no fix is available
    pub ignore_while_no_fix: HashSet<String>,
    /// Only check direct dependencies
    pub direct_only: bool,
    /// Include withdrawn vulnerabilities
    pub include_withdrawn: bool,
}

impl MatcherConfig {
    /// Create a new matcher configuration
    pub fn new(
        min_severity: SeverityLevel,
        ignore_ids: Vec<String>,
        ignore_while_no_fix: Vec<String>,
        direct_only: bool,
        include_withdrawn: bool,
    ) -> Self {
        Self {
            min_severity,
            ignore_ids: ignore_ids.into_iter().collect(),
            ignore_while_no_fix: ignore_while_no_fix.into_iter().collect(),
            direct_only,
            include_withdrawn,
        }
    }

    /// Check if a severity level meets the minimum threshold
    pub fn severity_matches(&self, severity: Severity) -> bool {
        if severity == Severity::Unknown {
            return true;
        }
        match self.min_severity {
            SeverityLevel::Low => true,
            SeverityLevel::Medium => severity >= Severity::Medium,
            SeverityLevel::High => severity >= Severity::High,
            SeverityLevel::Critical => severity >= Severity::Critical,
        }
    }

    /// Check if a vulnerability ID should be ignored
    pub fn should_ignore(&self, vulnerability_id: &str) -> bool {
        self.ignore_ids.contains(vulnerability_id)
    }

    /// Check if a vulnerability ID should be ignored while it has no fix
    pub fn should_ignore_while_no_fix(&self, vulnerability_id: &str, has_fix: bool) -> bool {
        if has_fix {
            false
        } else {
            self.ignore_while_no_fix.contains(vulnerability_id)
        }
    }

    pub fn should_include_withdrawn(&self, vulnerability: &Vulnerability) -> bool {
        match vulnerability.withdrawn {
            Some(_) => self.include_withdrawn,
            None => true,
        }
    }
}

/// Engine for matching dependencies against vulnerability database
pub struct VulnerabilityMatcher {
    database: VulnerabilityDatabase,
    config: MatcherConfig,
}

impl VulnerabilityMatcher {
    /// Create a new vulnerability matcher
    pub fn new(database: VulnerabilityDatabase, config: MatcherConfig) -> Self {
        Self { database, config }
    }

    /// Find all vulnerabilities affecting the given dependencies
    #[cfg_attr(feature = "hotpath", hotpath::measure)]
    pub fn find_vulnerabilities(
        &self,
        dependencies: &[ScannedDependency],
    ) -> Result<Vec<VulnerabilityMatch>> {
        info!(
            "Matching {} dependencies against vulnerability database",
            dependencies.len()
        );

        let mut matches = Vec::new();
        let mut stats = MatchingStats::new();

        for dependency in dependencies {
            // Apply direct-only filter
            if self.config.direct_only && !dependency.is_direct {
                continue;
            }

            stats.total_checked += 1;

            // Only check registry dependencies for now
            // TODO: Add support for git/path dependencies
            if !matches!(dependency.source, DependencySource::Registry) {
                stats.non_registry_skipped += 1;
                continue;
            }

            let dependency_matches = self.find_vulnerabilities_for_dependency(dependency);
            stats.vulnerable_packages += usize::from(!dependency_matches.is_empty());
            stats.total_vulnerabilities += dependency_matches.len();

            matches.extend(dependency_matches);
        }

        info!(
            "Vulnerability matching complete: {} vulnerabilities found in {} packages",
            stats.total_vulnerabilities, stats.vulnerable_packages
        );

        debug!("Matching statistics: {:?}", stats);

        Ok(matches)
    }

    /// Find vulnerabilities for a specific dependency
    fn find_vulnerabilities_for_dependency(
        &self,
        dependency: &ScannedDependency,
    ) -> Vec<VulnerabilityMatch> {
        let mut matches = Vec::new();

        // Get potential vulnerabilities for this package
        if let Some(vulnerability_indices) = self.database.package_index.get(&dependency.name) {
            for &vuln_index in vulnerability_indices {
                if let Some(vulnerability) = self.database.advisories.get(vuln_index) {
                    // Check if this vulnerability should be ignored
                    if self.config.should_ignore(&vulnerability.id) {
                        continue;
                    }

                    // Check if this vulnerability should be ignored while no fix is available
                    let has_fix = !vulnerability.fixed_versions.is_empty();
                    if self
                        .config
                        .should_ignore_while_no_fix(&vulnerability.id, has_fix)
                    {
                        continue;
                    }

                    // Check severity threshold
                    if !self.config.severity_matches(vulnerability.severity) {
                        continue;
                    }

                    if !self.config.should_include_withdrawn(vulnerability) {
                        continue;
                    }

                    // Check if the installed version is affected
                    if Self::is_version_affected(&dependency.version, vulnerability) {
                        matches.push(VulnerabilityMatch {
                            package_name: dependency.name.clone(),
                            installed_version: dependency.version.clone(),
                            vulnerability: vulnerability.clone(),
                            is_direct: dependency.is_direct,
                        });
                    }
                }
            }
        }

        matches
    }

    /// Check if a specific version is affected by a vulnerability
    fn is_version_affected(version: &Version, vulnerability: &Vulnerability) -> bool {
        vulnerability
            .affected_versions
            .iter()
            .any(|range| range.contains(version))
    }

    /// Get vulnerability statistics
    pub fn get_database_stats(&self) -> DatabaseStats {
        let mut severity_counts = HashMap::new();
        let mut package_counts = HashMap::new();

        for vulnerability in &self.database.advisories {
            *severity_counts.entry(vulnerability.severity).or_insert(0) += 1;
        }

        for (package_name, vuln_indices) in &self.database.package_index {
            package_counts.insert(package_name.clone(), vuln_indices.len());
        }

        DatabaseStats {
            total_vulnerabilities: self.database.advisories.len(),
            total_packages: self.database.package_index.len(),
            severity_counts,
            packages_with_most_vulns: Self::get_top_vulnerable_packages(&package_counts, 10),
        }
    }

    /// Get packages with the most vulnerabilities
    fn get_top_vulnerable_packages(
        package_counts: &HashMap<PackageName, usize>,
        limit: usize,
    ) -> Vec<(PackageName, usize)> {
        let mut packages: Vec<_> = package_counts
            .iter()
            .map(|(k, v)| (k.clone(), *v))
            .collect();
        packages.sort_by(|a, b| b.1.cmp(&a.1));
        packages.truncate(limit);
        packages
    }

    /// Filter matches based on additional criteria
    pub fn filter_matches(&self, matches: Vec<VulnerabilityMatch>) -> Vec<VulnerabilityMatch> {
        let mut filtered = Vec::new();
        let mut seen_vulnerability_ids = HashSet::new();

        for m in matches {
            // Deduplicate by vulnerability ID + package name
            let key = format!("{}:{}", m.vulnerability.id, m.package_name);
            if seen_vulnerability_ids.contains(&key) {
                continue;
            }
            seen_vulnerability_ids.insert(key);

            filtered.push(m);
        }

        // Sort by severity (highest first), then by package name
        filtered.sort_by(|a, b| {
            b.vulnerability
                .severity
                .cmp(&a.vulnerability.severity)
                .then_with(|| a.package_name.cmp(&b.package_name))
        });

        filtered
    }

    /// Check if any vulnerabilities have fixes available
    pub fn analyze_fixes(&self, matches: &[VulnerabilityMatch]) -> FixAnalysis {
        let mut analysis = FixAnalysis {
            total_matches: matches.len(),
            fixable: 0,
            unfixable: 0,
            fix_suggestions: Vec::new(),
        };

        for m in matches {
            if !m.vulnerability.fixed_versions.is_empty() {
                analysis.fixable += 1;

                let fix_version = m
                    .vulnerability
                    .fixed_versions
                    .iter()
                    .filter(|v| *v > &m.installed_version)
                    .min()
                    .or_else(|| m.vulnerability.fixed_versions.first());

                if let Some(fix_version) = fix_version {
                    analysis.fix_suggestions.push(FixSuggestion {
                        package_name: m.package_name.clone(),
                        current_version: m.installed_version.clone(),
                        suggested_version: fix_version.clone(),
                        vulnerability_id: m.vulnerability.id.clone(),
                    });
                }
            } else {
                analysis.unfixable += 1;
            }
        }

        analysis
    }
}

/// Statistics about vulnerability matching
#[derive(Debug)]
struct MatchingStats {
    total_checked: usize,
    vulnerable_packages: usize,
    total_vulnerabilities: usize,
    non_registry_skipped: usize,
}

impl MatchingStats {
    fn new() -> Self {
        Self {
            total_checked: 0,
            vulnerable_packages: 0,
            total_vulnerabilities: 0,
            non_registry_skipped: 0,
        }
    }
}

/// Statistics about the vulnerability database
#[derive(Debug, Clone)]
pub struct DatabaseStats {
    pub total_vulnerabilities: usize,
    pub total_packages: usize,
    pub severity_counts: HashMap<Severity, usize>,
    pub packages_with_most_vulns: Vec<(PackageName, usize)>,
}

impl std::fmt::Display for DatabaseStats {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(
            f,
            "Database: {} vulnerabilities across {} packages",
            self.total_vulnerabilities, self.total_packages
        )?;

        if !self.severity_counts.is_empty() {
            write!(f, " (")?;
            let mut first = true;
            for (severity, count) in &self.severity_counts {
                if !first {
                    write!(f, ", ")?;
                }
                write!(f, "{severity:?}: {count}")?;
                first = false;
            }
            write!(f, ")")?;
        }

        Ok(())
    }
}

/// Analysis of available fixes for vulnerabilities
#[derive(Debug, Clone)]
pub struct FixAnalysis {
    pub total_matches: usize,
    pub fixable: usize,
    pub unfixable: usize,
    pub fix_suggestions: Vec<FixSuggestion>,
}

/// A suggestion for fixing a vulnerability
#[derive(Debug, Clone, Serialize)]
pub struct FixSuggestion {
    pub package_name: PackageName,
    pub current_version: Version,
    pub suggested_version: Version,
    pub vulnerability_id: String,
}

impl std::fmt::Display for FixSuggestion {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(
            f,
            "{}: {} → {} (fixes {})",
            self.package_name, self.current_version, self.suggested_version, self.vulnerability_id
        )
    }
}

impl std::fmt::Display for FixAnalysis {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(
            f,
            "Fix analysis: {} fixable, {} unfixable out of {} total vulnerabilities",
            self.fixable, self.unfixable, self.total_matches
        )
    }
}

#[cfg(test)]
mod tests {
    use super::super::database::VersionRange;
    use super::*;
    use std::collections::HashMap;
    use std::str::FromStr;

    fn create_test_vulnerability() -> Vulnerability {
        Vulnerability {
            id: "TEST-2023-001".to_string(),
            summary: "Test vulnerability".to_string(),
            description: Some("A test vulnerability for unit tests".to_string()),
            severity: Severity::High,
            affected_versions: vec![VersionRange {
                min: Some(Version::from_str("1.0.0").unwrap()),
                max: Some(Version::from_str("1.5.0").unwrap()),
                constraint: ">=1.0.0,<1.5.0".to_string(),
                max_inclusive: false,
            }],
            fixed_versions: vec![Version::from_str("1.5.0").unwrap()],
            references: vec!["https://example.com/advisory".to_string()],
            cvss_score: Some(7.5),
            cvss_version: None,
            published: None,
            modified: None,
            source: Some("test".to_string()),
            withdrawn: None,
            aliases: vec![],
        }
    }

    fn create_test_database() -> VulnerabilityDatabase {
        let vulnerability = create_test_vulnerability();
        let mut package_index = HashMap::new();
        package_index.insert(
            PackageName::from_str("vulnerable-package").unwrap(),
            vec![0],
        );

        VulnerabilityDatabase {
            advisories: vec![vulnerability],
            package_index,
        }
    }

    #[test]
    fn test_matcher_config() {
        let config = MatcherConfig::new(
            SeverityLevel::High,
            vec!["IGNORE-001".to_string()],
            vec![],
            true,
            false,
        );

        assert!(config.severity_matches(Severity::High));
        assert!(config.severity_matches(Severity::Critical));
        assert!(!config.severity_matches(Severity::Medium));
        assert!(!config.severity_matches(Severity::Low));
        assert!(config.severity_matches(Severity::Unknown));

        assert!(config.should_ignore("IGNORE-001"));
        assert!(!config.should_ignore("INCLUDE-001"));
    }

    #[test]
    fn test_severity_matches_always_shows_unknown() {
        let config_critical =
            MatcherConfig::new(SeverityLevel::Critical, vec![], vec![], false, false);
        assert!(config_critical.severity_matches(Severity::Unknown));

        let config_low = MatcherConfig::new(SeverityLevel::Low, vec![], vec![], false, false);
        assert!(config_low.severity_matches(Severity::Unknown));
    }

    #[test]
    fn test_vulnerability_matching() {
        let database = create_test_database();
        let config = MatcherConfig::new(SeverityLevel::Low, vec![], vec![], false, false);
        let matcher = VulnerabilityMatcher::new(database, config);

        let dependencies = vec![
            ScannedDependency {
                name: PackageName::from_str("vulnerable-package").unwrap(),
                version: Version::from_str("1.2.0").unwrap(), // In vulnerable range
                is_direct: true,
                source: DependencySource::Registry,
                path: None,
                source_file: None,
            },
            ScannedDependency {
                name: PackageName::from_str("safe-package").unwrap(),
                version: Version::from_str("2.0.0").unwrap(),
                is_direct: true,
                source: DependencySource::Registry,
                path: None,
                source_file: None,
            },
        ];

        let matches = matcher.find_vulnerabilities(&dependencies).unwrap();
        assert_eq!(matches.len(), 1);
        assert_eq!(matches[0].package_name.to_string(), "vulnerable-package");
        assert_eq!(matches[0].vulnerability.id, "TEST-2023-001");
    }

    #[test]
    fn test_version_range_matching() {
        let database = create_test_database();
        let config = MatcherConfig::new(SeverityLevel::Low, vec![], vec![], false, false);
        let matcher = VulnerabilityMatcher::new(database, config);

        let vulnerability = &matcher.database.advisories[0];

        // Test vulnerable version
        assert!(VulnerabilityMatcher::is_version_affected(
            &Version::from_str("1.2.0").unwrap(),
            vulnerability
        ));

        // Test safe version (before range)
        assert!(!VulnerabilityMatcher::is_version_affected(
            &Version::from_str("0.9.0").unwrap(),
            vulnerability
        ));

        // Test safe version (after fix)
        assert!(!VulnerabilityMatcher::is_version_affected(
            &Version::from_str("1.5.0").unwrap(),
            vulnerability
        ));
    }

    #[test]
    fn test_fix_analysis() {
        let database = create_test_database();
        let config = MatcherConfig::new(SeverityLevel::Low, vec![], vec![], false, false);
        let matcher = VulnerabilityMatcher::new(database, config);

        let matches = vec![VulnerabilityMatch {
            package_name: PackageName::from_str("vulnerable-package").unwrap(),
            installed_version: Version::from_str("1.2.0").unwrap(),
            vulnerability: create_test_vulnerability(),
            is_direct: true,
        }];

        let analysis = matcher.analyze_fixes(&matches);
        assert_eq!(analysis.total_matches, 1);
        assert_eq!(analysis.fixable, 1);
        assert_eq!(analysis.unfixable, 0);
        assert_eq!(analysis.fix_suggestions.len(), 1);
    }

    #[test]
    fn test_ignore_when_no_fix() {
        let mut vulnerability_with_fix = create_test_vulnerability();
        vulnerability_with_fix.id = "CVE-2025-1234".to_string();
        vulnerability_with_fix.fixed_versions = vec![Version::from_str("1.5.0").unwrap()];

        let mut vulnerability_without_fix = create_test_vulnerability();
        vulnerability_without_fix.id = "CVE-2025-5678".to_string();
        vulnerability_without_fix.fixed_versions = vec![];

        let mut package_index = HashMap::new();
        package_index.insert(PackageName::from_str("test-package").unwrap(), vec![0, 1]);

        let database = VulnerabilityDatabase {
            advisories: vec![vulnerability_with_fix, vulnerability_without_fix],
            package_index,
        };

        let config_ignore_unfixable = MatcherConfig::new(
            SeverityLevel::Low,
            vec![],
            vec!["CVE-2025-5678".to_string()],
            false,
            false,
        );
        let matcher_ignore_unfixable =
            VulnerabilityMatcher::new(database.clone(), config_ignore_unfixable);

        let dependencies = vec![ScannedDependency {
            name: PackageName::from_str("test-package").unwrap(),
            version: Version::from_str("1.2.0").unwrap(),
            is_direct: true,
            source: DependencySource::Registry,
            path: None,
            source_file: None,
        }];

        let matches = matcher_ignore_unfixable
            .find_vulnerabilities(&dependencies)
            .unwrap();

        assert_eq!(matches.len(), 1);
        assert_eq!(matches[0].vulnerability.id, "CVE-2025-1234");

        let config_no_ignore = MatcherConfig::new(SeverityLevel::Low, vec![], vec![], false, false);
        let matcher_no_ignore = VulnerabilityMatcher::new(database, config_no_ignore);
        let matches_all = matcher_no_ignore
            .find_vulnerabilities(&dependencies)
            .unwrap();

        assert_eq!(matches_all.len(), 2);
    }

    #[test]
    fn test_ignore_when_no_fix_does_not_ignore_fixable() {
        let mut vulnerability_with_fix = create_test_vulnerability();
        vulnerability_with_fix.id = "CVE-2025-9999".to_string();
        vulnerability_with_fix.fixed_versions = vec![Version::from_str("1.5.0").unwrap()];

        let mut package_index = HashMap::new();
        package_index.insert(PackageName::from_str("test-package").unwrap(), vec![0]);

        let database = VulnerabilityDatabase {
            advisories: vec![vulnerability_with_fix],
            package_index,
        };

        let config = MatcherConfig::new(
            SeverityLevel::Low,
            vec![],
            vec!["CVE-2025-9999".to_string()],
            false,
            false,
        );
        let matcher = VulnerabilityMatcher::new(database, config);

        let dependencies = vec![ScannedDependency {
            name: PackageName::from_str("test-package").unwrap(),
            version: Version::from_str("1.2.0").unwrap(),
            is_direct: true,
            source: DependencySource::Registry,
            path: None,
            source_file: None,
        }];

        let matches = matcher.find_vulnerabilities(&dependencies).unwrap();

        assert_eq!(matches.len(), 1);
        assert_eq!(matches[0].vulnerability.id, "CVE-2025-9999");
    }

    #[test]
    fn test_withdrawn_vulnerability_filtering() {
        use chrono::Utc;

        let mut withdrawn_vulnerability = create_test_vulnerability();
        withdrawn_vulnerability.id = "WITHDRAWN-2023-001".to_string();
        withdrawn_vulnerability.withdrawn = Some(Utc::now());

        let mut package_index = HashMap::new();
        package_index.insert(PackageName::from_str("test-package").unwrap(), vec![0]);

        let database = VulnerabilityDatabase {
            advisories: vec![withdrawn_vulnerability],
            package_index,
        };

        let config_exclude = MatcherConfig::new(SeverityLevel::Low, vec![], vec![], false, false);
        let matcher_exclude = VulnerabilityMatcher::new(database.clone(), config_exclude);

        let dependencies = vec![ScannedDependency {
            name: PackageName::from_str("test-package").unwrap(),
            version: Version::from_str("1.2.0").unwrap(),
            is_direct: true,
            source: DependencySource::Registry,
            path: None,
            source_file: None,
        }];

        let matches_exclude = matcher_exclude.find_vulnerabilities(&dependencies).unwrap();
        assert_eq!(matches_exclude.len(), 0);

        let config_include = MatcherConfig::new(SeverityLevel::Low, vec![], vec![], false, true);
        let matcher_include = VulnerabilityMatcher::new(database, config_include);

        let matches_include = matcher_include.find_vulnerabilities(&dependencies).unwrap();
        assert_eq!(matches_include.len(), 1);
        assert!(matches_include[0].vulnerability.withdrawn.is_some());
    }
}
