//! Version check for oxi CLI
//!
//! Checks crates.io for the latest version and compares with current version.

use anyhow::{Context, Result};
use serde::Deserialize;
use std::time::Duration;
use tracing::{debug, info, warn};

/// Version information from crates.io
#[derive(Debug, Clone, Deserialize)]
pub struct CrateVersion {
    /// Version string (e.g., "1.2.3")
    pub num: String,
    /// ISO 8601 release date
    pub created_at: String,
    /// Download count
    #[serde(default)]
    pub downloads: Option<u64>,
}

/// Response from crates.io API
#[derive(Debug, Deserialize)]
pub struct CratesIoResponse {
/// pub.
    pub crate_info: CrateInfo,
}

#[derive(Debug, Deserialize)]
/// CrateInfo.
pub struct CrateInfo {
/// pub.
    pub latest_version: Option<CrateVersion>,
/// pub.
    #[serde(default)]
    pub versions: Vec<CrateVersion>,
}

/// Complete version information
#[derive(Debug, Clone)]
pub struct VersionInfo {
    /// Current version (from Cargo.toml)
    pub current: String,
    /// Latest version from crates.io
    pub latest: String,
    /// Release date of latest version
    pub release_date: String,
    /// Whether current version is outdated
    pub is_outdated: bool,
    /// Number of versions behind (0 if current)
    pub versions_behind: u32,
    /// URL to release notes / changelog
    pub release_url: String,
}

impl VersionInfo {
    /// Create new version info
    pub fn new(current: String, latest: String, release_date: String) -> Self {
        let is_outdated = compare_versions(&latest, &current) > 0;
        let versions_behind = calculate_versions_behind(&current, &latest);
        let release_url = format!(
            "https://crates.io/crates/oxi-cli/{}",
            latest.replace('.', "-").replace('+', "-")
        );

        Self {
            current,
            latest,
            release_date,
            is_outdated,
            versions_behind,
            release_url,
        }
    }

    /// Get display string for current version
    pub fn current_display(&self) -> String {
        format!("v{}", self.current)
    }

    /// Get display string for latest version
    pub fn latest_display(&self) -> String {
        format!("v{}", self.latest)
    }

    /// Get notification message
    pub fn notification_message(&self) -> String {
        if self.is_outdated {
            format!(
                "oxi {} available (current: {})",
                self.latest_display(),
                self.current_display()
            )
        } else {
            format!("oxi {} (up to date)", self.current_display())
        }
    }

    /// Get summary for display
    pub fn summary(&self) -> String {
        if self.is_outdated {
            if self.versions_behind > 0 {
                format!(
                    "New version available: {} ({} behind)",
                    self.latest_display(),
                    self.versions_behind
                )
            } else {
                format!("New version available: {}", self.latest_display())
            }
        } else {
            "You have the latest version.".to_string()
        }
    }
}

/// Parse version components from a version string
fn parse_version(version: &str) -> Option<(u32, u32, u32)> {
    let version = version.trim().trim_start_matches('v');

    // Handle pre-release versions (e.g., "1.2.3-alpha")
    let version = version.split('-').next().unwrap_or(version);

    let parts: Vec<&str> = version.split('.').collect();
    if parts.len() == 3 {
        let major = parts[0].parse().ok()?;
        let minor = parts[1].parse().ok()?;
        let patch = parts[2]
            .split('+')
            .next()
            .unwrap_or(parts[2])
            .parse()
            .ok()?;
        Some((major, minor, patch))
    } else {
        None
    }
}

/// Compare two version strings
/// Returns: negative if left < right, 0 if equal, positive if left > right
fn compare_versions(left: &str, right: &str) -> i32 {
    let left_parsed = match parse_version(left) {
        Some(v) => v,
        None => return 0,
    };
    let right_parsed = match parse_version(right) {
        Some(v) => v,
        None => return 0,
    };

    if left_parsed.0 != right_parsed.0 {
        return left_parsed.0 as i32 - right_parsed.0 as i32;
    }
    if left_parsed.1 != right_parsed.1 {
        return left_parsed.1 as i32 - right_parsed.1 as i32;
    }
    left_parsed.2 as i32 - right_parsed.2 as i32
}

