// SPDX-License-Identifier: MIT

use crate::cache::CacheEntry;
use crate::types::Version;
use async_trait::async_trait;
use chrono::{DateTime, Utc};
use futures::stream::{FuturesUnordered, StreamExt};
use indicatif::{ProgressBar, ProgressStyle};
use serde::{Deserialize, Serialize};
use std::collections::{HashMap, HashSet};
use std::str::FromStr;
use std::sync::Arc;
use tracing::{debug, warn};

use super::retry::{is_http_error_retryable, retry_with_backoff};
use crate::{
    AuditCache, AuditError, Result, Severity, VersionRange, Vulnerability, VulnerabilityDatabase,
    VulnerabilityProvider,
};

/// The OSV API base URL for fetching vulnerability data
const OSV_API_BASE: &str = "https://api.osv.dev/v1";

/// An OSV (Open Source Vulnerabilities) advisory record
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OsvAdvisory {
    /// Unique vulnerability identifier
    pub id: String,

    /// Vulnerability summary (optional - not all OSV records have this)
    pub summary: Option<String>,

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

    /// Affected packages and versions (defaults to empty if missing)
    #[serde(default)]
    pub affected: Vec<OsvAffected>,

    /// Reference URLs (optional - defaults to empty if missing)
    #[serde(default)]
    pub references: Vec<OsvReference>,

    /// Severity information (optional - not all OSV records have this)
    #[serde(default)]
    pub severity: Vec<OsvSeverity>,

    /// Publication timestamp
    pub published: Option<String>,

    /// Last modification timestamp
    pub modified: Option<String>,

    /// Database-specific fields
    pub database_specific: Option<serde_json::Value>,

    /// Aliases for this vulnerability (e.g., CVE-*, GHSA-*, PYSEC-*)
    #[serde(default)]
    pub aliases: Vec<String>,
}

/// Affected package information in an OSV advisory
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OsvAffected {
    /// Package information
    pub package: OsvPackage,

    /// Version ranges affected
    #[serde(default)]
    pub ranges: Vec<OsvRange>,

    /// Specific versions affected
    pub versions: Option<Vec<String>>,

    /// Ecosystem-specific database information
    pub database_specific: Option<serde_json::Value>,

    /// Ecosystem-specific fields
    pub ecosystem_specific: Option<serde_json::Value>,
}

/// Package information in OSV format
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OsvPackage {
    /// Package ecosystem (e.g., "PyPI")
    pub ecosystem: String,

    /// Package name
    pub name: String,

    /// Package URL if available
    pub purl: Option<String>,
}

/// Version range specification in OSV format
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OsvRange {
    /// Range type (e.g., "ECOSYSTEM")
    #[serde(rename = "type")]
    pub range_type: String,

    /// Repository URL for version control ranges
    pub repo: Option<String>,

    /// Events defining the range (introduced, fixed, etc.)
    #[serde(default)]
    pub events: Vec<OsvEvent>,

    /// Database-specific information
    pub database_specific: Option<serde_json::Value>,
}

/// A version event in an OSV range
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OsvEvent {
    /// Version where event occurs
    pub introduced: Option<String>,

    /// Version where issue is fixed
    pub fixed: Option<String>,

    /// Last affected version
    pub last_affected: Option<String>,

    /// Version limit
    pub limit: Option<String>,
}

/// Reference information in OSV format
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OsvReference {
    /// Reference type (e.g., "ADVISORY", "FIX", "WEB")
    #[serde(rename = "type")]
    pub ref_type: String,

    /// Reference URL
    pub url: String,
}

/// Severity information in OSV format
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OsvSeverity {
    /// Severity type (e.g., `CVSS_V3`)
    #[serde(rename = "type")]
    pub severity_type: String,

    /// Severity score
    pub score: String,
}

/// OSV.dev API source for vulnerability data
pub struct OsvSource {
    cache: AuditCache,
    no_cache: bool,
    client: reqwest::Client,
    http_config: crate::config::HttpConfig,
    vulnerability_ttl: u64,
}

impl OsvSource {
    /// Create a new OSV source with HTTP configuration
    pub fn new(
        cache: AuditCache,
        no_cache: bool,
        http_config: crate::config::HttpConfig,
        vulnerability_ttl: u64,
    ) -> Self {
        let client = reqwest::Client::builder()
            .timeout(std::time::Duration::from_secs(http_config.timeout))
            .connect_timeout(std::time::Duration::from_secs(http_config.connect_timeout))
            .build()
            .unwrap_or_default();

        Self {
            cache,
            no_cache,
            client,
            http_config,
            vulnerability_ttl,
        }
    }

    /// Get cache entry for OSV batch with package-specific key
    fn cache_entry(&self, packages: &[(String, String)]) -> CacheEntry {
        use std::collections::hash_map::DefaultHasher;
        use std::hash::{Hash, Hasher};

        // Create a hash of the package set to differentiate cache entries
        let mut hasher = DefaultHasher::new();
        for (name, version) in packages {
            name.hash(&mut hasher);
            version.hash(&mut hasher);
        }
        let package_hash = hasher.finish();

        self.cache.database_entry(&format!("osv-{package_hash:x}"))
    }

