use crate::cache::Cache;
use crate::error::{Result, OilError};
use console::style;
use serde::{Deserialize, Serialize};
use std::path::{Path, PathBuf};
use tokio::process::Command;
use tracing::instrument;

#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct Oilfile {
    #[serde(default)]
    pub tap: Vec<String>,
    #[serde(default)]
    pub brew: Vec<BundleEntry>,
    #[serde(default)]
    pub cask: Vec<BundleEntry>,
    #[serde(default)]
    pub cargo: Vec<BundleEntry>,
    #[serde(default)]
    pub uv: Vec<BundleEntry>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
pub enum BundleEntry {
    Simple(String),
    Detailed {
        name: String,
        #[serde(default)]
        version: Option<String>,
        #[serde(default)]
        args: Option<Vec<String>>,
    },
}

impl BundleEntry {
    pub fn name(&self) -> &str {
        match self {
            BundleEntry::Simple(s) => s,
            BundleEntry::Detailed { name, .. } => name,
        }
    }

    pub fn version(&self) -> Option<&str> {
        match self {
            BundleEntry::Simple(_) => None,
            BundleEntry::Detailed { version, .. } => version.as_deref(),
        }
    }

    pub fn args(&self) -> Option<&[String]> {
        match self {
            BundleEntry::Simple(_) => None,
            BundleEntry::Detailed { args, .. } => args.as_deref(),
        }
    }
}

fn find_waxfile() -> Result<PathBuf> {
    let candidates = ["Oilfile", "Oilfile.toml", "waxfile", "waxfile.toml"];
    for name in &candidates {
        let path = PathBuf::from(name);
        if path.exists() {
            return Ok(path);
        }
    }
    Err(OilError::BundleError(
        "No Oilfile found. Create a Oilfile.toml in your project root.".to_string(),
    ))
}

pub fn parse_waxfile(path: &Path) -> Result<Oilfile> {
    let content = std::fs::read_to_string(path)
        .map_err(|e| OilError::BundleError(format!("Cannot read {}: {}", path.display(), e)))?;
    let waxfile: Oilfile = toml::from_str(&content)?;
    Ok(waxfile)
}

#[instrument(skip(cache))]
pub async fn bundle(cache: &Cache, waxfile_path: Option<&str>, dry_run: bool) -> Result<()> {
    let start = std::time::Instant::now();

    let path = match waxfile_path {
        Some(p) => PathBuf::from(p),
        None => find_waxfile()?,
    };

    println!(
        "{} {}",
        style("wax bundle").bold(),
        style(path.display()).dim()
    );

    let waxfile = parse_waxfile(&path)?;

    let tap_count = waxfile.tap.len();
    let brew_count = waxfile.brew.len();
    let cask_count = waxfile.cask.len();
    let cargo_count = waxfile.cargo.len();
    let uv_count = waxfile.uv.len();
    let total = tap_count + brew_count + cask_count + cargo_count + uv_count;

    if total == 0 {
        println!("  {} Oilfile is empty", style("!").yellow());
        return Ok(());
    }

    println!(
        "  {} taps, {} formulae, {} casks, {} cargo, {} uv",
        style(tap_count).cyan(),
        style(brew_count).cyan(),
        style(cask_count).cyan(),
        style(cargo_count).cyan(),
        style(uv_count).cyan()
    );

    if dry_run {
        print_dry_run(&waxfile);
        return Ok(());
    }

    let mut success = 0usize;
    let mut failed = 0usize;
    let mut skipped = 0usize;

    for tap in &waxfile.tap {
        println!();
        println!("  {} tap {}", style("→").cyan(), style(tap).magenta());
        match add_tap(tap).await {
            Ok(true) => success += 1,
            Ok(false) => skipped += 1,
            Err(e) => {
                eprintln!(
                    "  {} tap {} failed: {}",
                    style("✗").red(),
                    style(tap).magenta(),
                    e
                );
                failed += 1;
            }
        }
    }

    if !waxfile.brew.is_empty() {
        let names: Vec<String> = waxfile.brew.iter().map(|e| e.name().to_string()).collect();
        println!();
        println!(
            "  {} installing {} formulae",
            style("→").cyan(),
            names.len()
        );
        match crate::commands::install::install(
            cache, &names, false, false, false, false, false, false, true,
        )
        .await
        {
            Ok(()) => success += names.len(),
            Err(e) => {
                eprintln!("  {} brew install failed: {}", style("✗").red(), e);
                failed += names.len();
            }
        }
    }

    if !waxfile.cask.is_empty() {
        let names: Vec<String> = waxfile.cask.iter().map(|e| e.name().to_string()).collect();
        println!();
        println!("  {} installing {} casks", style("→").cyan(), names.len());
        match crate::commands::install::install(
            cache, &names, false, true, false, false, false, false, true,
        )
        .await
        {
            Ok(()) => success += names.len(),
            Err(e) => {
                eprintln!("  {} cask install failed: {}", style("✗").red(), e);
                failed += names.len();
            }
        }
    }

    if !waxfile.cargo.is_empty() {
        println!();
        for entry in &waxfile.cargo {
            let name = entry.name();
            print!(
                "  {} cargo install {}",
                style("→").cyan(),
                style(name).magenta()
            );

            if is_cargo_installed(name).await {
                println!(" {}", style("(already installed)").dim());
                skipped += 1;
                continue;
            }
            println!();

            match cargo_install(entry).await {
                Ok(()) => {
                    println!("  {} cargo {}", style("✓").green(), style(name).magenta());
                    success += 1;
                }
                Err(e) => {
                    eprintln!(
                        "  {} cargo {} failed: {}",
                        style("✗").red(),
                        style(name).magenta(),
                        e
                    );
                    failed += 1;
                }
            }
        }
    }

    if !waxfile.uv.is_empty() {
        println!();
        for entry in &waxfile.uv {
            let name = entry.name();
            print!(
                "  {} uv tool install {}",
                style("→").cyan(),
                style(name).magenta()
            );

            if is_uv_tool_installed(name).await {
                println!(" {}", style("(already installed)").dim());
                skipped += 1;
                continue;
            }
            println!();

            match uv_tool_install(entry).await {
                Ok(()) => {
                    println!("  {} uv {}", style("✓").green(), style(name).magenta());
                    success += 1;
                }
                Err(e) => {
                    eprintln!(
                        "  {} uv {} failed: {}",
                        style("✗").red(),
                        style(name).magenta(),
                        e
                    );
                    failed += 1;
                }
            }
        }
    }

    let elapsed = start.elapsed();
    println!();
    if failed == 0 {
        println!(
            "{} installed, {} skipped{}",
            style(success).green(),
            style(skipped).dim(),
            crate::timing::elapsed_suffix(elapsed)
        );
    } else {
        println!(
            "{} installed, {} failed, {} skipped{}",
            style(success).green(),
            style(failed).red(),
            style(skipped).dim(),
            crate::timing::elapsed_suffix(elapsed)
        );
    }

    Ok(())
}

