use std::cmp::Ordering;

use cargo_metadata::semver::{BuildMetadata, Comparator, Op, Prerelease, Version, VersionReq};

/// Helper function to compare version requirements.
///
/// Compares both the lower and upper bound (if any).
pub fn compare_version_req(old: &VersionReq, new: &VersionReq) -> (Ordering, Ordering) {
    const fn version(major: u64, minor: u64, patch: u64, pre: Prerelease) -> Version {
        Version {
            major,
            minor,
            patch,
            pre,
            build: BuildMetadata::EMPTY,
        }
    }

    fn lower_bound(c: &Comparator) -> Option<Version> {
        match c.op {
            Op::Exact | Op::GreaterEq => Some(version(
                c.major,
                c.minor.unwrap_or(0),
                c.patch.unwrap_or(0),
                c.pre.clone(),
            )),
            Op::Less => None,
            _ => unreachable!(),
        }
    }

    fn upper_bound(c: &Comparator) -> Option<Version> {
        match c.op {
            Op::Exact | Op::Less => Some(version(
                c.major,
                c.minor.unwrap_or(0),
                c.patch.unwrap_or(0),
                c.pre.clone(),
            )),
            Op::GreaterEq => None,
            _ => unreachable!(),
        }
    }

    if old == new {
        return (Ordering::Equal, Ordering::Equal);
    }

    let old_comps: Vec<_> = old.comparators.iter().flat_map(normalize_version_req).collect();
    let new_comps: Vec<_> = new.comparators.iter().flat_map(normalize_version_req).collect();

    let old_lower_bound = old_comps.iter().filter_map(lower_bound).max();
    let new_lower_bound = new_comps.iter().filter_map(lower_bound).max();
    let old_upper_bound = old_comps.iter().filter_map(upper_bound).min();
    let new_upper_bound = new_comps.iter().filter_map(upper_bound).min();

    let lower_bound_cmp = match (old_lower_bound, new_lower_bound) {
        (None, None) => Ordering::Equal,
        (Some(old_bound), Some(new_bound)) => new_bound.cmp(&old_bound),
        (Some(_), None) => Ordering::Less,
        (None, Some(_)) => Ordering::Greater,
    };
    let upper_bound_cmp = match (old_upper_bound, new_upper_bound) {
        (None, None) => Ordering::Equal,
        (Some(old_bound), Some(new_bound)) => new_bound.cmp(&old_bound),
        (Some(_), None) => Ordering::Greater,
        (None, Some(_)) => Ordering::Less,
    };

    (lower_bound_cmp, upper_bound_cmp)
}

