// 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

use std::cmp::Ordering;
use std::str::FromStr;
use std::sync::OnceLock;

use anyhow::{Context, Result, anyhow, bail};
use debversion::Version as DebVersion;
use mvn_version::ComparableVersion as MvnVersion;
use osv::schema::Ecosystem;
use pep440_rs::Version as Pep440Version;
use regex::Regex;
use serde::{Deserialize, Serialize};
use version_spec::VersionSpec;

/// See [`RangeType`]
#[derive(Clone, Debug, Eq, PartialEq, Hash, Serialize, Deserialize)]
pub enum VersionType {
    Git,
    Semver,
    Pep440,
    Dpkg,
    Linux,
    Cran,
    Maven,
    PubDev,
    GitHubActions,
    Unsupported(Ecosystem),
}

impl From<&Ecosystem> for VersionType {
    fn from(ecosystem: &Ecosystem) -> Self {
        match ecosystem {
            Ecosystem::Debian { .. } | Ecosystem::Ubuntu { .. } => VersionType::Dpkg,
            Ecosystem::Linux => VersionType::Linux,
            Ecosystem::PyPI | Ecosystem::Python => VersionType::Pep440,
            Ecosystem::CRAN => VersionType::Cran,
            Ecosystem::Maven { .. } => VersionType::Maven,
            Ecosystem::Pub => VersionType::PubDev,
            Ecosystem::GitHubActions => VersionType::GitHubActions,
            ecosystem => VersionType::Unsupported(ecosystem.clone()),
        }
    }
}

#[derive(Debug, Clone, PartialEq, Eq)]
/// Linux kernel version in the form `X.Y[.Z]` or `X.Y[.Z]-rcN`.
///
/// ```
/// use anyhow::Result;
/// use swh_osv::packages::versions::LinuxVersion;
///
/// # fn main() -> Result<()> {
/// assert_eq!("5.4".parse::<LinuxVersion>()?, LinuxVersion { base: [5, 4, 0, 0], rc: None });
/// assert_eq!("5.4.0".parse::<LinuxVersion>()?, LinuxVersion { base: [5, 4, 0, 0], rc: None });
/// assert_eq!("5.4.0-rc2".parse::<LinuxVersion>()?, LinuxVersion { base: [5, 4, 0, 0], rc: Some(2) });
/// assert_eq!("5.4-rc2".parse::<LinuxVersion>()?, LinuxVersion { base: [5, 4, 0, 0], rc: Some(2) });
/// assert_eq!("2.6.16.35".parse::<LinuxVersion>()?, LinuxVersion { base: [2, 6, 16, 35], rc: None });
/// assert_eq!("2.6.16.35-rc2".parse::<LinuxVersion>()?, LinuxVersion { base: [2, 6, 16, 35], rc: Some(2) });
/// assert!("2.6.20.16".parse::<LinuxVersion>()? < "2.6.20.17".parse()?);
/// assert!("5.4.0-rc2".parse::<LinuxVersion>()? < "5.4.0".parse()?);
/// assert!("v5.4.0".parse::<LinuxVersion>().is_err());
/// assert!("5.4.0-rc".parse::<LinuxVersion>().is_err());
/// # Ok(())
/// # }
/// ```
pub struct LinuxVersion {
    /// version of the corresponding stable release
    pub base: [u32; 4],
    /// Release-candidate number, if not a stable release
    pub rc: Option<u32>,
}

impl PartialOrd for LinuxVersion {
    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
        Some(self.cmp(other))
    }
}

impl Ord for LinuxVersion {
    fn cmp(&self, other: &Self) -> Ordering {
        self.base.cmp(&other.base).then(match (self.rc, other.rc) {
            (Some(_), None) => Ordering::Less,    // RC before stable
            (None, Some(_)) => Ordering::Greater, // stable after RC
            (None, None) => Ordering::Equal,
            (Some(left_rc), Some(right_rc)) => left_rc.cmp(&right_rc),
        })
    }
}

impl FromStr for LinuxVersion {
    type Err = anyhow::Error;

