use crate::utils::{colors, multi_progress, packages, project, spinner};
use anyhow::{Context, Result};
use std::cell::RefCell;
use std::cmp::Ordering;
use std::collections::HashMap;
use std::io::Write;
use std::process::Command;

fn fmt_version(v: &str) -> String {
    if v.starts_with('v') {
        v.to_string()
    } else {
        format!("v{}", v)
    }
}

fn version_change(before: &str, after: &str) -> colors::VersionChange {
    match cmp_version(before, after) {
        Ordering::Less => colors::VersionChange::Upgraded,
        Ordering::Equal => colors::VersionChange::Unchanged,
        Ordering::Greater => colors::VersionChange::Downgraded,
    }
}

fn cmp_version(a: &str, b: &str) -> Ordering {
    let a = a.trim_start_matches('v');
    let b = b.trim_start_matches('v');
    let parts_a: Vec<u64> = a
        .split('.')
        .filter_map(|s| s.split('-').next())
        .filter_map(|s| s.parse().ok())
        .collect();
    let parts_b: Vec<u64> = b
        .split('.')
        .filter_map(|s| s.split('-').next())
        .filter_map(|s| s.parse().ok())
        .collect();
    for (pa, pb) in parts_a.iter().zip(parts_b.iter()) {
        match pa.cmp(pb) {
            Ordering::Equal => continue,
            o => return o,
        }
    }
    parts_a.len().cmp(&parts_b.len())
}

pub fn run(
    package_patterns: &[String],
    dry_run: bool,
    auto_select: bool,
    verbose: bool,
) -> Result<()> {
    let project_type = project::detect()?.context("No uv/poetry/cargo project found")?;

    if package_patterns.is_empty() {
        update_all(project_type, dry_run, verbose)?;
        return Ok(());
    }

    let all_packages = packages::list(project_type)?;
    let mut combined: Vec<String> = Vec::new();
    for pattern in package_patterns {
        let matches = packages::fuzzy_match(&all_packages, pattern)?;
        for m in matches {
            if !combined.contains(&m) {
                combined.push(m);
            }
        }
    }

    if combined.is_empty() {
        anyhow::bail!("No packages matched");
    }

    let selected: Vec<String> = if package_patterns.len() == 1 {
        let one = if dry_run || auto_select {
            packages::select_one_with_auto_select(combined, auto_select)?
        } else {
            packages::select_one(combined)?
        };
        vec![one]
    } else {
        let many = if dry_run || auto_select {
            packages::select_many_with_auto_select(combined, auto_select)?
        } else {
            packages::select_many(combined)?
        };
        if many.is_empty() {
            anyhow::bail!("No packages selected");
        }
        many
    };

    update_packages(project_type, &selected, dry_run, verbose)?;

    Ok(())
}