/// Helper function to "normalize" version requirements into a more canonical form
/// that only uses `Op::GreaterEq`, `Op::Less`, and `Op::Exact`.
///
/// Adapted from the cargo2rpm.semver Python module.
#[expect(clippy::too_many_lines)]
fn normalize_version_req(comp: &Comparator) -> Vec<Comparator> {
    const fn comparator(op: Op, major: u64, minor: Option<u64>, patch: Option<u64>, pre: Prerelease) -> Comparator {
        Comparator {
            op,
            major,
            minor,
            patch,
            pre,
        }
    }

    let mut result = Vec::with_capacity(2);

    match comp.op {
        Op::Exact => match (comp.minor, comp.patch) {
            (None, None) => {
                result.push(comparator(
                    Op::GreaterEq,
                    comp.major,
                    Some(0),
                    Some(0),
                    Prerelease::EMPTY,
                ));
                result.push(comparator(
                    Op::Less,
                    comp.major + 1,
                    Some(0),
                    Some(0),
                    Prerelease::EMPTY,
                ));
            },
            (Some(minor), None) => {
                result.push(comparator(
                    Op::GreaterEq,
                    comp.major,
                    Some(minor),
                    Some(0),
                    Prerelease::EMPTY,
                ));
                result.push(comparator(
                    Op::Less,
                    comp.major,
                    Some(minor + 1),
                    Some(0),
                    Prerelease::EMPTY,
                ));
            },
            (Some(minor), Some(patch)) => {
                result.push(comparator(
                    Op::Exact,
                    comp.major,
                    Some(minor),
                    Some(patch),
                    comp.pre.clone(),
                ));
            },
            (None, Some(_)) => unreachable!(),
        },
        Op::Greater => match (comp.minor, comp.patch) {
            (None, None) => {
                result.push(comparator(
                    Op::GreaterEq,
                    comp.major + 1,
                    Some(0),
                    Some(0),
                    Prerelease::EMPTY,
                ));
            },
            (Some(minor), None) => {
                result.push(comparator(
                    Op::GreaterEq,
                    comp.major,
                    Some(minor + 1),
                    Some(0),
                    Prerelease::EMPTY,
                ));
            },
            (Some(minor), Some(patch)) => {
                result.push(comparator(
                    Op::GreaterEq,
                    comp.major,
                    Some(minor),
                    Some(patch + 1),
                    Prerelease::EMPTY,
                ));
            },
            (None, Some(_)) => unreachable!(),
        },
        Op::GreaterEq => match (comp.minor, comp.patch) {
            (None, None) => {
                result.push(comparator(
                    Op::GreaterEq,
                    comp.major,
                    Some(0),
                    Some(0),
                    Prerelease::EMPTY,
                ));
            },
            (Some(minor), None) => {
                result.push(comparator(
                    Op::GreaterEq,
                    comp.major,
                    Some(minor),
                    Some(0),
                    Prerelease::EMPTY,
                ));
            },
            (Some(minor), Some(patch)) => {
                result.push(comparator(
                    Op::GreaterEq,
                    comp.major,
                    Some(minor),
                    Some(patch),
                    comp.pre.clone(),
                ));
            },
            (None, Some(_)) => unreachable!(),
        },
        Op::Less => match (comp.minor, comp.patch) {
            (None, None) => {
                result.push(comparator(Op::Less, comp.major, Some(0), Some(0), Prerelease::EMPTY));
            },
            (Some(minor), None) => {
                result.push(comparator(
                    Op::Less,
                    comp.major,
                    Some(minor),
                    Some(0),
                    Prerelease::EMPTY,
                ));
            },
            (Some(minor), Some(patch)) => {
                result.push(comparator(
                    Op::Less,
                    comp.major,
                    Some(minor),
                    Some(patch),
                    comp.pre.clone(),
                ));
            },
            (None, Some(_)) => unreachable!(),
        },
        Op::LessEq => match (comp.minor, comp.patch) {
            (None, None) => {
                result.push(comparator(
                    Op::Less,
                    comp.major + 1,
                    Some(0),
                    Some(0),
                    Prerelease::EMPTY,
                ));
            },
            (Some(minor), None) => {
                result.push(comparator(
                    Op::Less,
                    comp.major,
                    Some(minor + 1),
                    Some(0),
                    Prerelease::EMPTY,
                ));
            },
            (Some(minor), Some(patch)) => {
                result.push(comparator(
                    Op::Less,
                    comp.major,
                    Some(minor),
                    Some(patch + 1),
                    Prerelease::EMPTY,
                ));
            },
            (None, Some(_)) => unreachable!(),
        },
        Op::Tilde => match (comp.minor, comp.patch) {
            (None, None) => {
                result.extend(normalize_version_req(&comparator(
                    Op::Exact,
                    comp.major,
                    None,
                    None,
                    Prerelease::EMPTY,
                )));
            },
            (Some(minor), None) => {
                result.extend(normalize_version_req(&comparator(
                    Op::Exact,
                    comp.major,
                    Some(minor),
                    None,
                    Prerelease::EMPTY,
                )));
            },
            (Some(minor), Some(patch)) => {
                result.push(comparator(
                    Op::GreaterEq,
                    comp.major,
                    Some(minor),
                    Some(patch),
                    comp.pre.clone(),
                ));
                result.push(comparator(
                    Op::Less,
                    comp.major,
                    Some(minor + 1),
                    Some(0),
                    Prerelease::EMPTY,
                ));
            },
            (None, Some(_)) => unreachable!(),
        },
        Op::Caret => match (comp.major, comp.minor, comp.patch) {
            (major, None, None) => {
                result.extend(normalize_version_req(&comparator(
                    Op::Exact,
                    major,
                    None,
                    None,
                    Prerelease::EMPTY,
                )));
            },
            (0, Some(0), None) => {
                result.extend(normalize_version_req(&comparator(
                    Op::Exact,
                    0,
                    Some(0),
                    None,
                    Prerelease::EMPTY,
                )));
            },
            (major, Some(minor), None) => {
                result.extend(normalize_version_req(&comparator(
                    Op::Caret,
                    major,
                    Some(minor),
                    Some(0),
                    Prerelease::EMPTY,
                )));
            },
            (0, Some(0), Some(patch)) => {
                result.extend(normalize_version_req(&comparator(
                    Op::Exact,
                    0,
                    Some(0),
                    Some(patch),
                    comp.pre.clone(),
                )));
            },
            (0, Some(minor), Some(patch)) => {
                result.push(comparator(Op::GreaterEq, 0, Some(minor), Some(patch), comp.pre.clone()));
                result.push(comparator(Op::Less, 0, Some(minor + 1), Some(0), Prerelease::EMPTY));
            },
            (major, Some(minor), Some(patch)) => {
                result.push(comparator(
                    Op::GreaterEq,
                    major,
                    Some(minor),
                    Some(patch),
                    comp.pre.clone(),
                ));
                result.push(comparator(Op::Less, major + 1, Some(0), Some(0), Prerelease::EMPTY));
            },
            (_, None, Some(_)) => unreachable!(),
        },
        Op::Wildcard => match (comp.minor, comp.patch) {
            (None, None) => {
                result.extend(normalize_version_req(&comparator(
                    Op::Exact,
                    comp.major,
                    None,
                    None,
                    Prerelease::EMPTY,
                )));
            },
            (Some(minor), None) => {
                result.extend(normalize_version_req(&comparator(
                    Op::Exact,
                    comp.major,
                    Some(minor),
                    None,
                    Prerelease::EMPTY,
                )));
            },
            (Some(_) | None, Some(_)) => unreachable!(),
        },
        _ => unimplemented!(),
    }

    result
}

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

    #[test]
    fn test_compare_version_req() {
        let vr = |s| VersionReq::parse(s).unwrap();

        assert_eq!(
            compare_version_req(&vr("^1.0"), &vr("^2.0")),
            (Ordering::Greater, Ordering::Greater)
        );
        assert_eq!(
            compare_version_req(&vr("^2.0"), &vr("^1.0")),
            (Ordering::Less, Ordering::Less)
        );

        assert_eq!(
            compare_version_req(&vr(">=1.0,<3"), &vr("^2.0")),
            (Ordering::Greater, Ordering::Equal)
        );
        assert_eq!(
            compare_version_req(&vr("^2.0"), &vr(">=1.0,<3")),
            (Ordering::Less, Ordering::Equal)
        );
    }

    #[test]
    fn test_normalize_version_req() {
        let ct = |s| Comparator::parse(s).unwrap();

        assert_eq!(normalize_version_req(&ct("^1.0")), vec![ct(">=1.0.0"), ct("<2.0.0")]);
        assert_eq!(normalize_version_req(&ct("^0.1")), vec![ct(">=0.1.0"), ct("<0.2.0")]);
        assert_eq!(normalize_version_req(&ct("^0.0.1")), vec![ct("=0.0.1")]);

        assert_eq!(normalize_version_req(&ct("~1.1")), vec![ct(">=1.1.0"), ct("<1.2.0")]);
        assert_eq!(normalize_version_req(&ct("~0.1")), vec![ct(">=0.1.0"), ct("<0.2.0")]);

        assert_eq!(normalize_version_req(&ct("=1")), vec![ct(">=1.0.0"), ct("<2.0.0")]);
        assert_eq!(normalize_version_req(&ct("=1.1")), vec![ct(">=1.1.0"), ct("<1.2.0")]);
        assert_eq!(normalize_version_req(&ct("=1.1.0")), vec![ct("=1.1.0")]);
    }
}
