use crate::types::{PackageName, Version};
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::str::FromStr;

/// A security vulnerability affecting a Python package.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Vulnerability {
    /// Unique vulnerability identifier (GHSA-*, CVE-*, etc.)
    pub id: String,

    /// Vulnerability summary/title
    pub summary: String,

    /// Detailed description
    pub description: Option<String>,

    /// Severity level
    pub severity: Severity,

    /// Affected package versions
    pub affected_versions: Vec<VersionRange>,

    /// Fixed versions
    pub fixed_versions: Vec<Version>,

    /// Reference URLs
    pub references: Vec<String>,

    /// CVSS score if available
    pub cvss_score: Option<f32>,

    /// Date when vulnerability was published
    pub published: Option<DateTime<Utc>>,

    /// Date when vulnerability was last modified
    pub modified: Option<DateTime<Utc>>,

    /// Source of the vulnerability data (e.g., "pypa-zip", "pypi", "osv")
    pub source: Option<String>,
}

/// Severity levels for vulnerabilities.
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum Severity {
    Low,
    Medium,
    High,
    Critical,
}

/// A version range constraint for vulnerability matching.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct VersionRange {
    /// Minimum version (inclusive)
    pub min: Option<Version>,
    /// Maximum version (exclusive)
    pub max: Option<Version>,
    /// Version constraint string
    pub constraint: String,
}

impl VersionRange {
    /// Check if a version is within this range
    pub fn contains(&self, version: &Version) -> bool {
        let min_satisfied = self.min.as_ref().map_or(true, |min| version >= min);
        let max_satisfied = self.max.as_ref().map_or(true, |max| version < max);
        min_satisfied && max_satisfied
    }

    /// Create a new version range
    pub fn new(min: Option<Version>, max: Option<Version>, constraint: String) -> Self {
        Self {
            min,
            max,
            constraint,
        }
    }
}

/// A vulnerability match found during scanning.
#[derive(Debug, Clone)]
pub struct VulnerabilityMatch {
    /// The package that has the vulnerability
    pub package_name: PackageName,
    /// The installed version
    pub installed_version: Version,
    /// The vulnerability details
    pub vulnerability: Vulnerability,
    /// Whether this is a direct or transitive dependency
    pub is_direct: bool,
}

/// A vulnerability database containing advisories and indexed lookups.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct VulnerabilityDatabase {
    /// All advisories in the database
    pub advisories: Vec<Vulnerability>,
    /// Index mapping package names to advisory indices
    pub package_index: HashMap<PackageName, Vec<usize>>,
}

impl Default for VulnerabilityDatabase {
    fn default() -> Self {
        Self::new()
    }
}

impl VulnerabilityDatabase {
    /// Create a new empty vulnerability database
    pub fn new() -> Self {
        Self {
            advisories: Vec::new(),
            package_index: HashMap::new(),
        }
    }

    /// Get advisories for a specific package
    pub fn get_advisories_for_package(&self, package_name: &PackageName) -> Vec<&Vulnerability> {
        if let Some(indices) = self.package_index.get(package_name) {
            indices
                .iter()
                .filter_map(|&index| self.advisories.get(index))
                .collect()
        } else {
            Vec::new()
        }
    }

    /// Get the total number of advisories
    pub fn len(&self) -> usize {
        self.advisories.len()
    }

    /// Check if the database is empty
    pub fn is_empty(&self) -> bool {
        self.advisories.is_empty()
    }

    /// Create a database from a package-to-vulnerabilities mapping
    pub fn from_package_map(map: HashMap<String, Vec<Vulnerability>>) -> Self {
        let mut advisories = Vec::new();
        let mut package_index = HashMap::new();

        for (package_name, vulns) in map {
            let package = PackageName::from_str(&package_name).unwrap(); // Never fails due to Infallible error type
            let start_idx = advisories.len();
            for vuln in vulns {
                advisories.push(vuln);
            }
            let end_idx = advisories.len();
            if start_idx < end_idx {
                package_index.insert(package, (start_idx..end_idx).collect());
            }
        }

        Self {
            advisories,
            package_index,
        }
    }