    /// Convert OSV advisory to internal vulnerability format for a specific package
    fn convert_osv_vulnerability(
        advisory: &OsvAdvisory,
        target_package: &str,
    ) -> Option<Vulnerability> {
        if advisory.affected.is_empty() {
            return None;
        }

        let (severity, cvss_score, cvss_version) = Self::map_osv_severity(advisory);

        let target_normalized = target_package.to_lowercase().replace(['-', '_', '.'], "-");

        let mut affected_versions = Vec::new();
        let mut fixed_versions = Vec::new();

        for affected in &advisory.affected {
            if affected.package.ecosystem != "PyPI" {
                continue;
            }
            let pkg_normalized = affected
                .package
                .name
                .to_lowercase()
                .replace(['-', '_', '.'], "-");
            if pkg_normalized != target_normalized {
                continue;
            }
            affected_versions.extend(Self::extract_osv_ranges(affected));
            fixed_versions.extend(Self::extract_fixed_versions(affected));
        }

        if affected_versions.is_empty() && fixed_versions.is_empty() {
            return None;
        }

        // Build references
        let mut references: Vec<String> =
            advisory.references.iter().map(|r| r.url.clone()).collect();
        references.push(format!("https://osv.dev/vulnerability/{}", advisory.id));

        let aliases = advisory.aliases.clone();

        Some(Vulnerability {
            id: advisory.id.clone(),
            summary: match (&advisory.summary, &advisory.details) {
                (Some(summary), _) if !summary.is_empty() => summary.clone(),
                (_, Some(details)) => {
                    let summary = details.split('.').next().unwrap_or(details);
                    let truncated = super::truncate_chars(summary, 100);
                    if truncated.len() < summary.len() {
                        format!("{truncated}...")
                    } else {
                        summary.to_string()
                    }
                }
                _ => "OSV Advisory".to_string(),
            },
            description: advisory.details.clone(),
            severity,
            affected_versions,
            fixed_versions,
            references,
            cvss_score,
            cvss_version,
            published: advisory.published.as_ref().and_then(|s| {
                DateTime::parse_from_rfc3339(s)
                    .ok()
                    .map(|dt| dt.with_timezone(&Utc))
            }),
            modified: advisory.modified.as_ref().and_then(|s| {
                DateTime::parse_from_rfc3339(s)
                    .ok()
                    .map(|dt| dt.with_timezone(&Utc))
            }),
            source: Some("osv".to_string()),
            withdrawn: None,
            aliases,
        })
    }

    /// Map OSV severity to internal severity and extract CVSS score + version
    fn map_osv_severity(advisory: &OsvAdvisory) -> (Severity, Option<f32>, Option<u8>) {
        let best = super::extract_best_cvss_score(
            advisory
                .severity
                .iter()
                .map(|s| (s.severity_type.as_str(), s.score.as_str())),
        );

        match best {
            Some((score, version)) => {
                (Severity::from_cvss_score(score), Some(score), Some(version))
            }
            None => (Severity::Unknown, None, None),
        }
    }

    /// Extract version ranges from OSV affected entry
    fn extract_osv_ranges(affected: &OsvAffected) -> Vec<VersionRange> {
        let mut ranges = vec![];

        for range in &affected.ranges {
            if range.range_type != "ECOSYSTEM" && range.range_type != "SEMVER" {
                continue;
            }

            let mut min_version = None;
            let mut max_version = None;
            let mut has_fixed = false;

            for event in &range.events {
                if let Some(intro) = &event.introduced {
                    if intro != "0" {
                        if let Ok(version) = Version::from_str(intro) {
                            min_version = Some(version);
                        }
                    }
                }
                if let Some(fix) = &event.fixed {
                    if let Ok(version) = Version::from_str(fix) {
                        max_version = Some(version);
                        has_fixed = true;
                    }
                }
                if let Some(last) = &event.last_affected {
                    if !has_fixed {
                        if let Ok(version) = Version::from_str(last) {
                            max_version = Some(version);
                        }
                    }
                }
            }

            let max_inclusive = !has_fixed && max_version.is_some();

            let constraint = match (&min_version, &max_version, max_inclusive) {
                (Some(min), Some(max), true) => format!(">={min},<={max}"),
                (Some(min), Some(max), false) => format!(">={min},<{max}"),
                (Some(min), None, _) => format!(">={min}"),
                (None, Some(max), true) => format!("<={max}"),
                (None, Some(max), false) => format!("<{max}"),
                (None, None, _) => "*".to_string(),
            };

            ranges.push(VersionRange {
                min: min_version,
                max: max_version,
                constraint,
                max_inclusive,
            });
        }

        ranges
    }

    /// Extract fixed versions from OSV affected entry
    fn extract_fixed_versions(affected: &OsvAffected) -> Vec<Version> {
        let mut fixed_versions = vec![];

        for range in &affected.ranges {
            for event in &range.events {
                if let Some(fixed) = &event.fixed {
                    if let Ok(version) = Version::from_str(fixed) {
                        fixed_versions.push(version);
                    }
                }
            }
        }

        fixed_versions
    }

