//! Package version parsing and comparison module
//!
//! This module provides PackageVersion struct and functions for parsing and comparing
//! package versions in different formats (RPM, Debian, etc.).

/// Package version comparison module
///
/// Supports both RPM and Debian package version formats:
/// - RPM: [epoch:]upstream_version[-release]
/// - Debian: [epoch:]upstream_version[-debian_revision]
///
/// Comparison priority (high to low):
/// 1. Epoch (number before colon, defaults to 0)
/// 2. Upstream version (main version part)
/// 3. Release/Revision (part after last dash)

use color_eyre::Result;

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PackageVersion {
    pub epoch: u64,
    pub upstream: String,
    pub revision: String,
}

impl PackageVersion {
    /// Parse a package version string into components
    ///
    /// Format: [epoch:]upstream_version[-revision]
    ///
    /// Key parsing rules:
    /// - Epoch: Optional number before colon (:), defaults to 0
    /// - Upstream: Main version part, may contain dashes for pre-release markers (e.g., "1.0-rc1")
    /// - Revision: Optional part after last dash
    ///   - Debian: debian_revision (e.g., "1.0-5", "1.0-1ubuntu2", "2.14.14-z")
    ///   - RPM: release (e.g., "1.0-2.el8", "1.0-1.fc35")
    ///   - Pre-release markers (rc, beta, alpha, pre, dev, snapshot) are treated as part of upstream
    ///   - Other alphabetic suffixes after the last dash are treated as revisions
    ///
    /// Examples:
    /// - "1.0" -> epoch=0, upstream="1.0", revision="0"
    /// - "2:1.0-rc1" -> epoch=2, upstream="1.0-rc1", revision="0"
    /// - "1.0-rc1-5" -> epoch=0, upstream="1.0-rc1", revision="5"
    /// - "2.14.14-z" -> epoch=0, upstream="2.14.14", revision="z"
    /// - "2:1.18.3~beta+dfsg1-5+b1" -> epoch=2, upstream="1.18.3~beta+dfsg1", revision="5+b1"
    pub fn parse(version_str: &str) -> Result<Self> {
        let version_str = version_str.trim();

        // Split by epoch (colon)
        let (epoch_str, remaining) = if let Some(colon_pos) = version_str.find(':') {
            let epoch_part = &version_str[..colon_pos];
            let remaining_part = &version_str[colon_pos + 1..];
            (epoch_part, remaining_part)
        } else {
            ("0", version_str)
        };

        let epoch = epoch_str.parse::<u64>().unwrap_or(0);

        // Find revision: rightmost dash followed by a digit
        // This correctly handles upstream versions like "1.0-rc1" vs revisions like "5"
        let (upstream, revision) = Self::split_upstream_revision(remaining);

        Ok(PackageVersion {
            epoch,
            upstream,
            revision,
        })
    }