/// Calculate how many versions behind current is from latest
fn calculate_versions_behind(current: &str, latest: &str) -> u32 {
    let current_parsed = parse_version(current);
    let latest_parsed = parse_version(latest);

    match (current_parsed, latest_parsed) {
        (Some((c_major, c_minor, c_patch)), Some((l_major, l_minor, l_patch))) => {
            if l_major < c_major
                || (l_major == c_major && l_minor < c_minor)
                || (l_major == c_major && l_minor == c_minor && l_patch <= c_patch)
            {
                0
            } else {
                // Weight: major bumps count as 2, minor and patch as 1 each
                let major_diff = (l_major - c_major) as u32;
                let minor_diff = (l_minor.saturating_sub(c_minor)) as u32;
                let patch_diff = (l_patch.saturating_sub(c_patch)) as u32;
                (major_diff * 2 + minor_diff + patch_diff).min(10)
            }
        }
        _ => 0,
    }
}

/// Fetch the latest version from crates.io
pub async fn fetch_latest_version(crate_name: &str) -> Result<CrateVersion> {
    let url = format!("https://crates.io/api/v1/crates/{}", crate_name);

    let client = reqwest::Client::builder()
        .timeout(Duration::from_secs(10))
        .build()
        .context("Failed to create HTTP client")?;

    let response = client
        .get(&url)
        .header(
            "User-Agent",
            format!("oxi-cli/{}", env!("CARGO_PKG_VERSION")),
        )
        .header("Accept", "application/json")
        .send()
        .await
        .context("Failed to fetch from crates.io")?;

    if !response.status().is_success() {
        anyhow::bail!("crates.io returned status: {}", response.status());
    }

    let data: serde_json::Value = response
        .json()
        .await
        .context("Failed to parse crates.io response")?;

    // Extract the latest version
    let latest = data
        .get("crate")
        .and_then(|c| c.get("latest_version"))
        .and_then(|v| v.get("num"))
        .and_then(|v| v.as_str())
        .unwrap_or("0.0.0")
        .to_string();

    let created_at = data
        .get("crate")
        .and_then(|c| c.get("latest_version"))
        .and_then(|v| v.get("created_at"))
        .and_then(|v| v.as_str())
        .unwrap_or("2024-01-01T00:00:00Z")
        .to_string();

    Ok(CrateVersion {
        num: latest,
        created_at,
        downloads: None,
    })
}

/// Check for updates and return version info
pub async fn check_for_updates() -> Result<VersionInfo> {
    let current_version = env!("CARGO_PKG_VERSION");

    // Skip check in offline mode
    if std::env::var("OXI_OFFLINE").is_ok() || std::env::var("OXI_SKIP_VERSION_CHECK").is_ok() {
        debug!("Skipping version check (offline mode)");
        return Ok(VersionInfo::new(
            current_version.to_string(),
            current_version.to_string(),
            String::new(),
        ));
    }

    match fetch_latest_version("oxi-cli").await {
        Ok(crate_info) => {
            info!(
                "Version check: current={}, latest={}",
                current_version, crate_info.num
            );
            Ok(VersionInfo::new(
                current_version.to_string(),
                crate_info.num,
                crate_info.created_at,
            ))
        }
        Err(e) => {
            warn!("Failed to check for updates: {}", e);
            // Don't fail on version check errors - just return current version
            Ok(VersionInfo::new(
                current_version.to_string(),
                current_version.to_string(),
                String::new(),
            ))
        }
    }
}

/// Show version notification in the UI
pub fn show_version_notification(info: &VersionInfo) -> Option<String> {
    if info.is_outdated {
        Some(format!(
            "oxi {} available (current: v{})",
            info.latest_display(),
            info.current
        ))
    } else {
        None
    }
}

/// Format version info for debug output
pub fn format_for_debug(info: &VersionInfo) -> String {
    format!(
        "Version check: current={}, latest={}, outdated={}, date={}",
        info.current, info.latest, info.is_outdated, info.release_date
    )
}

