#![allow(clippy::disallowed_methods, reason = "tooling is exempt")]
use std::io::{self, Write};
use std::process::{Command, Output, Stdio};

use anyhow::{Context as _, Result, bail};
use clap::Parser;

#[derive(Parser)]
pub struct PublishGpuiArgs {
    /// Optional pre-release identifier to append to the version (e.g., alpha, test.1). Always bumps the minor version.
    #[arg(long)]
    pre_release: Option<String>,

    /// Perform a dry-run and wait for user confirmation before each publish
    #[arg(long)]
    dry_run: bool,
}

pub fn run_publish_gpui(args: PublishGpuiArgs) -> Result<()> {
    println!(
        "Starting GPUI publish process{}...",
        if args.dry_run { " (with dry-run)" } else { "" }
    );

    let start_time = std::time::Instant::now();
    check_workspace_root()?;
    ensure_cargo_set_version()?;
    check_git_clean()?;

    let current_version = read_gpui_version()?;
    let new_version = bump_version(&current_version, args.pre_release.as_deref())?;
    println!(
        "Updating GPUI version: {} -> {}",
        current_version, new_version
    );
    publish_dependencies(&new_version, args.dry_run)?;
    publish_gpui(&new_version, args.dry_run)?;
    println!("GPUI published in {}s", start_time.elapsed().as_secs_f32());
    Ok(())
}

fn read_gpui_version() -> Result<String> {
    let gpui_cargo_toml_path = "crates/gpui/Cargo.toml";
    let contents = std::fs::read_to_string(gpui_cargo_toml_path)
        .context("Failed to read crates/gpui/Cargo.toml")?;

    let cargo_toml: toml::Value =
        toml::from_str(&contents).context("Failed to parse crates/gpui/Cargo.toml")?;

    let version = cargo_toml
        .get("package")
        .and_then(|p| p.get("version"))
        .and_then(|v| v.as_str())
        .context("Failed to find version in crates/gpui/Cargo.toml")?;

    Ok(version.to_string())
}

fn bump_version(current_version: &str, pre_release: Option<&str>) -> Result<String> {
    // Strip any existing metadata and pre-release
    let without_metadata = current_version.split('+').next().unwrap();
    let base_version = without_metadata.split('-').next().unwrap();

    // Parse major.minor.patch
    let parts: Vec<&str> = base_version.split('.').collect();
    if parts.len() != 3 {
        bail!("Invalid version format: {}", current_version);
    }

    let major: u32 = parts[0].parse().context("Failed to parse major version")?;
    let minor: u32 = parts[1].parse().context("Failed to parse minor version")?;

    // Always bump minor version
    let new_version = format!("{}.{}.0", major, minor + 1);

    // Add pre-release if specified
    if let Some(pre) = pre_release {
        Ok(format!("{}-{}", new_version, pre))
    } else {
        Ok(new_version)
    }
}

fn publish_dependencies(new_version: &str, dry_run: bool) -> Result<()> {
    let gpui_dependencies = vec![
        ("zed-collections", "collections"),
        ("zed-perf", "perf"),
        ("zed-util-macros", "util_macros"),
        ("zed-util", "util"),
        ("gpui-macros", "gpui_macros"),
        ("zed-http-client", "http_client"),
        ("zed-derive-refineable", "derive_refineable"),
        ("zed-refineable", "refineable"),
        ("zed-semantic-version", "semantic_version"),
        ("zed-sum-tree", "sum_tree"),
        ("zed-media", "media"),
    ];

    for (crate_name, package_name) in gpui_dependencies {
        println!(
            "Publishing dependency: {} (package: {})",
            crate_name, package_name
        );

        update_crate_version(crate_name, new_version)?;
        update_workspace_dependency_version(package_name, new_version)?;
        publish_crate(crate_name, dry_run)?;

        // println!("Waiting 60s for the rate limit...");
        // thread::sleep(Duration::from_secs(60));
    }

    Ok(())
}

fn publish_gpui(new_version: &str, dry_run: bool) -> Result<()> {
    update_crate_version("gpui", new_version)?;

    publish_crate("gpui", dry_run)?;

    Ok(())
}

fn update_crate_version(package_name: &str, new_version: &str) -> Result<()> {
    let output = run_command(
        Command::new("cargo")
            .arg("set-version")
            .arg("--package")
            .arg(package_name)
            .arg(new_version),
    )?;

    if !output.status.success() {
        bail!("Failed to set version for package {}", package_name);
    }

    Ok(())
}

fn publish_crate(crate_name: &str, dry_run: bool) -> Result<()> {
    let publish_crate_impl = |crate_name, dry_run| {
        let cargo = std::env::var("CARGO").unwrap_or_else(|_| "cargo".to_string());

        let mut command = Command::new(&cargo);
        command
            .arg("publish")
            .arg("--allow-dirty")
            .args(["-p", crate_name]);

        if dry_run {
            command.arg("--dry-run");
        }

        run_command(&mut command)?;

        anyhow::Ok(())
    };

    if dry_run {
        publish_crate_impl(crate_name, true)?;

        print!("Press Enter to publish for real (or ctrl-c to abort)...");
        io::stdout().flush()?;

        let mut input = String::new();
        io::stdin().read_line(&mut input)?;
    }

    publish_crate_impl(crate_name, false)?;

    Ok(())
}

fn update_workspace_dependency_version(package_name: &str, new_version: &str) -> Result<()> {
    let workspace_cargo_toml_path = "Cargo.toml";
    let contents = std::fs::read_to_string(workspace_cargo_toml_path)
        .context("Failed to read workspace Cargo.toml")?;

    let updated = update_dependency_version_in_toml(&contents, package_name, new_version)?;

    std::fs::write(workspace_cargo_toml_path, updated)
        .context("Failed to write workspace Cargo.toml")?;

    Ok(())
}