    fn from_str(version_name: &str) -> Result<Self, Self::Err> {
        static LINUX_VERSION_REGEX: OnceLock<Regex> = OnceLock::new();
        let regex = LINUX_VERSION_REGEX.get_or_init(|| {
            Regex::new(r"^(?P<base>\d+\.\d+(?:\.\d+){0,2})(?:-rc(?P<rc>\d+))?$")
                .expect("linux version regex is valid")
        });

        let captures = regex
            .captures(version_name)
            .ok_or_else(|| anyhow!("invalid linux version format {version_name}"))?;

        let base = captures
            .name("base")
            .expect("base capture is always present")
            .as_str();

        let mut parts = base
            .split('.')
            .map(|part| {
                part.parse::<u32>()
                    .map_err(|err| anyhow!("invalid linux version part {part}: {err}"))
            })
            .collect::<Result<Vec<_>>>()?;

        while parts.len() < 4 {
            parts.push(0)
        }

        let base = [parts[0], parts[1], parts[2], parts[3]];

        let rc = captures
            .name("rc")
            .map(|rc_match| {
                rc_match
                    .as_str()
                    .parse::<u32>()
                    .map_err(|err| anyhow!("invalid linux rc part {}: {err}", rc_match.as_str()))
            })
            .transpose()?;

        Ok(LinuxVersion { base, rc })
    }
}

#[derive(Debug, Clone)]
/// Numbers separated by dots and dashes
///
/// > The mandatory 'Version' field gives the version of the package. This is a sequence of at least two (and usually three) non-negative integers separated by single '.' or '-' characters. The canonical form is as shown in the example, and a version such as '0.01' or '0.01.0' will be handled as if it were '0.1-0'. It is not a decimal number, so for example 0.9 < 0.75 since 9 < 75.
/// > -- <https://cran.r-project.org/doc/manuals/R-exts.html#The-DESCRIPTION-file-1>
///
/// https://r-pkgs.org/lifecycle.html#sec-lifecycle-version-number further specifies that dashes
/// are equivalent to dots, and missing trailing numbers are equivalent to 0.
///
/// ```
/// use anyhow::Result;
/// use swh_osv::packages::versions::CranVersion;
///
/// # fn main() -> Result<()> {
/// assert_eq!("1.0".parse::<CranVersion>()?, CranVersion { parts: vec![1, 0] });
/// assert_eq!("1.0.0".parse::<CranVersion>()?, CranVersion { parts: vec![1, 0, 0] });
/// assert_eq!("1.0-0".parse::<CranVersion>()?, CranVersion { parts: vec![1, 0, 0] });
/// assert_eq!("2.4.1".parse::<CranVersion>()?, CranVersion { parts: vec![2, 4, 1] });
/// assert_eq!("0.01".parse::<CranVersion>()?, CranVersion { parts: vec![0, 1] });
/// assert_eq!("1.02.3".parse::<CranVersion>()?, CranVersion { parts: vec![1, 2, 3] });
///
/// assert_eq!("1.0".parse::<CranVersion>()?, "1.0.0".parse()?);
/// assert!("0.9".parse::<CranVersion>()? < "0.75".parse()?);
/// assert!("1.9".parse::<CranVersion>()? < "1.75".parse()?);
///
/// assert!("".parse::<CranVersion>().is_err());
/// assert!("1.2.a".parse::<CranVersion>().is_err());
/// assert!("v1.2.3".parse::<CranVersion>().is_err());
/// # Ok(())
/// # }
/// ```
pub struct CranVersion {
    pub parts: Vec<u64>,
}

impl PartialEq for CranVersion {
    fn eq(&self, other: &Self) -> bool {
        self.cmp(other) == Ordering::Equal
    }
}

impl Eq for CranVersion {}

impl PartialOrd for CranVersion {
    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
        Some(self.cmp(other))
    }
}

impl Ord for CranVersion {
    fn cmp(&self, other: &Self) -> Ordering {
        let max_len = self.parts.len().max(other.parts.len());
        for i in 0..max_len {
            let left = self.parts.get(i).copied().unwrap_or(0);
            let right = other.parts.get(i).copied().unwrap_or(0);
            let ordering = left.cmp(&right);
            if ordering != Ordering::Equal {
                return ordering;
            }
        }
        Ordering::Equal
    }
}

impl FromStr for CranVersion {
    type Err = anyhow::Error;