/// Parse release date from ISO 8601 format
pub fn parse_release_date(date_str: &str) -> Option<String> {
    // Input: "2024-01-15T10:30:00Z"
    // Output: "January 15, 2024"

    if date_str.is_empty() {
        return None;
    }

    // Simple parsing without external dependencies
    let parts: Vec<&str> = date_str
        .split(|c| c == '-' || c == 'T' || c == ':' || c == 'Z' || c == '+')
        .collect();

    if parts.len() >= 3 {
        let year: u32 = parts[0].parse().ok()?;
        let month: u32 = parts[1].parse().ok()?;
        let day: u32 = parts[2].parse().ok()?;

        let month_names = [
            "January",
            "February",
            "March",
            "April",
            "May",
            "June",
            "July",
            "August",
            "September",
            "October",
            "November",
            "December",
        ];

        if month >= 1 && month <= 12 {
            return Some(format!(
                "{} {}, {}",
                month_names[month as usize - 1],
                day,
                year
            ));
        }
    }

    None
}

/// Get the full notification with details
pub fn get_full_notification(info: &VersionInfo) -> String {
    let base = info.notification_message();

    if info.is_outdated {
        let date = parse_release_date(&info.release_date)
            .map(|d| format!(" (released {})", d))
            .unwrap_or_default();

        format!("{}{}", base, date)
    } else {
        base
    }
}

