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)]
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().is_none_or(|min| version >= min);
        let max_satisfied = self.max.as_ref().is_none_or(|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,
        }
    }
}