fn update_packages(
    project_type: project::ProjectType,
    packages: &[String],
    dry_run: bool,
    verbose: bool,
) -> Result<()> {
    if packages.is_empty() {
        return Ok(());
    }

    if dry_run {
        match project_type {
            project::ProjectType::Uv => {
                for p in packages {
                    println!("uv lock --upgrade-package \"{}\"", p);
                }
                println!("uv sync --all-extras");
            }
            project::ProjectType::Poetry => {
                let args: Vec<&str> = packages.iter().map(String::as_str).collect();
                println!("poetry update {}", args.join(" "));
            }
            project::ProjectType::Cargo => {
                let args: Vec<String> = packages
                    .iter()
                    .flat_map(|p| vec!["-p".to_string(), p.clone()])
                    .collect();
                println!("cargo update {}", args.join(" "));
            }
        }
        return Ok(());
    }

    let stdout_buf = RefCell::new(Vec::<u8>::new());
    let stderr_buf = RefCell::new(Vec::<u8>::new());

    let run_update = || -> Result<()> {
        let forward = |out: &std::process::Output| {
            if verbose {
                stdout_buf.borrow_mut().extend_from_slice(&out.stdout);
                stderr_buf.borrow_mut().extend_from_slice(&out.stderr);
            }
        };
        match project_type {
            project::ProjectType::Uv => {
                let mut lock = Command::new("uv");
                lock.arg("lock");
                for p in packages {
                    lock.args(["--upgrade-package", p]);
                }
                let out1 = lock.output().context("Failed to run uv lock")?;
                forward(&out1);
                if !out1.status.success() {
                    anyhow::bail!("uv lock failed");
                }
                let out2 = Command::new("uv")
                    .args(["sync", "--all-extras"])
                    .output()
                    .context("Failed to run uv sync")?;
                forward(&out2);
                if !out2.status.success() {
                    anyhow::bail!("uv sync failed");
                }
                Ok(())
            }
            project::ProjectType::Poetry => {
                let out = Command::new("poetry")
                    .arg("update")
                    .args(packages)
                    .output()
                    .context("Failed to run poetry update")?;
                forward(&out);
                if !out.status.success() {
                    anyhow::bail!("poetry update failed");
                }
                Ok(())
            }
            project::ProjectType::Cargo => {
                let mut cmd = Command::new("cargo");
                cmd.arg("update");
                for p in packages {
                    cmd.args(["-p", p]);
                }
                let out = cmd.output().context("Failed to run cargo update")?;
                forward(&out);
                if !out.status.success() {
                    anyhow::bail!("cargo update failed");
                }
                Ok(())
            }
        }
    };

    let result = if spinner::should_show_spinner() {
        let before_versions: HashMap<String, Option<String>> = packages
            .iter()
            .map(|p| {
                (
                    p.clone(),
                    packages::get_installed_version(project_type, p)
                        .ok()
                        .flatten(),
                )
            })
            .collect();
        let multi = multi_progress::multi_progress_stderr();
        multi_progress::run_spinners_then_single_op(
            &multi,
            packages,
            |one_indexed, total, pkg| {
                if atty::is(atty::Stream::Stderr) {
                    format!(
                        "[{}/{}] {}[{}]{} ",
                        one_indexed,
                        total,
                        colors::ANSI_GREEN,
                        pkg,
                        colors::ANSI_RESET
                    )
                } else {
                    format!("[{}/{}] [{}] ", one_indexed, total, pkg)
                }
            },
            run_update,
            |pkg, success| {
                if success {
                    let before = before_versions
                        .get(pkg)
                        .and_then(|v| v.as_deref())
                        .map(fmt_version)
                        .unwrap_or_else(|| "?".to_string());
                    let after = packages::get_installed_version(project_type, pkg)
                        .ok()
                        .flatten()
                        .map(|v| fmt_version(&v))
                        .unwrap_or_else(|| "?".to_string());
                    let change = version_change(&before, &after);
                    colors::format_bumped_message_colored(&before, &after, change)
                } else {
                    let failed = "Failed";
                    if atty::is(atty::Stream::Stderr) {
                        format!("{}{}{}", colors::ANSI_RED, failed, colors::ANSI_RESET)
                    } else {
                        failed.to_string()
                    }
                }
            },
        )
    } else {
        run_update()
    };

    if verbose {
        let _ = std::io::stdout().write_all(&stdout_buf.borrow());
        let _ = std::io::stderr().write_all(&stderr_buf.borrow());
        let _ = std::io::stdout().flush();
        let _ = std::io::stderr().flush();
    }

    result
}