    /// Split upstream and revision parts
    ///
    /// Revision is identified by scanning from right to left, but with special handling
    /// for cases where multiple dashes are followed by digits (revisions can contain dashes).
    /// This distinguishes between:
    /// - Upstream suffixes: "1.0-rc1", "2.0-beta", "1.5-alpha2" (pre-release markers)
    /// - Actual revisions: "1.0-5", "1.0-1ubuntu2", "1.0-2.el8", "2.14.14-z" (dash + digit/letter)
    /// - Revisions with dashes: "1.48.0-2-3" -> revision="2-3" (revisions can contain dashes)
    ///
    /// Pre-release markers (rc, beta, alpha, etc.) are treated as part of upstream.
    /// Other suffixes after the first valid dash are treated as revisions.
    fn split_upstream_revision(version_part: &str) -> (String, String) {
        // Find all dash positions
        let dash_positions: Vec<usize> = version_part.match_indices('-').map(|(pos, _)| pos).collect();

        if dash_positions.is_empty() {
            // No dashes - entire part is upstream
            return (version_part.to_string(), "0".to_string());
        }

        // Check dashes from right to left, but track the leftmost dash followed by digits
        // This handles cases like "1.48.0-2-3" where revision="2-3" contains dashes
        let mut rightmost_candidate: Option<usize> = None;
        let mut leftmost_digit_dash: Option<usize> = None;

        for &dash_pos in dash_positions.iter().rev() {
            let after_dash = &version_part[dash_pos + 1..];
            if after_dash.is_empty() {
                continue;
            }

            let first_char = after_dash.chars().next().unwrap();

            if first_char.is_ascii_digit() {
                // Dash followed by digit - track both rightmost and leftmost
                if rightmost_candidate.is_none() {
                    rightmost_candidate = Some(dash_pos);
                }
                leftmost_digit_dash = Some(dash_pos);
                // Continue to check if there are more dashes to the left
            } else if first_char.is_ascii_alphabetic() {
                let lower_after = after_dash.to_lowercase();
                // Check if it's a pre-release marker
                let is_prerelease = lower_after.starts_with("rc") ||
                                    lower_after.starts_with("beta") ||
                                    lower_after.starts_with("alpha") ||
                                    lower_after.starts_with("pre") ||
                                    lower_after.starts_with("dev") ||
                                    lower_after.starts_with("snapshot");

                if !is_prerelease {
                    // Not a pre-release marker
                    // If we already found a dash followed by a digit, use that instead
                    // (e.g., "1.0-git20230101-1" -> use "-1", not "-git20230101")
                    if rightmost_candidate.is_some() && leftmost_digit_dash.is_some() {
                        // We already have a digit dash, ignore this letter dash
                        break;
                    }
                    // Otherwise, treat as revision (e.g., "2.14.14-z")
                    if rightmost_candidate.is_none() {
                        rightmost_candidate = Some(dash_pos);
                    }
                    // Stop here - we found a non-prerelease letter, so this is the revision separator
                    break;
                }
                // Otherwise, it's a pre-release marker, continue looking for earlier dashes
            } else {
                // Dash followed by something else (not digit or letter) - stop here
                // This might be part of upstream
                break;
            }
        }

        if let Some(separator_pos) = rightmost_candidate {
            // If we found multiple dashes followed by digits, use the leftmost one
            // This handles "1.48.0-2-3" -> revision="2-3"
            let final_separator = leftmost_digit_dash.unwrap_or(separator_pos);
            let upstream_part = &version_part[..final_separator];
            let revision_part = &version_part[final_separator + 1..];
            (upstream_part.to_string(), revision_part.to_string())
        } else {
            // No revision found - entire part is upstream
            (version_part.to_string(), "0".to_string())
        }
    }

    /// Normalize APK revision format for comparison
    /// In Alpine/APK, -r0, -r1, etc. are explicit revisions.
    /// For comparison: strip 'r' prefix and compare numerically, but mark as explicit
    /// Explicit revisions should compare less than implicit ones when numeric values are equal.
    pub fn normalize_apk_revision(revision: &str) -> String {
        // If revision starts with 'r' followed by digits, strip the 'r' and compare numerically
        // But we need to distinguish explicit vs implicit. For now, strip 'r' and compare.
        // Note: This means r0 == 0 numerically, but we want r0 < 0 (explicit < implicit)
        // We'll handle this by checking if both are "0" and one is explicit
        if revision.len() > 1 && revision.starts_with('r') {
            let after_r = &revision[1..];
            if after_r.chars().all(|c| c.is_ascii_digit()) {
                // Return with a special marker to indicate it's explicit
                // Use a character that sorts before digits: ~
                return format!("~{}", after_r);
            }
        }
        revision.to_string()
    }