    /// Merge multiple vulnerability databases into a single database
    /// Vulnerabilities with the same ID are merged, combining their sources
    pub fn merge(databases: Vec<Self>) -> Self {
        if databases.is_empty() {
            return Self::new();
        }

        if databases.len() == 1 {
            return databases.into_iter().next().unwrap();
        }

        // Collect all unique vulnerabilities by ID and track package associations
        let mut vuln_by_id: HashMap<String, Vulnerability> = HashMap::new();
        let mut package_to_vuln_ids: HashMap<PackageName, Vec<String>> = HashMap::new();

        // Process all databases to collect vulnerabilities and package associations
        for database in &databases {
            // Process vulnerabilities from this database
            for (package_name, indices) in &database.package_index {
                for &index in indices {
                    if let Some(vuln) = database.advisories.get(index) {
                        let id = vuln.id.clone();

                        // Merge vulnerability if we've seen this ID before
                        if let Some(existing_vuln) = vuln_by_id.get_mut(&id) {
                            // Merge sources
                            if let (Some(existing_source), Some(new_source)) =
                                (&existing_vuln.source, &vuln.source)
                            {
                                let mut sources: Vec<&str> = existing_source.split(',').collect();
                                if !sources.contains(&new_source.as_str()) {
                                    sources.push(new_source);
                                    sources.sort();
                                    existing_vuln.source = Some(sources.join(","));
                                }
                            } else if existing_vuln.source.is_none() && vuln.source.is_some() {
                                existing_vuln.source = vuln.source.clone();
                            }

                            // Merge other fields (prefer non-empty values)
                            if existing_vuln.description.is_none() && vuln.description.is_some() {
                                existing_vuln.description = vuln.description.clone();
                            }
                            if existing_vuln.cvss_score.is_none() && vuln.cvss_score.is_some() {
                                existing_vuln.cvss_score = vuln.cvss_score;
                            }
                            if existing_vuln.published.is_none() && vuln.published.is_some() {
                                existing_vuln.published = vuln.published;
                            }
                            if existing_vuln.modified.is_none() && vuln.modified.is_some() {
                                existing_vuln.modified = vuln.modified;
                            }

                            // Merge references (remove duplicates)
                            for reference in &vuln.references {
                                if !existing_vuln.references.contains(reference) {
                                    existing_vuln.references.push(reference.clone());
                                }
                            }
                            existing_vuln.references.sort();

                            // Merge affected and fixed versions (remove duplicates)
                            for affected_version in &vuln.affected_versions {
                                if !existing_vuln.affected_versions.contains(affected_version) {
                                    existing_vuln
                                        .affected_versions
                                        .push(affected_version.clone());
                                }
                            }
                            for fixed_version in &vuln.fixed_versions {
                                if !existing_vuln.fixed_versions.contains(fixed_version) {
                                    existing_vuln.fixed_versions.push(fixed_version.clone());
                                }
                            }
                        } else {
                            // First time seeing this vulnerability ID
                            vuln_by_id.insert(id.clone(), vuln.clone());
                        }

                        // Track package association
                        package_to_vuln_ids
                            .entry(package_name.clone())
                            .or_default()
                            .push(id);
                    }
                }
            }
        }

        // Build the final database structure
        let mut all_advisories = Vec::new();
        let mut package_index = HashMap::new();

        // Build advisories list and package index
        for (package_name, vuln_ids) in package_to_vuln_ids {
            let start_index = all_advisories.len();
            let mut added_any = false;

            for vuln_id in vuln_ids {
                if let Some(vuln) = vuln_by_id.get(&vuln_id) {
                    // Only add if not already present in all_advisories
                    if !all_advisories
                        .iter()
                        .any(|v: &Vulnerability| v.id == vuln_id)
                    {
                        all_advisories.push(vuln.clone());
                        added_any = true;
                    }
                }
            }

            let end_index = all_advisories.len();
            if added_any && start_index < end_index {
                package_index.insert(package_name, (start_index..end_index).collect());
            }
        }

        Self {
            advisories: all_advisories,
            package_index,
        }
    }
}

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

    fn create_test_vulnerability(id: &str, source: Option<&str>) -> Vulnerability {
        Vulnerability {
            id: id.to_string(),
            summary: format!("Test vulnerability {id}"),
            description: Some(format!("Description for {id}")),
            severity: Severity::Medium,
            affected_versions: vec![],
            fixed_versions: vec![],
            references: vec![],
            cvss_score: None,
            published: Some(Utc::now()),
            modified: None,
            source: source.map(|s| s.to_string()),
        }
    }

    #[test]
    fn test_merge_empty_databases() {
        let databases = vec![];
        let merged = VulnerabilityDatabase::merge(databases);
        assert!(merged.is_empty());
    }

    #[test]
    fn test_merge_single_database() {
        let mut db = VulnerabilityDatabase::new();
        let vuln = create_test_vulnerability("GHSA-1234", Some("test"));
        db.advisories.push(vuln.clone());

        let package = PackageName::from("test-package");
        db.package_index.insert(package, vec![0]);

        let databases = vec![db.clone()];
        let merged = VulnerabilityDatabase::merge(databases);

        assert_eq!(merged.len(), 1);
        assert_eq!(merged.advisories[0].id, "GHSA-1234");
        assert_eq!(merged.advisories[0].source, Some("test".to_string()));
    }

    #[test]
    fn test_merge_different_vulnerabilities() {
        let mut db1 = VulnerabilityDatabase::new();
        let vuln1 = create_test_vulnerability("GHSA-1234", Some("source1"));
        db1.advisories.push(vuln1);
        let package1 = PackageName::from("package1");
        db1.package_index.insert(package1, vec![0]);

        let mut db2 = VulnerabilityDatabase::new();
        let vuln2 = create_test_vulnerability("GHSA-5678", Some("source2"));
        db2.advisories.push(vuln2);
        let package2 = PackageName::from("package2");
        db2.package_index.insert(package2, vec![0]);

        let databases = vec![db1, db2];
        let merged = VulnerabilityDatabase::merge(databases);

        assert_eq!(merged.len(), 2);

        let ids: Vec<_> = merged.advisories.iter().map(|v| &v.id).collect();
        assert!(ids.contains(&&"GHSA-1234".to_string()));
        assert!(ids.contains(&&"GHSA-5678".to_string()));
    }

    #[test]
    fn test_merge_duplicate_vulnerability_ids() {
        let mut db1 = VulnerabilityDatabase::new();
        let mut vuln1 = create_test_vulnerability("GHSA-1234", Some("source1"));
        vuln1.references = vec!["ref1".to_string()];
        db1.advisories.push(vuln1);
        let package = PackageName::from("test-package");
        db1.package_index.insert(package.clone(), vec![0]);

        let mut db2 = VulnerabilityDatabase::new();
        let mut vuln2 = create_test_vulnerability("GHSA-1234", Some("source2"));
        vuln2.references = vec!["ref2".to_string()];
        vuln2.cvss_score = Some(7.5);
        db2.advisories.push(vuln2);
        db2.package_index.insert(package, vec![0]);

        let databases = vec![db1, db2];
        let merged = VulnerabilityDatabase::merge(databases);

        assert_eq!(merged.len(), 1);
        let merged_vuln = &merged.advisories[0];

        assert_eq!(merged_vuln.id, "GHSA-1234");
        assert_eq!(merged_vuln.source, Some("source1,source2".to_string()));
        assert_eq!(merged_vuln.references.len(), 2);
        assert!(merged_vuln.references.contains(&"ref1".to_string()));
        assert!(merged_vuln.references.contains(&"ref2".to_string()));
        assert_eq!(merged_vuln.cvss_score, Some(7.5));
    }

    #[test]
    fn test_merge_source_deduplication() {
        let mut db1 = VulnerabilityDatabase::new();
        let vuln1 = create_test_vulnerability("GHSA-1234", Some("source1"));
        db1.advisories.push(vuln1);
        let package = PackageName::from("test-package");
        db1.package_index.insert(package.clone(), vec![0]);

        let mut db2 = VulnerabilityDatabase::new();
        let vuln2 = create_test_vulnerability("GHSA-1234", Some("source1")); // Same source
        db2.advisories.push(vuln2);
        db2.package_index.insert(package, vec![0]);

        let databases = vec![db1, db2];
        let merged = VulnerabilityDatabase::merge(databases);

        assert_eq!(merged.len(), 1);
        let merged_vuln = &merged.advisories[0];

        assert_eq!(merged_vuln.id, "GHSA-1234");
        assert_eq!(merged_vuln.source, Some("source1".to_string())); // No duplication
    }

    #[test]
    fn test_merge_preserves_package_associations() {
        let mut db1 = VulnerabilityDatabase::new();
        let vuln1 = create_test_vulnerability("GHSA-1234", Some("source1"));
        db1.advisories.push(vuln1);
        let package1 = PackageName::from("package1");
        db1.package_index.insert(package1.clone(), vec![0]);

        let mut db2 = VulnerabilityDatabase::new();
        let vuln2 = create_test_vulnerability("GHSA-5678", Some("source2"));
        db2.advisories.push(vuln2);
        let package2 = PackageName::from("package2");
        db2.package_index.insert(package2.clone(), vec![0]);

        let databases = vec![db1, db2];
        let merged = VulnerabilityDatabase::merge(databases);

        assert_eq!(merged.package_index.len(), 2);
        assert!(merged.package_index.contains_key(&package1));
        assert!(merged.package_index.contains_key(&package2));

        let package1_vulns = merged.get_advisories_for_package(&package1);
        let package2_vulns = merged.get_advisories_for_package(&package2);

        assert_eq!(package1_vulns.len(), 1);
        assert_eq!(package2_vulns.len(), 1);
        assert_eq!(package1_vulns[0].id, "GHSA-1234");
        assert_eq!(package2_vulns[0].id, "GHSA-5678");
    }
}
