// Copyright (C) 2026  The Software Heritage developers
// See the AUTHORS file at the top-level directory of this distribution
// License: GNU General Public License version 3, or any later version
// See top-level LICENSE file for more information

//! Version detection from package managers

use std::sync::atomic::{AtomicU64, Ordering};

use dashmap::DashMap;
use osv::schema::Ecosystem;
use serde::{Deserialize, Serialize};
use swh_graph::graph::{NodeId, SwhGraphWithProperties};
use swh_graph::{SWHID, properties};

pub mod graph;
mod list_versions;
mod map_versions;
pub mod versions;

pub use list_versions::guess_version_from_branch_name;
pub use list_versions::list_affected_versions;
pub use list_versions::version_from_branch_name;
pub use map_versions::{
    MapAffectedResult, SerializableVersionToSwhidMapping, SwhAffected, SwhEvent, SwhRange,
    SwhVersion, VersionInSwh, VersionSearchInfo, VersionToSwhidMapping,
    find_corresponding_source_in_vcs, find_single_package_version_in_swh, map_affected,
    map_package_version_to_swh,
};

#[derive(Debug, Default)]
pub struct Stats {
    // Overall counts
    pub total_vulns: AtomicU64,
    pub vulns_with_packages: AtomicU64,

    // Git range details
    pub git_ranges_with_repo: AtomicU64,
    pub git_ranges_without_repo: AtomicU64,
    pub git_ranges_with_commits: AtomicU64,
    pub git_ranges_without_commits: AtomicU64,

    // Range type counts
    pub total_git_ranges: AtomicU64,
    pub total_semver_ranges: AtomicU64,
    pub total_ecosystem_ranges: AtomicU64,

    // Package mapping to SWH (by ecosystem)
    pub packages_unmapped_ecosystem: AtomicU64,
    pub packages_found_in_swh_by_ecosystem: DashMap<String, AtomicU64>,
    pub packages_not_found_in_swh_by_ecosystem: DashMap<String, AtomicU64>,

    // Package version resolution in SWH (by ecosystem)
    pub unfound_package_version_in_swh_by_ecosystem: DashMap<String, AtomicU64>,
    pub found_sure_package_version_in_swh_by_ecosystem: DashMap<String, AtomicU64>,
    pub found_fuzzy_package_version_in_swh_by_ecosystem: DashMap<String, AtomicU64>,
    pub package_version_dir_in_vcs_by_ecosystem: DashMap<String, AtomicU64>,
    pub package_version_dir_not_in_vcs_by_ecosystem: DashMap<String, AtomicU64>,

    // Revision pointing to package directories
    pub total_revisions_pointing_to_package_dirs: DashMap<String, AtomicU64>,
    pub package_dirs_with_revisions: DashMap<String, AtomicU64>,
    pub package_dirs_without_revisions: DashMap<String, AtomicU64>,

    // Incorrect reports
    pub fixed_event_without_corresponding_introduced: DashMap<String, AtomicU64>,
    pub unsorted_events: DashMap<String, AtomicU64>,
}

#[derive(Debug, PartialEq, Eq)]
pub struct PackageVersionInVcs {
    pub dir: NodeId,
    pub rev: NodeId,
}

impl PackageVersionInVcs {
    pub fn to_serializable(
        self,
        graph: &impl SwhGraphWithProperties<Maps: properties::Maps>,
    ) -> SerializablePackageVersionInVcs {
        let PackageVersionInVcs { dir, rev } = self;
        SerializablePackageVersionInVcs {
            dir: graph.properties().swhid(dir),
            rev: graph.properties().swhid(rev),
        }
    }
}

#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct SerializablePackageVersionInVcs {
    pub dir: SWHID,
    pub rev: SWHID,
}

#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct PackageNotFoundInSwh {
    pub tried_origin_urls: Vec<String>,
}

#[derive(Debug, Serialize, Deserialize)]
pub struct PackageVersionNotFoundInSwh {
    pub tried_origin_urls: Vec<Option<String>>,
    pub branch_names: Vec<String>,
}

#[derive(Debug, Serialize, Deserialize)]
pub struct PackageVersionMappingResult<T> {
    pub vulnerability_filename: String,
    pub ecosystem: Ecosystem,
    pub package_name: String,
    pub result: T,
}

pub fn increment(counter: &AtomicU64) {
    counter.fetch_add(1, Ordering::Relaxed);
}