#[instrument(skip(_cache))]
pub async fn bundle_dump(_cache: &Cache) -> Result<()> {
    let state = crate::install::InstallState::new()?;
    let installed = state.load().await?;
    let cask_state = crate::cask::CaskState::new()?;
    let installed_casks = cask_state.load().await?;

    let mut waxfile = String::new();

    if !installed.is_empty() {
        waxfile.push_str("brew = [\n");
        let mut names: Vec<_> = installed.keys().collect();
        names.sort();
        for name in names {
            waxfile.push_str(&format!("  \"{}\",\n", name));
        }
        waxfile.push_str("]\n\n");
    }

    if !installed_casks.is_empty() {
        waxfile.push_str("cask = [\n");
        let mut names: Vec<_> = installed_casks.keys().collect();
        names.sort();
        for name in names {
            waxfile.push_str(&format!("  \"{}\",\n", name));
        }
        waxfile.push_str("]\n");
    }

    print!("{}", waxfile);
    Ok(())
}

fn print_dry_run(waxfile: &Oilfile) {
    println!();
    for tap in &waxfile.tap {
        println!("  tap {}", style(tap).magenta());
    }
    for entry in &waxfile.brew {
        println!("  brew {}", style(entry.name()).magenta());
    }
    for entry in &waxfile.cask {
        println!(
            "  cask {} {}",
            style(entry.name()).magenta(),
            style("(cask)").yellow()
        );
    }
    for entry in &waxfile.cargo {
        println!("  cargo {}", style(entry.name()).magenta());
    }
    for entry in &waxfile.uv {
        println!("  uv {}", style(entry.name()).magenta());
    }
    println!("\n{}", style("dry run - no changes made").dim());
}

async fn add_tap(tap: &str) -> Result<bool> {
    let mut tap_manager = crate::tap::TapManager::new()?;
    tap_manager.load().await?;
    if tap_manager.has_tap(tap).await {
        return Ok(false);
    }

    let tap_parts: Vec<&str> = tap.split('/').collect();
    if tap_parts.len() < 2 {
        return Err(OilError::BundleError(format!(
            "Invalid tap format: {}",
            tap
        )));
    }

    tap_manager.add_tap(tap).await?;
    Ok(true)
}

async fn is_cargo_installed(name: &str) -> bool {
    let output = Command::new("cargo")
        .args(["install", "--list"])
        .output()
        .await;

    match output {
        Ok(out) => {
            let stdout = String::from_utf8_lossy(&out.stdout);
            stdout
                .lines()
                .any(|line| !line.starts_with(' ') && line.starts_with(name))
        }
        Err(_) => false,
    }
}

async fn cargo_install(entry: &BundleEntry) -> Result<()> {
    let mut cmd = Command::new("cargo");
    cmd.arg("install");

    let name = entry.name();
    cmd.arg(name);

    if let Some(version) = entry.version() {
        cmd.args(["--version", version]);
    }

    if let Some(args) = entry.args() {
        cmd.args(args);
    }

    let status = cmd
        .status()
        .await
        .map_err(|e| OilError::BundleError(format!("cargo not found: {}", e)))?;

    if !status.success() {
        return Err(OilError::BundleError(format!(
            "cargo install {} failed with exit code {}",
            name,
            status.code().unwrap_or(-1)
        )));
    }

    Ok(())
}

async fn is_uv_tool_installed(name: &str) -> bool {
    let output = Command::new("uv").args(["tool", "list"]).output().await;

    match output {
        Ok(out) => {
            let stdout = String::from_utf8_lossy(&out.stdout);
            stdout.lines().any(|line| line.starts_with(name))
        }
        Err(_) => false,
    }
}

async fn uv_tool_install(entry: &BundleEntry) -> Result<()> {
    let mut cmd = Command::new("uv");
    cmd.args(["tool", "install"]);

    let name = entry.name();

    if let Some(version) = entry.version() {
        cmd.arg(format!("{}=={}", name, version));
    } else {
        cmd.arg(name);
    }

    if let Some(args) = entry.args() {
        cmd.args(args);
    }

    let status = cmd
        .status()
        .await
        .map_err(|e| OilError::BundleError(format!("uv not found: {}", e)))?;

    if !status.success() {
        return Err(OilError::BundleError(format!(
            "uv tool install {} failed with exit code {}",
            name,
            status.code().unwrap_or(-1)
        )));
    }

    Ok(())
}