    /// Check if a string after a dash is a recognized pre-release marker
    pub fn is_prerelease_marker(after_dash: &str) -> bool {
        if after_dash.is_empty() {
            return false;
        }
        let first_char = after_dash.chars().next().unwrap();
        if !first_char.is_ascii_alphabetic() {
            return false;
        }
        let lower_after = after_dash.to_lowercase();
        // Check for multi-character pre-release markers
        if lower_after.starts_with("rc") ||
           lower_after.starts_with("beta") ||
           lower_after.starts_with("alpha") ||
           lower_after.starts_with("pre") ||
           lower_after.starts_with("dev") ||
           lower_after.starts_with("snapshot") {
            return true;
        }
        // Check for single-letter pre-release markers (e.g., "a0", "b0", "a1", "b2")
        // These are common in Conda versioning (alpha, beta, etc.)
        // But be careful: "b24" could be a build identifier, not a beta pre-release
        // Only treat as pre-release if it's a single letter followed by digits and
        // the pattern matches common pre-release formats (typically short numbers like "a0", "b1")
        if after_dash.len() >= 2 {
            let second_char = after_dash.chars().nth(1).unwrap();
            if second_char.is_ascii_digit() {
                // For single-letter markers, only treat as pre-release if:
                // 1. It's "a" (alpha) or "b" (beta) followed by a short number (typically 0-9)
                // 2. The number is very short (single digit or low double digits)
                // This is a heuristic to distinguish "b1" (beta 1) from "b24" (build 24)
                let first_lower = first_char.to_ascii_lowercase();
                if first_lower == 'a' || first_lower == 'b' {
                    // Extract the numeric part
                    let numeric_part = &after_dash[1..];
                    if let Ok(num) = numeric_part.parse::<u32>() {
                        // Only treat as pre-release if the number is small (typically 0-9, sometimes up to 20)
                        // This distinguishes "a0", "a1", "b2" (pre-releases) from "b24", "a100" (build identifiers)
                        if num <= 20 {
                            return true;
                        }
                    }
                }
            }
        }
        false
    }