    /// Recovery for malformed OSV JSON
    fn recover_from_malformed_json(
        value: &serde_json::Value,
        vuln_id: &str,
    ) -> Option<OsvAdvisory> {
        let obj = value.as_object()?;

        // Extract basic fields with fallbacks
        let id = obj
            .get("id")
            .and_then(|v| v.as_str())
            .unwrap_or(vuln_id)
            .to_string();

        let summary = obj
            .get("summary")
            .and_then(|v| v.as_str())
            .map(|s| s.to_string());

        let details = obj
            .get("details")
            .and_then(|v| v.as_str())
            .map(|s| s.to_string());

        // Affected package extraction - just get what we can
        let mut affected = Vec::new();
        if let Some(affected_array) = obj.get("affected").and_then(|v| v.as_array()) {
            for item in affected_array {
                if let Some(item_obj) = item.as_object() {
                    // Try to extract package name and ecosystem
                    if let Some(pkg) = item_obj.get("package").and_then(|p| p.as_object()) {
                        let package = OsvPackage {
                            ecosystem: pkg
                                .get("ecosystem")
                                .and_then(|v| v.as_str())
                                .unwrap_or("PyPI")
                                .to_string(),
                            name: pkg
                                .get("name")
                                .and_then(|v| v.as_str())
                                .unwrap_or("unknown")
                                .to_string(),
                            purl: pkg
                                .get("purl")
                                .and_then(|v| v.as_str())
                                .map(|s| s.to_string()),
                        };

                        // Simple range extraction - just create empty ranges if malformed
                        let ranges = item_obj
                            .get("ranges")
                            .and_then(|r| r.as_array())
                            .map(|arr| {
                                arr.iter()
                                    .filter_map(|range| {
                                        range.as_object().map(|r| OsvRange {
                                            range_type: r
                                                .get("type")
                                                .and_then(|v| v.as_str())
                                                .unwrap_or("ECOSYSTEM")
                                                .to_string(),
                                            repo: r
                                                .get("repo")
                                                .and_then(|v| v.as_str())
                                                .map(|s| s.to_string()),
                                            events: r
                                                .get("events")
                                                .and_then(|e| e.as_array())
                                                .map(|events_arr| {
                                                    events_arr
                                                        .iter()
                                                        .filter_map(|e| e.as_object())
                                                        .map(|e_obj| OsvEvent {
                                                            introduced: e_obj
                                                                .get("introduced")
                                                                .and_then(|v| v.as_str())
                                                                .map(|s| s.to_string()),
                                                            fixed: e_obj
                                                                .get("fixed")
                                                                .and_then(|v| v.as_str())
                                                                .map(|s| s.to_string()),
                                                            last_affected: e_obj
                                                                .get("last_affected")
                                                                .and_then(|v| v.as_str())
                                                                .map(|s| s.to_string()),
                                                            limit: e_obj
                                                                .get("limit")
                                                                .and_then(|v| v.as_str())
                                                                .map(|s| s.to_string()),
                                                        })
                                                        .collect()
                                                })
                                                .unwrap_or_default(),
                                            database_specific: r.get("database_specific").cloned(),
                                        })
                                    })
                                    .collect()
                            })
                            .unwrap_or_default();

                        affected.push(OsvAffected {
                            package,
                            ranges,
                            versions: item_obj.get("versions").and_then(|v| v.as_array()).map(
                                |arr| {
                                    arr.iter()
                                        .filter_map(|v| v.as_str().map(|s| s.to_string()))
                                        .collect()
                                },
                            ),
                            database_specific: item_obj.get("database_specific").cloned(),
                            ecosystem_specific: item_obj.get("ecosystem_specific").cloned(),
                        });
                    }
                }
            }
        }

        // Create minimal affected entry if none found
        if affected.is_empty() {
            affected.push(OsvAffected {
                package: OsvPackage {
                    ecosystem: "PyPI".to_string(),
                    name: "unknown".to_string(),
                    purl: None,
                },
                ranges: vec![],
                versions: None,
                database_specific: None,
                ecosystem_specific: None,
            });
        }

        // Simple reference extraction
        let references = obj
            .get("references")
            .and_then(|r| r.as_array())
            .map(|arr| {
                arr.iter()
                    .filter_map(|ref_item| {
                        ref_item.as_object().map(|r| OsvReference {
                            ref_type: r
                                .get("type")
                                .and_then(|v| v.as_str())
                                .unwrap_or("WEB")
                                .to_string(),
                            url: r
                                .get("url")
                                .and_then(|v| v.as_str())
                                .unwrap_or("")
                                .to_string(),
                        })
                    })
                    .collect()
            })
            .unwrap_or_default();

        let severity = obj
            .get("severity")
            .and_then(|s| s.as_array())
            .map(|arr| {
                arr.iter()
                    .filter_map(|sev_item| {
                        sev_item.as_object().map(|s| OsvSeverity {
                            severity_type: s
                                .get("type")
                                .and_then(|v| v.as_str())
                                .unwrap_or("UNKNOWN")
                                .to_string(),
                            score: s
                                .get("score")
                                .and_then(|v| v.as_str())
                                .unwrap_or("0.0")
                                .to_string(),
                        })
                    })
                    .collect()
            })
            .unwrap_or_default();

        let aliases = obj
            .get("aliases")
            .and_then(|a| a.as_array())
            .map(|arr| {
                arr.iter()
                    .filter_map(|v| v.as_str().map(|s| s.to_string()))
                    .collect()
            })
            .unwrap_or_default();

        Some(OsvAdvisory {
            id,
            summary,
            details,
            affected,
            references,
            severity,
            published: obj
                .get("published")
                .and_then(|v| v.as_str())
                .map(|s| s.to_string()),
            modified: obj
                .get("modified")
                .and_then(|v| v.as_str())
                .map(|s| s.to_string()),
            database_specific: obj.get("database_specific").cloned(),
            aliases,
        })
    }

    /// Fetch full vulnerability details for a specific vulnerability ID
    async fn fetch_vulnerability_details(&self, vuln_id: &str) -> Result<Option<OsvAdvisory>> {
        let response = self
            .client
            .get(format!("{OSV_API_BASE}/vulns/{vuln_id}"))
            .send()
            .await
            .map_err(|e| AuditError::DatabaseDownload(Box::new(e)))?;

        if response.status() == 404 {
            return Ok(None);
        }

        if !response.status().is_success() {
            warn!(
                "OSV API returned error {} for vulnerability {}",
                response.status(),
                vuln_id
            );
            return Ok(None);
        }

        // Get the raw response text first to debug parsing issues
        let response_text = response
            .text()
            .await
            .map_err(|e| AuditError::DatabaseDownload(Box::new(e)))?;

        // Try to parse the JSON response normally first
        match serde_json::from_str::<OsvAdvisory>(&response_text) {
            Ok(advisory) => Ok(Some(advisory)),
            Err(e) => {
                warn!(
                    "Failed to parse OSV response for vulnerability {}: {}",
                    vuln_id, e
                );
                let truncated_response = super::truncate_chars(&response_text, 200);
                debug!("Raw response (first 200 chars): {}", truncated_response);
                // Try to recover data from malformed JSON
                if let Ok(generic) = serde_json::from_str::<serde_json::Value>(&response_text) {
                    if let Some(advisory) = Self::recover_from_malformed_json(&generic, vuln_id) {
                        debug!(
                            "Successfully recovered data from malformed OSV response for {}",
                            vuln_id
                        );
                        return Ok(Some(advisory));
                    }
                }

                // If all recovery attempts fail, skip this vulnerability
                debug!(
                    "Unable to recover data from malformed OSV response for {}",
                    vuln_id
                );
                Ok(None)
            }
        }
    }