pub fn increment_by_ecosystem(map: &DashMap<String, AtomicU64>, ecosystem: &Ecosystem) {
    let ecosystem_key = format!("{:?}", ecosystem);
    map.entry(ecosystem_key)
        .or_insert_with(|| AtomicU64::new(0))
        .fetch_add(1, Ordering::Relaxed);
}

#[derive(Debug, PartialEq, Eq)]
pub struct VersionMatch {
    /// Name of the version in the original vulnerability report
    pub version_name: String,
    /// Confidence in the matching
    pub fuzzy_branch_name_match: bool,
    /// Origin used for the match
    pub origin_url: String,
    /// Name used for the match (along with origin url)
    pub branch_name: Vec<u8>,
    /// Node id in the SWH graph
    pub node: NodeId,
}

impl VersionMatch {
    pub fn to_serializable(
        self,
        graph: &impl SwhGraphWithProperties<Maps: properties::Maps>,
    ) -> SerializableVersionMatch {
        let VersionMatch {
            version_name,
            origin_url,
            fuzzy_branch_name_match,
            branch_name,
            node,
        } = self;
        SerializableVersionMatch {
            version_name,
            origin_url,
            fuzzy_branch_name_match,
            branch_name: String::from_utf8_lossy(&branch_name).into(),
            node: graph.properties().swhid(node),
        }
    }
}

#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct SerializableVersionMatch {
    /// Name of the version in the original vulnerability report
    pub version_name: String,
    /// Confidence in the matching
    pub fuzzy_branch_name_match: bool,
    /// Origin used for the match
    pub origin_url: String,
    /// Name used for the match (along with origin url)
    pub branch_name: String,
    /// Corresponding SWH object
    pub node: SWHID,
}

#[derive(Debug, PartialEq, Eq)]
pub enum VersionSearchResult {
    Found(VersionMatch),
    NotFound { branch_names: Vec<Vec<u8>> },
}

/// Map a package (ecosystem + name) to potential origin URLs in SWH
pub fn map_package_to_origin_urls(ecosystem: &Ecosystem, package_name: &str) -> Vec<String> {
    match ecosystem {
        // Ecosystems with mappings to SWH
        Ecosystem::Npm => {
            vec![format!("https://www.npmjs.com/package/{}", package_name)]
        }
        Ecosystem::PyPI | Ecosystem::Python => {
            vec![format!("https://pypi.org/project/{}/", package_name)]
        }
        Ecosystem::Go => {
            vec![format!("https://pkg.go.dev/{}", package_name)]
        }
        Ecosystem::CRAN => {
            vec![format!(
                "https://cran.r-project.org/package={}",
                package_name
            )]
        }
        Ecosystem::GitHubActions => {
            vec![format!("https://github.com/{}", package_name)]
        }
        Ecosystem::Pub => {
            vec![format!("https://pub.dev/packages/{}", package_name)]
        }
        Ecosystem::Ubuntu { pro: false, .. } => {
            vec![
                format!("deb://Ubuntu/packages/{}", package_name),
                format!("deb://Ubuntu-Legacy/packages/{}", package_name),
            ]
        }
        Ecosystem::Ubuntu { pro: true, .. } => {
            // Ubuntu Pro is not free software, can't be archived
            vec![]
        }
        Ecosystem::Debian(_) => {
            vec![
                format!("deb://Debian/packages/{}", package_name),
                format!("deb://Debian-Security/packages/{}", package_name),
                format!("http://snapshot.debian.org/package/{}/", package_name),
            ]
        }
        Ecosystem::Maven(repository) => {
            if let Some((group_id, artifact_id)) = package_name.split_once(':') {
                GetMavenUrls {
                    group_id,
                    artifact_id,
                    repository,
                }
                .get()
            } else {
                vec![]
            }
        }
        Ecosystem::SwiftURL => {
            // "The name is a Git URL to the source of the package"
            vec![package_name.to_string()]
        }
        Ecosystem::Linux => {
            vec![
                // most complete
                //
                // has a trailing slash in the archive.
                "https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git/".to_string(),
                // has some RCs that the previous repo doesn't have
                //
                // no trailing slash in the archive.
                "https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git".to_string(),
            ]
        }
        Ecosystem::JavaScript => {
            vec![format!("https://www.npmjs.com/package/{}", package_name)]
        }

        // Ecosystems not yet archived by SWH
        Ecosystem::AlmaLinux(_)
        | Ecosystem::Alpine(_)
        | Ecosystem::Android
        | Ecosystem::Bioconductor
        | Ecosystem::Bitnami
        | Ecosystem::Chainguard
        | Ecosystem::ConanCenter
        | Ecosystem::CratesIO
        | Ecosystem::GHC
        | Ecosystem::Hackage
        | Ecosystem::Hex
        | Ecosystem::Julia
        | Ecosystem::Kubernetes
        | Ecosystem::Mageia(_)
        | Ecosystem::MinimOS
        | Ecosystem::NuGet
        | Ecosystem::OpenEuler
        | Ecosystem::OpenSUSE(_)
        | Ecosystem::OssFuzz
        | Ecosystem::Packagist
        | Ecosystem::PhotonOS(_)
        | Ecosystem::RedHat(_)
        | Ecosystem::RockyLinux(_)
        | Ecosystem::RubyGems
        | Ecosystem::SUSE(_)
        | Ecosystem::Wolfi
        | Ecosystem::Alpaquita
        | Ecosystem::BellSoftHardenedContainers
        | Ecosystem::DWF
        | Ecosystem::Echo
        | Ecosystem::GSD
        | Ecosystem::UVI
        | Ecosystem::VSCode => {
            // These ecosystems are not archived by SWH yet
            vec![]
        }

        _ => {
            log::warn!("Unknown ecosystem {ecosystem:?}");
            vec![]
        }
    }
}