fn update_dependency_version_in_toml(
    toml_contents: &str,
    package_name: &str,
    new_version: &str,
) -> Result<String> {
    let mut doc = toml_contents
        .parse::<toml_edit::DocumentMut>()
        .context("Failed to parse TOML")?;

    // Navigate to workspace.dependencies.<package_name>
    let dependency = doc
        .get_mut("workspace")
        .and_then(|w| w.get_mut("dependencies"))
        .and_then(|d| d.get_mut(package_name))
        .context(format!(
            "Failed to find {} in workspace dependencies",
            package_name
        ))?;

    // Update the version field if it exists
    if let Some(dep_table) = dependency.as_table_like_mut() {
        if dep_table.contains_key("version") {
            dep_table.insert("version", toml_edit::value(new_version));
        } else {
            bail!(
                "No version field found for {} in workspace dependencies",
                package_name
            );
        }
    } else {
        bail!("{} is not a table in workspace dependencies", package_name);
    }

    Ok(doc.to_string())
}

fn check_workspace_root() -> Result<()> {
    let cwd = std::env::current_dir().context("Failed to get current directory")?;

    // Check if Cargo.toml exists in the current directory
    let cargo_toml_path = cwd.join("Cargo.toml");
    if !cargo_toml_path.exists() {
        bail!(
            "Cargo.toml not found in current directory. Please run this command from the workspace root."
        );
    }

    // Check if it's a workspace by looking for [workspace] section
    let contents =
        std::fs::read_to_string(&cargo_toml_path).context("Failed to read Cargo.toml")?;

    if !contents.contains("[workspace]") {
        bail!(
            "Current directory does not appear to be a workspace root. Please run this command from the workspace root."
        );
    }

    Ok(())
}

fn ensure_cargo_set_version() -> Result<()> {
    let output = run_command(
        Command::new("which")
            .arg("cargo-set-version")
            .stdout(Stdio::piped()),
    )
    .context("Failed to check for cargo-set-version")?;

    if !output.status.success() {
        println!("cargo-set-version not found. Installing cargo-edit...");

        let install_output = run_command(Command::new("cargo").arg("install").arg("cargo-edit"))?;

        if !install_output.status.success() {
            bail!("Failed to install cargo-edit");
        }
    }

    Ok(())
}

fn check_git_clean() -> Result<()> {
    let output = run_command(
        Command::new("git")
            .args(["status", "--porcelain"])
            .stdout(Stdio::piped())
            .stderr(Stdio::piped()),
    )?;

    if !output.status.success() {
        bail!("git status command failed");
    }

    let stdout = String::from_utf8_lossy(&output.stdout);
    if !stdout.trim().is_empty() {
        bail!(
            "Working directory is not clean. Please commit or stash your changes before publishing."
        );
    }

    Ok(())
}

fn run_command(command: &mut Command) -> Result<Output> {
    let command_str = {
        let program = command.get_program().to_string_lossy();
        let args = command
            .get_args()
            .map(|arg| arg.to_string_lossy())
            .collect::<Vec<_>>()
            .join(" ");

        if args.is_empty() {
            program.to_string()
        } else {
            format!("{} {}", program, args)
        }
    };
    eprintln!("+ {}", command_str);

    let output = command
        .spawn()
        .context("failed to spawn child process")?
        .wait_with_output()
        .context("failed to wait for child process")?;

    Ok(output)
}

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

    use super::*;

    #[test]
    fn test_update_dependency_version_in_toml() {
        let input = indoc! {r#"
            [workspace]
            resolver = "2"

            [workspace.dependencies]
            # here's a comment
            collections = { path = "crates/collections", package = "zed-collections", version = "0.1.0" }

            util = { path = "crates/util", package = "zed-util", version = "0.1.0" }
        "#};

        let result = update_dependency_version_in_toml(input, "collections", "0.2.0").unwrap();

        let output = indoc! {r#"
            [workspace]
            resolver = "2"

            [workspace.dependencies]
            # here's a comment
            collections = { path = "crates/collections", package = "zed-collections", version = "0.2.0" }

            util = { path = "crates/util", package = "zed-util", version = "0.1.0" }
        "#};

        assert_eq!(result, output);
    }

    #[test]
    fn test_bump_version() {
        // Test bumping minor version (default behavior)
        assert_eq!(bump_version("0.1.0", None).unwrap(), "0.2.0");
        assert_eq!(bump_version("0.1.5", None).unwrap(), "0.2.0");
        assert_eq!(bump_version("1.42.7", None).unwrap(), "1.43.0");

        // Test stripping pre-release and bumping minor
        assert_eq!(bump_version("0.1.0-alpha.1", None).unwrap(), "0.2.0");
        assert_eq!(bump_version("0.1.0-beta", None).unwrap(), "0.2.0");

        // Test stripping existing metadata and bumping
        assert_eq!(bump_version("0.1.0+old.metadata", None).unwrap(), "0.2.0");

        // Test bumping minor with pre-release
        assert_eq!(bump_version("0.1.0", Some("alpha")).unwrap(), "0.2.0-alpha");

        // Test bumping minor with complex pre-release identifier
        assert_eq!(
            bump_version("0.1.0", Some("test.1")).unwrap(),
            "0.2.0-test.1"
        );

        // Test bumping from existing pre-release adds new pre-release
        assert_eq!(
            bump_version("0.1.0-alpha", Some("beta")).unwrap(),
            "0.2.0-beta"
        );

        // Test bumping and stripping metadata while adding pre-release
        assert_eq!(
            bump_version("0.1.0+metadata", Some("alpha")).unwrap(),
            "0.2.0-alpha"
        );
    }
}