    /// Create a future for fetching vulnerability details with retry
    async fn fetch_vulnerability_future(
        &self,
        vuln_id: String,
    ) -> (String, Result<Option<OsvAdvisory>>) {
        let vuln_id_clone = vuln_id.clone();
        let context = format!("OSV vulnerability {}", vuln_id);

        let result = retry_with_backoff(
            self.http_config.max_retries,
            self.http_config.retry_initial_backoff,
            self.http_config.retry_max_backoff,
            is_http_error_retryable,
            || self.fetch_vulnerability_details(&vuln_id_clone),
            &context,
        )
        .await
        .map_err(|err| AuditError::DatabaseDownloadDetailed {
            resource: format!("OSV vulnerability {}", vuln_id_clone),
            url: format!("{}/vulns/{}", OSV_API_BASE, vuln_id_clone),
            source: Box::new(err),
        });

        (vuln_id, result)
    }

    /// Query OSV batch API with retry logic
    async fn query_batch(&self, batch: &[OsvQuery]) -> Result<OsvBatchResponse> {
        let batch_len = batch.len();
        let batch_clone = batch.to_vec();
        let context = format!("OSV batch query ({} packages)", batch_len);

        retry_with_backoff(
            self.http_config.max_retries,
            self.http_config.retry_initial_backoff,
            self.http_config.retry_max_backoff,
            is_http_error_retryable,
            || async {
                let request = OsvBatchRequest {
                    queries: batch_clone.clone(),
                };

                let response = self
                    .client
                    .post(format!("{OSV_API_BASE}/querybatch"))
                    .json(&request)
                    .send()
                    .await?;

                if !response.status().is_success() {
                    return Err(AuditError::other(format!(
                        "OSV API returned error: {}",
                        response.status()
                    )));
                }

                let response_text = response.text().await?;
                let batch_response: OsvBatchResponse = serde_json::from_str(&response_text)?;

                Ok(batch_response)
            },
            &context,
        )
        .await
        .map_err(|err| AuditError::DatabaseDownloadDetailed {
            resource: format!("OSV batch query ({} packages)", batch_len),
            url: format!("{}/querybatch", OSV_API_BASE),
            source: Box::new(err),
        })
    }

    fn should_cache_result(
        db: &VulnerabilityDatabase,
        expected_vuln_count: usize,
        no_cache: bool,
    ) -> bool {
        if no_cache {
            return false;
        }
        if db.is_empty() && expected_vuln_count > 0 {
            return false;
        }
        true
    }
}