pub struct GetMavenUrls<S: AsRef<str>> {
    group_id: S,
    artifact_id: S,
    repository: S,
}

impl<S: AsRef<str>> GetMavenUrls<S> {
    pub fn get(&self) -> Vec<String> {
        let group_id = self.group_id.as_ref();
        let artifact_id = self.artifact_id.as_ref();
        let repository = self.repository.as_ref();

        let path = group_id.replace('.', "/");
        let repo = if repository.is_empty() || repository == "https://repo.maven.apache.org/maven2"
        {
            // The OSV spec says ``https://repo.maven.apache.org/maven2`` is the default but SWH archives
            // ``https://repo1.maven.org/maven2`` instead, so let's use that instead in
            // either case, because they are the same repository under the hood.
            "https://repo1.maven.org/maven2"
        } else {
            repository
        };
        let mut urls = vec![format!("{}/{}/{}", repo, path, artifact_id)];

        // Try alternative repositories for known group IDs if default repo was used
        if repository.is_empty() || repository == "https://repo.maven.apache.org/maven2" {
            if group_id.starts_with("org.xwiki.") {
                urls.push(format!(
                    "https://maven.xwiki.org/releases/{}/{}",
                    path, artifact_id
                ));
            } else if group_id.starts_with("org.jenkins-ci.")
                || group_id.starts_with("org.jenkinsci.")
                || group_id.starts_with("io.jenkins.")
                || group_id == "org.jenkins-ci"
                || group_id == "org.jenkinsci"
            {
                urls.push(format!(
                    "https://repo.jenkins-ci.org/releases/{}/{}",
                    path, artifact_id
                ));
            } else if group_id == "org.gradle" {
                urls.push(format!(
                    "https://repo.gradle.org/ui/native/libs-releases/{}/{}",
                    path, artifact_id
                ));
            }
        }
        urls
    }
}

/// Map a package URL to potential origin URLs in SWH
pub fn map_purl_to_origin_urls(purl: purl::Purl) -> Vec<String> {
    use purl::PackageType;

    let package_name = purl.name();

    match purl.package_type() {
        // Ecosystems with mappings to SWH
        PackageType::Npm => {
            vec![format!("https://www.npmjs.com/package/{}", package_name)]
        }
        PackageType::PyPI => {
            vec![format!("https://pypi.org/project/{}/", package_name)]
        }
        PackageType::Golang => {
            vec![format!("https://pkg.go.dev/{}", package_name)]
        }
        PackageType::Maven => {
            if let Some(group_id) = purl.namespace() {
                GetMavenUrls {
                    group_id,
                    artifact_id: purl.name(),
                    repository: purl.qualifiers().get("repository_url").unwrap_or_default(),
                }
                .get()
            } else {
                vec![]
            }
        }
        package_type => {
            log::warn!("Unknown package type {package_type:?}");
            vec![]
        }
    }
}