fn update_all(project_type: project::ProjectType, dry_run: bool, verbose: bool) -> Result<()> {
    if dry_run {
        match project_type {
            project::ProjectType::Uv => {
                println!("uv lock --upgrade");
                println!("uv sync --all-extras");
            }
            project::ProjectType::Poetry => {
                println!("poetry update");
            }
            project::ProjectType::Cargo => {
                println!("cargo update");
            }
        }
        return Ok(());
    }

    let stdout_buf = RefCell::new(Vec::<u8>::new());
    let stderr_buf = RefCell::new(Vec::<u8>::new());

    let forward = |out: &std::process::Output| {
        if verbose {
            stdout_buf.borrow_mut().extend_from_slice(&out.stdout);
            stderr_buf.borrow_mut().extend_from_slice(&out.stderr);
        }
    };

    let run_all = || -> Result<()> {
        match project_type {
            project::ProjectType::Uv => {
                let out1 = Command::new("uv")
                    .args(["lock", "--upgrade"])
                    .output()
                    .context("Failed to run uv lock")?;
                forward(&out1);
                if !out1.status.success() {
                    anyhow::bail!("uv lock failed");
                }
                let out2 = Command::new("uv")
                    .args(["sync", "--all-extras"])
                    .output()
                    .context("Failed to run uv sync")?;
                forward(&out2);
                if !out2.status.success() {
                    anyhow::bail!("uv sync failed");
                }
                Ok(())
            }
            project::ProjectType::Poetry => {
                let out = Command::new("poetry")
                    .arg("update")
                    .output()
                    .context("Failed to run poetry update")?;
                forward(&out);
                if !out.status.success() {
                    anyhow::bail!("poetry update failed");
                }
                Ok(())
            }
            project::ProjectType::Cargo => {
                let out = Command::new("cargo")
                    .arg("update")
                    .output()
                    .context("Failed to run cargo update")?;
                forward(&out);
                if !out.status.success() {
                    anyhow::bail!("cargo update failed");
                }
                Ok(())
            }
        }
    };

    let result = if spinner::should_show_spinner() {
        let multi = multi_progress::multi_progress_stderr();
        multi_progress::run_header_spinner(
            &multi,
            "Updating all packages...",
            "✓ Updated",
            "✗ Failed",
            run_all,
        )
    } else {
        run_all()
    };

    if verbose {
        let _ = std::io::stdout().write_all(&stdout_buf.borrow());
        let _ = std::io::stderr().write_all(&stderr_buf.borrow());
        let _ = std::io::stdout().flush();
        let _ = std::io::stderr().flush();
    }

    result
}

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

    #[test]
    fn test_fmt_version_adds_v_prefix() {
        assert_eq!(fmt_version("1.0.0"), "v1.0.0");
        assert_eq!(fmt_version("0.29.0"), "v0.29.0");
    }

    #[test]
    fn test_fmt_version_preserves_existing_v() {
        assert_eq!(fmt_version("v1.0.0"), "v1.0.0");
    }

    #[test]
    fn test_cmp_version_upgraded() {
        assert_eq!(cmp_version("1.0.0", "1.0.102"), Ordering::Less);
        assert_eq!(cmp_version("v1.0.0", "1.0.102"), Ordering::Less);
        assert_eq!(cmp_version("0.29.0", "0.30.0"), Ordering::Less);
        assert_eq!(cmp_version("1.0", "1.0.1"), Ordering::Less);
    }

    #[test]
    fn test_cmp_version_unchanged() {
        assert_eq!(cmp_version("1.0.102", "1.0.102"), Ordering::Equal);
        assert_eq!(cmp_version("v1.0.102", "v1.0.102"), Ordering::Equal);
    }

    #[test]
    fn test_cmp_version_downgraded() {
        assert_eq!(cmp_version("1.0.102", "1.0.0"), Ordering::Greater);
        assert_eq!(cmp_version("1.0.1", "1.0"), Ordering::Greater);
    }

    #[test]
    fn test_version_change() {
        assert_eq!(
            version_change("1.0.0", "1.0.102"),
            colors::VersionChange::Upgraded
        );
        assert_eq!(
            version_change("1.0.102", "1.0.102"),
            colors::VersionChange::Unchanged
        );
        assert_eq!(
            version_change("1.0.102", "1.0.0"),
            colors::VersionChange::Downgraded
        );
    }

    #[test]
    fn test_update_packages_dry_run_uv() {
        let result = update_packages(ProjectType::Uv, &["test-package".into()], true, false);
        assert!(result.is_ok());
    }

    #[test]
    fn test_update_packages_dry_run_poetry() {
        let result = update_packages(ProjectType::Poetry, &["test-package".into()], true, false);
        assert!(result.is_ok());
    }

    #[test]
    fn test_update_packages_dry_run_cargo() {
        let result = update_packages(ProjectType::Cargo, &["test-package".into()], true, false);
        assert!(result.is_ok());
    }

    #[test]
    fn test_update_packages_dry_run_empty() {
        let result = update_packages(ProjectType::Cargo, &[], true, false);
        assert!(result.is_ok());
    }

    #[test]
    fn test_update_packages_dry_run_special_chars() {
        let result = update_packages(
            ProjectType::Cargo,
            &["test-package_v1.0".into()],
            true,
            false,
        );
        assert!(result.is_ok());
    }

    #[test]
    fn test_update_packages_dry_run_multiple() {
        let result = update_packages(
            ProjectType::Cargo,
            &["pkg-a".into(), "pkg-b".into()],
            true,
            false,
        );
        assert!(result.is_ok());
    }

    #[test]
    fn test_update_all_dry_run_uv() {
        let result = update_all(ProjectType::Uv, true, false);
        assert!(result.is_ok());
    }

    #[test]
    fn test_update_all_dry_run_poetry() {
        let result = update_all(ProjectType::Poetry, true, false);
        assert!(result.is_ok());
    }

    #[test]
    fn test_update_all_dry_run_cargo() {
        let result = update_all(ProjectType::Cargo, true, false);
        assert!(result.is_ok());
    }

    #[test]
    fn test_update_packages_output_format() {
        let result = update_packages(ProjectType::Cargo, &["test".into()], true, false);
        assert!(result.is_ok());
    }

    #[test]
    fn test_update_all_output_format() {
        let result = update_all(ProjectType::Cargo, true, false);
        assert!(result.is_ok());
    }

    #[test]
    fn test_update_packages_with_special_chars() {
        let result = update_packages(
            ProjectType::Cargo,
            &["test-package_v1.0".into()],
            true,
            false,
        );
        assert!(result.is_ok());
    }

    #[test]
    fn test_update_all_uv() {
        let result = update_all(ProjectType::Uv, true, false);
        assert!(result.is_ok());
    }

    #[test]
    fn test_update_all_poetry() {
        let result = update_all(ProjectType::Poetry, true, false);
        assert!(result.is_ok());
    }

    #[test]
    fn test_update_packages_uv() {
        let result = update_packages(ProjectType::Uv, &["test-package".into()], true, false);
        assert!(result.is_ok());
    }

    #[test]
    fn test_update_packages_poetry() {
        let result = update_packages(ProjectType::Poetry, &["test-package".into()], true, false);
        assert!(result.is_ok());
    }

    #[test]
    fn test_update_packages_cargo() {
        let result = update_packages(ProjectType::Cargo, &["test-package".into()], true, false);
        assert!(result.is_ok());
    }
}