// ============================================================================
// Tests
// ============================================================================

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

    #[test]
    fn test_parse_version_basic() {
        assert_eq!(parse_version("1.2.3"), Some((1, 2, 3)));
        assert_eq!(parse_version("0.1.0"), Some((0, 1, 0)));
        assert_eq!(parse_version("10.20.30"), Some((10, 20, 30)));
    }

    #[test]
    fn test_parse_version_with_v_prefix() {
        assert_eq!(parse_version("v1.2.3"), Some((1, 2, 3)));
        assert_eq!(parse_version("v0.0.1"), Some((0, 0, 1)));
    }

    #[test]
    fn test_parse_version_with_prerelease() {
        // Pre-release should be stripped
        assert_eq!(parse_version("1.2.3-alpha"), Some((1, 2, 3)));
        assert_eq!(parse_version("2.0.0-beta.1"), Some((2, 0, 0)));
        assert_eq!(parse_version("1.0.0+build123"), Some((1, 0, 0)));
    }

    #[test]
    fn test_parse_version_invalid() {
        assert_eq!(parse_version("invalid"), None);
        assert_eq!(parse_version("1.2"), None);
        assert_eq!(parse_version(""), None);
        assert_eq!(parse_version("1.2.3.4"), None);
    }

    #[test]
    fn test_compare_versions_equal() {
        assert_eq!(compare_versions("1.2.3", "1.2.3"), 0);
        assert_eq!(compare_versions("0.1.0", "0.1.0"), 0);
        assert_eq!(compare_versions("v1.0.0", "v1.0.0"), 0);
    }

    #[test]
    fn test_compare_versions_less() {
        assert!(compare_versions("1.2.3", "1.2.4") < 0);
        assert!(compare_versions("1.2.3", "1.3.0") < 0);
        assert!(compare_versions("1.2.3", "2.0.0") < 0);
        assert!(compare_versions("0.9.9", "1.0.0") < 0);
    }

    #[test]
    fn test_compare_versions_greater() {
        assert!(compare_versions("1.2.4", "1.2.3") > 0);
        assert!(compare_versions("1.3.0", "1.2.3") > 0);
        assert!(compare_versions("2.0.0", "1.2.3") > 0);
        assert!(compare_versions("1.0.1", "1.0.0") > 0);
    }

    #[test]
    fn test_compare_versions_with_prerelease() {
        // Pre-release comparison is simplified
        assert_eq!(compare_versions("1.2.3-alpha", "1.2.3"), 0);
    }

    #[test]
    fn test_compare_versions_invalid() {
        // Invalid versions return 0 (equal)
        assert_eq!(compare_versions("invalid", "1.0.0"), 0);
        assert_eq!(compare_versions("1.0.0", "invalid"), 0);
    }

    #[test]
    fn test_version_info_creation() {
        let info = VersionInfo::new(
            "1.0.0".to_string(),
            "1.1.0".to_string(),
            "2024-01-15T00:00:00Z".to_string(),
        );

        assert_eq!(info.current, "1.0.0");
        assert_eq!(info.latest, "1.1.0");
        assert!(info.is_outdated);
        assert_eq!(info.versions_behind, 1);
    }

    #[test]
    fn test_version_info_not_outdated() {
        let info = VersionInfo::new(
            "1.1.0".to_string(),
            "1.0.0".to_string(),
            "2024-01-15T00:00:00Z".to_string(),
        );

        assert!(!info.is_outdated);
        assert_eq!(info.versions_behind, 0);
    }

    #[test]
    fn test_version_info_display() {
        let info = VersionInfo::new("1.2.3".to_string(), "1.3.0".to_string(), String::new());

        assert_eq!(info.current_display(), "v1.2.3");
        assert_eq!(info.latest_display(), "v1.3.0");
    }

    #[test]
    fn test_version_info_notification() {
        let outdated = VersionInfo::new("1.0.0".to_string(), "1.1.0".to_string(), String::new());
        assert_eq!(
            outdated.notification_message(),
            "oxi v1.1.0 available (current: v1.0.0)"
        );

        let current = VersionInfo::new("1.1.0".to_string(), "1.1.0".to_string(), String::new());
        assert_eq!(current.notification_message(), "oxi v1.1.0 (up to date)");
    }

    #[test]
    fn test_version_info_summary() {
        let outdated = VersionInfo::new("1.0.0".to_string(), "1.2.0".to_string(), String::new());
        assert!(outdated.summary().contains("New version"));
        assert!(outdated.summary().contains("1.2.0"));

        let current = VersionInfo::new("1.0.0".to_string(), "1.0.0".to_string(), String::new());
        assert_eq!(current.summary(), "You have the latest version.");
    }

    #[test]
    fn test_calculate_versions_behind() {
        assert_eq!(calculate_versions_behind("1.0.0", "1.0.0"), 0);
        assert_eq!(calculate_versions_behind("1.0.0", "1.1.0"), 1);
        assert_eq!(calculate_versions_behind("1.0.0", "2.0.0"), 2);
        assert_eq!(calculate_versions_behind("1.0.0", "1.0.1"), 1);
    }

    #[test]
    fn test_parse_release_date() {
        assert_eq!(
            parse_release_date("2024-01-15T10:30:00Z"),
            Some("January 15, 2024".to_string())
        );
        assert_eq!(
            parse_release_date("2023-12-01T00:00:00Z"),
            Some("December 1, 2023".to_string())
        );
        assert_eq!(parse_release_date(""), None);
        assert_eq!(parse_release_date("invalid"), None);
    }

    #[test]
    fn test_show_version_notification_outdated() {
        let info = VersionInfo::new("1.0.0".to_string(), "1.1.0".to_string(), String::new());

        let notification = show_version_notification(&info);
        assert!(notification.is_some());
        assert!(notification.unwrap().contains("1.1.0"));
    }

    #[test]
    fn test_show_version_notification_current() {
        let info = VersionInfo::new("1.1.0".to_string(), "1.1.0".to_string(), String::new());

        let notification = show_version_notification(&info);
        assert!(notification.is_none());
    }

    #[test]
    fn test_get_full_notification() {
        let info = VersionInfo::new(
            "1.0.0".to_string(),
            "1.1.0".to_string(),
            "2024-01-15T00:00:00Z".to_string(),
        );

        let full = get_full_notification(&info);
        assert!(full.contains("v1.1.0"));
        assert!(full.contains("January"));
    }

    #[test]
    fn test_format_for_debug() {
        let info = VersionInfo::new(
            "1.0.0".to_string(),
            "1.1.0".to_string(),
            "2024-01-15T00:00:00Z".to_string(),
        );

        let debug = format_for_debug(&info);
        assert!(debug.contains("1.0.0"));
        assert!(debug.contains("1.1.0"));
        assert!(debug.contains("true"));
    }

    #[test]
    fn test_version_info_release_url() {
        let info = VersionInfo::new("1.0.0".to_string(), "1.2.3".to_string(), String::new());

        assert!(info.release_url.contains("crates.io"));
        assert!(info.release_url.contains("1-2-3"));
    }

    #[test]
    fn test_version_info_edge_cases() {
        // Alpha version
        let info = VersionInfo::new(
            "0.1.5-alpha".to_string(),
            "0.2.0".to_string(),
            String::new(),
        );
        assert!(info.is_outdated);

        // Empty release date
        let info = VersionInfo::new("1.0.0".to_string(), "1.0.0".to_string(), String::new());
        assert!(!info.is_outdated);
    }
}