#[async_trait]
impl VulnerabilityProvider for OsvSource {
    fn name(&self) -> &'static str {
        "osv"
    }

    #[cfg_attr(feature = "hotpath", hotpath::measure)]
    async fn fetch_vulnerabilities(
        &self,
        packages: &[(String, String)],
    ) -> Result<VulnerabilityDatabase> {
        use crate::cache::Freshness;
        use std::time::Duration;

        let cache_entry = self.cache_entry(packages);
        let ttl = Duration::from_secs(self.vulnerability_ttl * 3600);

        // Check cache freshness first unless no_cache is set
        let cache_is_fresh = if self.no_cache {
            false
        } else {
            matches!(cache_entry.freshness(ttl), Ok(Freshness::Fresh))
        };

        if cache_is_fresh {
            if let Ok(content) = fs_err::read(cache_entry.path()) {
                if let Ok(db) = serde_json::from_slice::<VulnerabilityDatabase>(&content) {
                    debug!(
                        "Using cached OSV vulnerabilities (TTL: {} hours)",
                        self.vulnerability_ttl
                    );
                    return Ok(db);
                }
            }
        }

        // Build batch query with version constraints
        let queries: Vec<OsvQuery> = packages
            .iter()
            .map(|(name, version)| {
                debug!(
                    "Building OSV query for package: {} version: {}",
                    name, version
                );
                OsvQuery {
                    package: Some(OsvPackage {
                        name: name.clone(),
                        ecosystem: "PyPI".to_string(),
                        purl: None,
                    }),
                    version: Some(version.clone()),
                }
            })
            .collect();

        debug!("Built {} OSV queries total", queries.len());

        // Split into batches of 1000 (OSV API limit)
        const BATCH_SIZE: usize = 1000;
        let mut all_vulnerability_ids = Vec::new();
        let mut package_vuln_mapping: HashMap<String, HashSet<String>> = HashMap::new();

        let batches: Vec<_> = queries.chunks(BATCH_SIZE).collect();
        let total_batches = batches.len();

        // Create progress bar for batch queries if enabled
        let batch_pb = if self.http_config.show_progress && total_batches > 1 {
            let bar = ProgressBar::new(total_batches as u64);
            bar.set_style(
                ProgressStyle::default_bar()
                    .template("{msg}\n{spinner:.green} [{elapsed_precise}] [{wide_bar:.cyan/blue}] {pos}/{len} batches")
                    .unwrap()
                    .progress_chars("#>-"),
            );
            bar.set_message("Querying OSV for vulnerability IDs");
            Some(Arc::new(bar))
        } else {
            None
        };

        for batch in batches {
            debug!("Querying OSV API with {} packages", batch.len());

            match self.query_batch(batch).await {
                Ok(batch_response) => {
                    debug!(
                        "Successfully parsed batch response with {} results",
                        batch_response.results.len()
                    );

                    // Collect vulnerability IDs and map them to packages
                    for (idx, result) in batch_response.results.into_iter().enumerate() {
                        let package_name = if let Some(query) = batch.get(idx) {
                            query
                                .package
                                .as_ref()
                                .map(|p| p.name.clone())
                                .unwrap_or_else(|| "unknown".to_string())
                        } else {
                            "unknown".to_string()
                        };

                        for vuln in result.vulns {
                            debug!(
                                "Found vulnerability {} for package {}",
                                vuln.id, package_name
                            );
                            all_vulnerability_ids.push(vuln.id.clone());
                            package_vuln_mapping
                                .entry(vuln.id)
                                .or_default()
                                .insert(package_name.clone());
                        }
                    }
                }
                Err(e) => {
                    warn!("Failed to query OSV batch: {}", e);
                }
            }

            // Update batch progress bar
            if let Some(ref bar) = batch_pb {
                bar.inc(1);
            }
        }

        // Finish batch progress bar
        if let Some(bar) = batch_pb {
            bar.finish_with_message(format!(
                "Queried OSV: {} vulnerability IDs found",
                all_vulnerability_ids.len()
            ));
        }

        debug!(
            "Found {} vulnerability IDs, fetching full details",
            all_vulnerability_ids.len()
        );

        // Fetch full vulnerability details concurrently
        let mut all_vulnerabilities: HashMap<String, Vec<Vulnerability>> = HashMap::new();
        let mut successful_fetches = 0;
        let mut failed_fetches = 0;

        // Create progress bar for vulnerability details if enabled
        let details_pb = if self.http_config.show_progress && all_vulnerability_ids.len() > 1 {
            let bar = ProgressBar::new(all_vulnerability_ids.len() as u64);
            bar.set_style(
                ProgressStyle::default_bar()
                    .template("{msg}\n{spinner:.green} [{elapsed_precise}] [{wide_bar:.cyan/blue}] {pos}/{len} vulnerabilities ({eta})")
                    .unwrap()
                    .progress_chars("#>-"),
            );
            bar.set_message("Fetching OSV vulnerability details");
            Some(Arc::new(bar))
        } else {
            None
        };

        // Create concurrent futures with rate limiting
        const MAX_CONCURRENT_REQUESTS: usize = 10; // Limit concurrent requests to avoid overwhelming OSV API
        let mut futures = FuturesUnordered::new();
        let mut vuln_iter = all_vulnerability_ids.clone().into_iter();

        // Start initial batch of requests
        for _ in 0..MAX_CONCURRENT_REQUESTS.min(all_vulnerability_ids.len()) {
            if let Some(vuln_id) = vuln_iter.next() {
                futures.push(self.fetch_vulnerability_future(vuln_id));
            }
        }

        // Process results as they complete, maintaining rate limit
        while let Some((vuln_id, result)) = futures.next().await {
            // Start a new request if there are more vulnerability IDs to process
            if let Some(next_vuln_id) = vuln_iter.next() {
                futures.push(self.fetch_vulnerability_future(next_vuln_id));
            }
            match result {
                Ok(Some(advisory)) => {
                    successful_fetches += 1;
                    let target_packages = package_vuln_mapping
                        .get(&vuln_id)
                        .cloned()
                        .unwrap_or_default();

                    for package_name in target_packages {
                        match Self::convert_osv_vulnerability(&advisory, &package_name) {
                            Some(vuln) => {
                                debug!(
                                    "Successfully processed vulnerability {} for package {}",
                                    vuln_id, package_name
                                );
                                all_vulnerabilities
                                    .entry(package_name)
                                    .or_default()
                                    .push(vuln);
                            }
                            None => {
                                debug!(
                                    "No matching affected entry in {} for package {}",
                                    vuln_id, package_name
                                );
                            }
                        }
                    }
                }
                Ok(None) => {
                    failed_fetches += 1;
                    debug!("No details available for vulnerability: {} (may be malformed or removed from OSV database)", vuln_id);
                }
                Err(e) => {
                    failed_fetches += 1;
                    // Categorize error types to provide better user feedback
                    let error_str = e.to_string();
                    if error_str.contains("timeout") || error_str.contains("connect") {
                        warn!("Network error fetching vulnerability {}: {}", vuln_id, e);
                    } else if error_str.contains("decode") || error_str.contains("json") {
                        debug!("Data parsing issue for vulnerability {} (likely upstream data quality issue): {}", vuln_id, e);
                    } else {
                        debug!("Failed to fetch vulnerability {}: {}", vuln_id, e);
                    }
                }
            }

            // Update details progress bar
            if let Some(ref bar) = details_pb {
                bar.inc(1);
            }
        }

        // Finish details progress bar
        if let Some(bar) = details_pb {
            bar.finish_with_message(format!(
                "Fetched OSV details: {} successful, {} failed",
                successful_fetches, failed_fetches
            ));
        }

        debug!(
            "OSV vulnerability processing complete: {} successful, {} failed, {} total packages with vulnerabilities",
            successful_fetches,
            failed_fetches,
            all_vulnerabilities.len()
        );

        // Provide user-friendly summary if there were failures
        if failed_fetches > 0 {
            let success_rate =
                (successful_fetches as f64 / (successful_fetches + failed_fetches) as f64) * 100.0;
            if success_rate < 90.0 {
                warn!(
                    "OSV data quality notice: {}/{} vulnerability records successfully processed ({:.1}%). Some OSV records may contain malformed data.",
                    successful_fetches, successful_fetches + failed_fetches, success_rate
                );
            } else {
                debug!(
                    "OSV processing: {}/{} records processed successfully ({:.1}%)",
                    successful_fetches,
                    successful_fetches + failed_fetches,
                    success_rate
                );
            }
        }

        let db = VulnerabilityDatabase::from_package_map(all_vulnerabilities);

        if Self::should_cache_result(&db, all_vulnerability_ids.len(), self.no_cache) {
            let content = serde_json::to_vec(&db)?;
            cache_entry.write(&content).await?;
        } else if !self.no_cache && db.is_empty() && !all_vulnerability_ids.is_empty() {
            warn!(
                "Skipping OSV cache write: 0 vulnerabilities resolved despite {} IDs from batch query (likely network failure)",
                all_vulnerability_ids.len()
            );
        }

        Ok(db)
    }
}