    fn from_str(version_name: &str) -> Result<Self, Self::Err> {
        let parts: Result<Vec<u64>> = version_name
            .split(['.', '-'])
            .map(|part| {
                part.parse::<u64>()
                    .map_err(|err| anyhow!("invalid CRAN version part {part}: {err}"))
            })
            .collect();

        Ok(CranVersion { parts: parts? })
    }
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ParsedVersion {
    Semver(VersionSpec),
    PubDev(VersionSpec), // https://github.com/dart-lang/pub-dev/blob/03b829fe992494a2cc6737330f370eaadc5645c7/doc/versions.md says it's almost a semver
    Pep440(Pep440Version),
    Debian(DebVersion),
    Linux(LinuxVersion),
    Cran(CranVersion),
    Maven(MvnVersion),
    GitHubActions(MvnVersion), // "It's completely up to the maintainers of the action repository,
    // there's no version parsing involved".
    // -- https://github.com/orgs/community/discussions/48058#discussioncomment-5055159
    // Maven is the most permissive and seems to try to approximate
    // intuition of informal systems, so let's use that.
    Raw(String),
}

impl PartialOrd for ParsedVersion {
    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
        Some(match (self, other) {
            (ParsedVersion::Semver(a), ParsedVersion::Semver(b)) => a.cmp(b),
            (ParsedVersion::PubDev(a), ParsedVersion::PubDev(b)) => a.cmp(b),
            (ParsedVersion::Pep440(a), ParsedVersion::Pep440(b)) => a.cmp(b),
            (ParsedVersion::Debian(a), ParsedVersion::Debian(b)) => a.cmp(b),
            (ParsedVersion::Linux(a), ParsedVersion::Linux(b)) => a.cmp(b),
            (ParsedVersion::Cran(a), ParsedVersion::Cran(b)) => a.cmp(b),
            (ParsedVersion::Maven(a), ParsedVersion::Maven(b)) => a.cmp(b),
            (ParsedVersion::GitHubActions(a), ParsedVersion::GitHubActions(b)) => a.cmp(b),
            (ParsedVersion::Raw(a), ParsedVersion::Raw(b)) => a.cmp(b),
            _ => {
                log::debug!("Comparing versions of different types: {self:?} and {other:?}");
                return None;
            }
        })
    }
}

impl ParsedVersion {
    pub fn try_cmp(&self, other: &Self) -> Result<Ordering> {
        self.partial_cmp(other)
            .with_context(|| format!("incomparable versions {self:?} and {other:?}"))
    }
}

impl ParsedVersion {
    /// In case of invalid version, returns `Ok(None)` for ecosystems where version validity is not
    /// enforced (Python, Semver, ...), or `Err` on ecosystems where it is (PubDev, Dpkg, ...)
    pub fn parse(version_name: &str, version_type: &VersionType) -> Result<Option<Self>> {
        Ok(match version_type {
            VersionType::Semver => match VersionSpec::parse(version_name) {
                Ok(parsed) => Some(ParsedVersion::Semver(parsed)),
                Err(_) => {
                    log::info!("Could not parse semver version {version_name}");
                    None
                }
            },
            VersionType::PubDev => {
                Some(ParsedVersion::PubDev(version_name.parse().with_context(
                    || format!("Could not parse PubDev version {version_name}"),
                )?))
            }
            VersionType::Pep440 => match version_name.parse() {
                Ok(version) => Some(ParsedVersion::Pep440(version)),
                Err(e) => {
                    log::debug!("Could not parse pep440 version {version_name}: {e}");
                    None
                }
            },
            VersionType::Dpkg => {
                Some(ParsedVersion::Debian(version_name.parse().with_context(
                    || format!("Could not parse Debian version {version_name}"),
                )?))
            }
            VersionType::Linux => {
                if version_name.ends_with("-tree")
                    || version_name.ends_with("-pre1")
                    || version_name.ends_with("-dontuse")
                {
                    // old branches. not worth adding support for.
                    return Ok(None);
                }

                Some(ParsedVersion::Linux(
                    version_name
                        .parse::<LinuxVersion>()
                        .with_context(|| format!("Could not parse Linux version {version_name}"))?,
                ))
            }
            VersionType::Cran => {
                Some(ParsedVersion::Cran(version_name.parse().with_context(
                    || format!("Could not parse CRAN version {version_name}"),
                )?))
            }
            VersionType::Maven => {
                Some(ParsedVersion::Maven(version_name.parse().with_context(
                    || format!("Could not parse Maven version {version_name}"),
                )?))
            }
            VersionType::GitHubActions => match version_name.parse() {
                Ok(version) => Some(ParsedVersion::GitHubActions(version)),
                Err(e) => {
                    log::debug!("Could not parse GithubActions version {version_name}: {e}");
                    None
                }
            },
            VersionType::Git => {
                bail!("Cannot parse Git commit id as package version");
            }
            VersionType::Unsupported(ecosystem) => {
                log::warn!("Using raw version comparison for unsupported ecosystem {ecosystem:?}");
                Some(ParsedVersion::Raw(version_name.to_string()))
            }
        })
    }
}