    /// Extract a segment stopping at dots (for RPM version comparison)
    pub fn extract_segment_stopping_at_dot(chars: &mut std::iter::Peekable<std::str::Chars>) -> (String, bool) {
        let mut segment = String::new();
        let mut is_digit = false;
        let mut first_char = true;

        while let Some(&ch) = chars.peek() {
            // Stop at dots - they're segment separators
            if ch == '.' {
                break;
            }

            if first_char {
                is_digit = ch.is_ascii_digit();
                first_char = false;
            } else if ch.is_ascii_digit() != is_digit {
                // Changed from digit to non-digit or vice versa - stop here
                break;
            }

            segment.push(ch);
            chars.next();
        }

        // Skip the dot if we stopped at one
        if chars.peek() == Some(&'.') {
            chars.next();
        }

        (segment, is_digit)
    }
}

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

    #[test]
    fn test_version_parsing() {
        let v1 = PackageVersion::parse("2:1.18.3~beta+dfsg1-5+b1").unwrap();
        assert_eq!(v1.epoch, 2);
        assert_eq!(v1.upstream, "1.18.3~beta+dfsg1");
        assert_eq!(v1.revision, "5+b1");

        let v2 = PackageVersion::parse("1.0").unwrap();
        assert_eq!(v2.epoch, 0);
        assert_eq!(v2.upstream, "1.0");
        assert_eq!(v2.revision, "0");

        let v3 = PackageVersion::parse("1:2.0-3").unwrap();
        assert_eq!(v3.epoch, 1);
        assert_eq!(v3.upstream, "2.0");
        assert_eq!(v3.revision, "3");

        // Test alphabetic revision
        let v4 = PackageVersion::parse("2.14.14-z").unwrap();
        assert_eq!(v4.epoch, 0);
        assert_eq!(v4.upstream, "2.14.14");
        assert_eq!(v4.revision, "z");
    }

    #[test]
    fn test_upstream_with_dashes() {
        // Upstream versions with pre-release markers (no revision)
        let v1 = PackageVersion::parse("1.0-rc1").unwrap();
        assert_eq!(v1.epoch, 0);
        assert_eq!(v1.upstream, "1.0-rc1");
        assert_eq!(v1.revision, "0");

        let v2 = PackageVersion::parse("2.0-beta").unwrap();
        assert_eq!(v2.epoch, 0);
        assert_eq!(v2.upstream, "2.0-beta");
        assert_eq!(v2.revision, "0");

        let v3 = PackageVersion::parse("1.5-alpha2").unwrap();
        assert_eq!(v3.epoch, 0);
        assert_eq!(v3.upstream, "1.5-alpha2");
        assert_eq!(v3.revision, "0");

        // Upstream with pre-release markers AND revision
        let v4 = PackageVersion::parse("1.0-rc1-5").unwrap();
        assert_eq!(v4.epoch, 0);
        assert_eq!(v4.upstream, "1.0-rc1");
        assert_eq!(v4.revision, "5");

        let v5 = PackageVersion::parse("2.0-beta-1ubuntu2").unwrap();
        assert_eq!(v5.epoch, 0);
        assert_eq!(v5.upstream, "2.0-beta");
        assert_eq!(v5.revision, "1ubuntu2");

        // With epoch
        let v6 = PackageVersion::parse("2:1.0-rc1").unwrap();
        assert_eq!(v6.epoch, 2);
        assert_eq!(v6.upstream, "1.0-rc1");
        assert_eq!(v6.revision, "0");

        let v7 = PackageVersion::parse("1:1.0-rc1-3").unwrap();
        assert_eq!(v7.epoch, 1);
        assert_eq!(v7.upstream, "1.0-rc1");
        assert_eq!(v7.revision, "3");
    }

    #[test]
    fn test_complex_upstream_parsing() {
        // Multiple dashes in upstream, revision starts with digit
        let v1 = PackageVersion::parse("1.0-beta-rc2-1").unwrap();
        assert_eq!(v1.upstream, "1.0-beta-rc2");
        assert_eq!(v1.revision, "1");

        // Multiple dashes, no revision (last part starts with letter)
        let v2 = PackageVersion::parse("1.0-beta-rc2").unwrap();
        assert_eq!(v2.upstream, "1.0-beta-rc2");
        assert_eq!(v2.revision, "0");

        // Real-world examples
        let v3 = PackageVersion::parse("7.4.052-1ubuntu3").unwrap();
        assert_eq!(v3.upstream, "7.4.052");
        assert_eq!(v3.revision, "1ubuntu3");

        let v4 = PackageVersion::parse("1.2.3-rc1-2.el8").unwrap();
        assert_eq!(v4.upstream, "1.2.3-rc1");
        assert_eq!(v4.revision, "2.el8");

        // Version with git hash-like suffix
        let v5 = PackageVersion::parse("1.0-git20230101-1").unwrap();
        assert_eq!(v5.upstream, "1.0-git20230101");
        assert_eq!(v5.revision, "1");

        // Version with revision containing dashes (Debian format)
        // This is the case that was failing: "1.48.0-2-3" should parse as revision="2-3"
        let v6 = PackageVersion::parse("1.48.0-2-3").unwrap();
        assert_eq!(v6.upstream, "1.48.0");
        assert_eq!(v6.revision, "2-3");

        // Test that "1.48.0-2-3" >= "1.48.0-2" comparison works correctly
        let v7 = PackageVersion::parse("1.48.0-2").unwrap();
        assert_eq!(v7.upstream, "1.48.0");
        assert_eq!(v7.revision, "2");
        // v6 should be greater than v7 because revision "2-3" > "2"
        assert_eq!(v6.compare(&v7), Ordering::Greater);
    }

    #[test]
    fn test_edge_cases_parsing() {
        // Just revision number
        let v1 = PackageVersion::parse("5").unwrap();
        assert_eq!(v1.upstream, "5");
        assert_eq!(v1.revision, "0");

        // Single dash with letter
        let v2 = PackageVersion::parse("1-beta").unwrap();
        assert_eq!(v2.upstream, "1-beta");
        assert_eq!(v2.revision, "0");

        // Single dash with number
        let v3 = PackageVersion::parse("1-5").unwrap();
        assert_eq!(v3.upstream, "1");
        assert_eq!(v3.revision, "5");

        // Empty after dash
        let v4 = PackageVersion::parse("1.0-").unwrap();
        assert_eq!(v4.upstream, "1.0-");
        assert_eq!(v4.revision, "0");

        // Multiple consecutive dashes
        let v5 = PackageVersion::parse("1.0--5").unwrap();
        assert_eq!(v5.upstream, "1.0-");
        assert_eq!(v5.revision, "5");
    }

}