/// OSV batch API request
#[derive(Debug, Clone, Serialize)]
struct OsvBatchRequest {
    queries: Vec<OsvQuery>,
}

/// OSV query structure
#[derive(Debug, Clone, Serialize)]
struct OsvQuery {
    #[serde(skip_serializing_if = "Option::is_none")]
    package: Option<OsvPackage>,
    #[serde(skip_serializing_if = "Option::is_none")]
    version: Option<String>,
}

/// OSV batch API response
#[derive(Debug, Deserialize)]
struct OsvBatchResponse {
    results: Vec<OsvResult>,
}

/// OSV query result - batch API returns lightweight data
#[derive(Debug, Deserialize)]
struct OsvResult {
    #[serde(default)]
    vulns: Vec<OsvLightweightVuln>,
}

/// Lightweight vulnerability data from batch API
#[derive(Debug, Deserialize)]
struct OsvLightweightVuln {
    id: String,
}

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

    #[test]
    fn test_osv_advisory_parsing() {
        let json = r#"{
            "id": "GHSA-test-1234",
            "summary": "Test vulnerability",
            "details": "This is a test vulnerability",
            "affected": [
                {
                    "package": {
                        "ecosystem": "PyPI",
                        "name": "test-package"
                    },
                    "ranges": [
                        {
                            "type": "ECOSYSTEM",
                            "events": [
                                {"introduced": "1.0.0"},
                                {"fixed": "1.2.0"}
                            ]
                        }
                    ]
                }
            ],
            "references": [],
            "severity": [],
            "published": "2023-01-01T00:00:00Z"
        }"#;

        let advisory: OsvAdvisory = serde_json::from_str(json).unwrap();
        assert_eq!(advisory.id, "GHSA-test-1234");
        assert_eq!(advisory.summary, Some("Test vulnerability".to_string()));
        assert_eq!(advisory.affected.len(), 1);
    }

    #[test]
    fn test_cvss_v3_vector_parsing() {
        let advisory = OsvAdvisory {
            id: "TEST-001".to_string(),
            summary: Some("Test".to_string()),
            details: None,
            affected: vec![],
            references: vec![],
            severity: vec![OsvSeverity {
                severity_type: "CVSS_V3".to_string(),
                score: "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H".to_string(),
            }],
            published: None,
            modified: None,
            database_specific: None,
            aliases: vec![],
        };

        let (severity, cvss_score, _cvss_version) = OsvSource::map_osv_severity(&advisory);
        assert_eq!(severity, Severity::Critical);
        assert!(cvss_score.is_some());
        let score = cvss_score.unwrap();
        assert!(
            (9.0..=10.0).contains(&score),
            "Expected critical score, got {}",
            score
        );
    }

    #[test]
    fn test_cvss_v3_high_severity_parsing() {
        let advisory = OsvAdvisory {
            id: "TEST-002".to_string(),
            summary: Some("Test".to_string()),
            details: None,
            affected: vec![],
            references: vec![],
            severity: vec![OsvSeverity {
                severity_type: "CVSS_V3".to_string(),
                score: "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H".to_string(),
            }],
            published: None,
            modified: None,
            database_specific: None,
            aliases: vec![],
        };

        let (severity, cvss_score, _cvss_version) = OsvSource::map_osv_severity(&advisory);
        assert_eq!(severity, Severity::High);
        assert!(cvss_score.is_some());
        let score = cvss_score.unwrap();
        assert!(
            (7.0..9.0).contains(&score),
            "Expected high severity score, got {}",
            score
        );
    }

    #[test]
    fn test_cvss_severity_uses_max_score() {
        let advisory = OsvAdvisory {
            id: "TEST-002B".to_string(),
            summary: Some("Test".to_string()),
            details: None,
            affected: vec![],
            references: vec![],
            severity: vec![
                OsvSeverity {
                    severity_type: "CVSS_V2".to_string(),
                    score: "4.0".to_string(),
                },
                OsvSeverity {
                    severity_type: "CVSS_V3".to_string(),
                    score: "9.5".to_string(),
                },
            ],
            published: None,
            modified: None,
            database_specific: None,
            aliases: vec![],
        };

        let (severity, cvss_score, _cvss_version) = OsvSource::map_osv_severity(&advisory);
        assert_eq!(severity, Severity::Critical);
        assert_eq!(cvss_score, Some(9.5));
    }

    #[test]
    fn test_cvss_v4_vector_parsing() {
        let advisory = OsvAdvisory {
            id: "TEST-003".to_string(),
            summary: Some("Test".to_string()),
            details: None,
            affected: vec![],
            references: vec![],
            severity: vec![OsvSeverity {
                severity_type: "CVSS_V4".to_string(),
                score: "CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:H/VA:H/SC:N/SI:N/SA:N"
                    .to_string(),
            }],
            published: None,
            modified: None,
            database_specific: None,
            aliases: vec![],
        };

        let (severity, cvss_score, _cvss_version) = OsvSource::map_osv_severity(&advisory);
        assert!(
            cvss_score.is_some(),
            "CVSS v4 vector should parse to a score"
        );
        assert!(severity == Severity::Critical || severity == Severity::High);
    }

    #[test]
    fn test_cvss_fallback_to_numeric_score() {
        let advisory = OsvAdvisory {
            id: "TEST-004".to_string(),
            summary: Some("Test".to_string()),
            details: None,
            affected: vec![],
            references: vec![],
            severity: vec![OsvSeverity {
                severity_type: "CVSS_V3".to_string(),
                score: "7.5".to_string(),
            }],
            published: None,
            modified: None,
            database_specific: None,
            aliases: vec![],
        };

        let (severity, cvss_score, _cvss_version) = OsvSource::map_osv_severity(&advisory);
        assert_eq!(severity, Severity::High);
        assert_eq!(cvss_score, Some(7.5));
    }

    #[test]
    fn test_severity_unknown_when_keywords_only() {
        let advisory = OsvAdvisory {
            id: "TEST-005".to_string(),
            summary: Some("Test".to_string()),
            details: None,
            affected: vec![],
            references: vec![],
            severity: vec![OsvSeverity {
                severity_type: "UNKNOWN".to_string(),
                score: "CRITICAL".to_string(),
            }],
            published: None,
            modified: None,
            database_specific: None,
            aliases: vec![],
        };

        let (severity, cvss_score, _cvss_version) = OsvSource::map_osv_severity(&advisory);
        assert_eq!(severity, Severity::Unknown);
        assert!(cvss_score.is_none());
    }

    #[test]
    fn test_severity_unknown_when_database_specific_only() {
        let advisory = OsvAdvisory {
            id: "TEST-006".to_string(),
            summary: Some("Test".to_string()),
            details: None,
            affected: vec![],
            references: vec![],
            severity: vec![],
            published: None,
            modified: None,
            database_specific: Some(serde_json::json!({
                "severity": "HIGH"
            })),
            aliases: vec![],
        };

        let (severity, cvss_score, _cvss_version) = OsvSource::map_osv_severity(&advisory);
        assert_eq!(severity, Severity::Unknown);
        assert!(cvss_score.is_none());
    }

    #[test]
    fn test_severity_default_to_unknown() {
        let advisory = OsvAdvisory {
            id: "TEST-007".to_string(),
            summary: Some("Test".to_string()),
            details: None,
            affected: vec![],
            references: vec![],
            severity: vec![],
            published: None,
            modified: None,
            database_specific: None,
            aliases: vec![],
        };

        let (severity, cvss_score, _cvss_version) = OsvSource::map_osv_severity(&advisory);
        assert_eq!(severity, Severity::Unknown);
        assert!(cvss_score.is_none());
    }

    #[test]
    fn test_convert_osv_vulnerability_populates_cvss_score() {
        let advisory = OsvAdvisory {
            id: "TEST-008".to_string(),
            summary: Some("Test vulnerability".to_string()),
            details: Some("Details".to_string()),
            affected: vec![OsvAffected {
                package: OsvPackage {
                    ecosystem: "PyPI".to_string(),
                    name: "test-package".to_string(),
                    purl: None,
                },
                ranges: vec![OsvRange {
                    range_type: "ECOSYSTEM".to_string(),
                    repo: None,
                    events: vec![
                        OsvEvent {
                            introduced: Some("0".to_string()),
                            fixed: None,
                            last_affected: None,
                            limit: None,
                        },
                        OsvEvent {
                            introduced: None,
                            fixed: Some("2.0.0".to_string()),
                            last_affected: None,
                            limit: None,
                        },
                    ],
                    database_specific: None,
                }],
                versions: None,
                database_specific: None,
                ecosystem_specific: None,
            }],
            references: vec![],
            severity: vec![OsvSeverity {
                severity_type: "CVSS_V3".to_string(),
                score: "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H".to_string(),
            }],
            published: None,
            modified: None,
            database_specific: None,
            aliases: vec![],
        };

        let vuln = OsvSource::convert_osv_vulnerability(&advisory, "test-package").unwrap();
        assert_eq!(vuln.severity, Severity::Critical);
        assert!(vuln.cvss_score.is_some());
        assert!(vuln.cvss_score.unwrap() >= 9.0);
    }

    #[test]
    fn test_convert_osv_multi_package_advisory_filters_by_package() {
        let advisory = OsvAdvisory {
            id: "GHSA-test".to_string(),
            summary: Some("Multi-package advisory".to_string()),
            details: None,
            affected: vec![
                OsvAffected {
                    package: OsvPackage {
                        ecosystem: "PyPI".to_string(),
                        name: "langchain".to_string(),
                        purl: None,
                    },
                    ranges: vec![OsvRange {
                        range_type: "ECOSYSTEM".to_string(),
                        repo: None,
                        events: vec![
                            OsvEvent {
                                introduced: Some("0".to_string()),
                                fixed: None,
                                last_affected: None,
                                limit: None,
                            },
                            OsvEvent {
                                introduced: None,
                                fixed: Some("0.0.308".to_string()),
                                last_affected: None,
                                limit: None,
                            },
                        ],
                        database_specific: None,
                    }],
                    versions: None,
                    database_specific: None,
                    ecosystem_specific: None,
                },
                OsvAffected {
                    package: OsvPackage {
                        ecosystem: "PyPI".to_string(),
                        name: "numexpr".to_string(),
                        purl: None,
                    },
                    ranges: vec![OsvRange {
                        range_type: "ECOSYSTEM".to_string(),
                        repo: None,
                        events: vec![
                            OsvEvent {
                                introduced: Some("0".to_string()),
                                fixed: None,
                                last_affected: None,
                                limit: None,
                            },
                            OsvEvent {
                                introduced: None,
                                fixed: Some("2.8.5".to_string()),
                                last_affected: None,
                                limit: None,
                            },
                        ],
                        database_specific: None,
                    }],
                    versions: None,
                    database_specific: None,
                    ecosystem_specific: None,
                },
            ],
            references: vec![],
            severity: vec![],
            published: None,
            modified: None,
            database_specific: None,
            aliases: vec!["CVE-2023-39631".to_string()],
        };

        // langchain conversion should only have langchain's ranges
        let langchain_vuln = OsvSource::convert_osv_vulnerability(&advisory, "langchain").unwrap();
        assert_eq!(langchain_vuln.affected_versions.len(), 1);
        assert_eq!(langchain_vuln.fixed_versions.len(), 1);
        assert_eq!(
            langchain_vuln.fixed_versions[0],
            Version::from_str("0.0.308").unwrap()
        );
        // langchain 1.2.10 should NOT be in range (>= 0.0.308)
        assert!(
            !langchain_vuln.affected_versions[0].contains(&Version::from_str("1.2.10").unwrap())
        );

        // numexpr conversion should only have numexpr's ranges
        let numexpr_vuln = OsvSource::convert_osv_vulnerability(&advisory, "numexpr").unwrap();
        assert_eq!(numexpr_vuln.affected_versions.len(), 1);
        assert_eq!(numexpr_vuln.fixed_versions.len(), 1);
        assert_eq!(
            numexpr_vuln.fixed_versions[0],
            Version::from_str("2.8.5").unwrap()
        );

        // Non-existent package should return None
        assert!(OsvSource::convert_osv_vulnerability(&advisory, "nonexistent").is_none());
    }

    #[test]
    fn test_convert_osv_vulnerability_normalizes_package_name() {
        let advisory = OsvAdvisory {
            id: "TEST-NORM".to_string(),
            summary: Some("Test".to_string()),
            details: None,
            affected: vec![OsvAffected {
                package: OsvPackage {
                    ecosystem: "PyPI".to_string(),
                    name: "My_Package".to_string(),
                    purl: None,
                },
                ranges: vec![OsvRange {
                    range_type: "ECOSYSTEM".to_string(),
                    repo: None,
                    events: vec![
                        OsvEvent {
                            introduced: Some("1.0".to_string()),
                            fixed: None,
                            last_affected: None,
                            limit: None,
                        },
                        OsvEvent {
                            introduced: None,
                            fixed: Some("2.0".to_string()),
                            last_affected: None,
                            limit: None,
                        },
                    ],
                    database_specific: None,
                }],
                versions: None,
                database_specific: None,
                ecosystem_specific: None,
            }],
            references: vec![],
            severity: vec![],
            published: None,
            modified: None,
            database_specific: None,
            aliases: vec![],
        };

        // Should match with different casing and separator style
        assert!(OsvSource::convert_osv_vulnerability(&advisory, "my-package").is_some());
        assert!(OsvSource::convert_osv_vulnerability(&advisory, "my_package").is_some());
        assert!(OsvSource::convert_osv_vulnerability(&advisory, "My_Package").is_some());
    }

    #[test]
    fn test_last_affected_creates_inclusive_range() {
        let affected = OsvAffected {
            package: OsvPackage {
                ecosystem: "PyPI".to_string(),
                name: "test-package".to_string(),
                purl: None,
            },
            ranges: vec![OsvRange {
                range_type: "ECOSYSTEM".to_string(),
                repo: None,
                events: vec![
                    OsvEvent {
                        introduced: Some("1.0.0".to_string()),
                        fixed: None,
                        last_affected: None,
                        limit: None,
                    },
                    OsvEvent {
                        introduced: None,
                        fixed: None,
                        last_affected: Some("1.5.0".to_string()),
                        limit: None,
                    },
                ],
                database_specific: None,
            }],
            versions: None,
            database_specific: None,
            ecosystem_specific: None,
        };

        let ranges = OsvSource::extract_osv_ranges(&affected);
        assert_eq!(ranges.len(), 1);
        assert!(ranges[0].max_inclusive);
        assert!(ranges[0].constraint.contains("<="));
        // Version at upper bound should match (inclusive)
        assert!(ranges[0].contains(&Version::from_str("1.5.0").unwrap()));
        // Version above upper bound should not
        assert!(!ranges[0].contains(&Version::from_str("1.5.1").unwrap()));
    }

    #[test]
    fn test_fixed_takes_precedence_over_last_affected() {
        let affected = OsvAffected {
            package: OsvPackage {
                ecosystem: "PyPI".to_string(),
                name: "test-package".to_string(),
                purl: None,
            },
            ranges: vec![OsvRange {
                range_type: "ECOSYSTEM".to_string(),
                repo: None,
                events: vec![
                    OsvEvent {
                        introduced: Some("1.0.0".to_string()),
                        fixed: None,
                        last_affected: None,
                        limit: None,
                    },
                    OsvEvent {
                        introduced: None,
                        fixed: Some("2.0.0".to_string()),
                        last_affected: None,
                        limit: None,
                    },
                    OsvEvent {
                        introduced: None,
                        fixed: None,
                        last_affected: Some("1.9.0".to_string()),
                        limit: None,
                    },
                ],
                database_specific: None,
            }],
            versions: None,
            database_specific: None,
            ecosystem_specific: None,
        };

        let ranges = OsvSource::extract_osv_ranges(&affected);
        assert_eq!(ranges.len(), 1);
        assert!(!ranges[0].max_inclusive);
        assert_eq!(
            ranges[0].max.as_ref().unwrap(),
            &Version::from_str("2.0.0").unwrap()
        );
    }

    #[test]
    fn test_should_cache_empty_db_with_expected_vulns() {
        let db = VulnerabilityDatabase::new();
        assert!(
            !OsvSource::should_cache_result(&db, 5, false),
            "empty DB with expected vulns should NOT be cached"
        );
    }

    #[test]
    fn test_should_cache_empty_db_no_expected_vulns() {
        let db = VulnerabilityDatabase::new();
        assert!(
            OsvSource::should_cache_result(&db, 0, false),
            "empty DB with no expected vulns IS a legitimate result"
        );
    }

    #[test]
    fn test_should_cache_nonempty_db() {
        let db = VulnerabilityDatabase::from_package_map(HashMap::from([(
            "pkg".to_string(),
            vec![Vulnerability {
                id: "TEST".to_string(),
                summary: "t".to_string(),
                description: None,
                severity: Severity::Low,
                affected_versions: vec![],
                fixed_versions: vec![],
                references: vec![],
                cvss_score: None,
                cvss_version: None,
                published: None,
                modified: None,
                source: None,
                withdrawn: None,
                aliases: vec![],
            }],
        )]));
        assert!(OsvSource::should_cache_result(&db, 1, false));
    }

    #[test]
    fn test_should_cache_respects_no_cache() {
        let db = VulnerabilityDatabase::new();
        assert!(!OsvSource::should_cache_result(&db, 0, true));
    }
}
