use anyhow::{bail, Context, Result};

use crate::config::{Config, EntityKind};
use crate::registry::{packages, tools, InstallMethod, Package, ToolDef};
use crate::state::{MachineInstall, MachineState};
use crate::ui::style;

use super::exec;
use super::runner::{self, Job, Outcome};

pub fn run(name: Option<String>) -> Result<()> {
    let mut machine = MachineState::load()?;
    let mut cfg = Config::load()?;
    let candidates = collect_candidates(&machine, name.as_deref())?;

    if candidates.is_empty() {
        println!("nothing to update");
        return Ok(());
    }

    let mut jobs: Vec<Job> = Vec::new();
    let mut plans: Vec<Plan> = Vec::new();
    for cand in candidates {
        let spec = match update_spec_for(&cand) {
            Ok(s) => s,
            Err(e) => {
                eprintln!("{} skipping `{}`: {e}", style::warn_mark(), cand.name());
                continue;
            }
        };
        let entity_kind = match cand.kind() {
            Kind::Tool => EntityKind::Tool,
            Kind::Package => EntityKind::Package,
        };
        if !exec::trust_gate(&mut cfg, entity_kind, cand.name(), &spec.command)? {
            eprintln!(
                "{} skipping `{}` (declined)",
                style::warn_mark(),
                cand.name()
            );
            continue;
        }
        jobs.push(Job {
            name: cand.name().to_string(),
            command: spec.command,
            interactive: spec.interactive,
        });
        plans.push(Plan {
            name: cand.name().to_string(),
            kind: cand.kind(),
            method: cand.recorded_method().to_string(),
        });
    }
    cfg.save()?;
    machine.save()?;

    if jobs.is_empty() {
        return Ok(());
    }

    let outcomes = runner::run_batch(jobs);
    apply_outcomes(&mut machine, &plans, &outcomes);
    machine.save()?;
    print_summary(&outcomes);

    if runner::is_cancelled() {
        eprintln!("{} cancelled by user", style::cancel_mark());
        std::process::exit(130);
    }
    if outcomes.iter().any(|o| !o.success) {
        bail!("one or more updates failed");
    }
    Ok(())
}

#[derive(Copy, Clone, Debug, Eq, PartialEq)]
enum Kind {
    Tool,
    Package,
}

enum Candidate {
    Tool(ToolDef, MachineInstall),
    Package(Package, MachineInstall),
}

impl Candidate {
    fn name(&self) -> &str {
        match self {
            Candidate::Tool(t, _) => &t.name,
            Candidate::Package(p, _) => &p.name,
        }
    }

    fn kind(&self) -> Kind {
        match self {
            Candidate::Tool(_, _) => Kind::Tool,
            Candidate::Package(_, _) => Kind::Package,
        }
    }

    fn recorded(&self) -> &MachineInstall {
        match self {
            Candidate::Tool(_, m) => m,
            Candidate::Package(_, m) => m,
        }
    }

    fn recorded_method(&self) -> &str {
        &self.recorded().method
    }

    fn update_str(&self) -> Option<&str> {
        match self {
            Candidate::Tool(t, _) => t.update.as_deref(),
            Candidate::Package(p, _) => p.update.as_deref(),
        }
    }

    fn install_methods(&self) -> &[InstallMethod] {
        match self {
            Candidate::Tool(t, _) => &t.install,
            Candidate::Package(p, _) => &p.install,
        }
    }
}

struct Plan {
    name: String,
    kind: Kind,
    method: String,
}

#[derive(Debug)]
struct UpdateSpec {
    command: String,
    interactive: bool,
}

fn collect_candidates(machine: &MachineState, name: Option<&str>) -> Result<Vec<Candidate>> {
    if let Some(n) = name {
        if let Some(mp) = machine.tools.get(n).cloned() {
            let tool = tools::resolve(n)?
                .map(|r| r.tool)
                .with_context(|| format!("tool `{n}` is not in the registry"))?;
            return Ok(vec![Candidate::Tool(tool, mp)]);
        }
        if let Some(mp) = machine.packages.get(n).cloned() {
            let pkg = packages::resolve(n)?
                .map(|r| r.package)
                .with_context(|| format!("package `{n}` is not in the registry"))?;
            return Ok(vec![Candidate::Package(pkg, mp)]);
        }
        bail!("`{n}` is not installed on this machine");
    }

    let mut out = Vec::new();
    for (name, mp) in machine.tools.iter().map(|(k, v)| (k.clone(), v.clone())) {
        match tools::resolve(&name)? {
            Some(r) => out.push(Candidate::Tool(r.tool, mp)),
            None => eprintln!(
                "{} tool `{name}` no longer in the registry; skipping",
                style::warn_mark()
            ),
        }
    }
    for (name, mp) in machine.packages.iter().map(|(k, v)| (k.clone(), v.clone())) {
        match packages::resolve(&name)? {
            Some(r) => out.push(Candidate::Package(r.package, mp)),
            None => eprintln!(
                "{} package `{name}` no longer in the registry; skipping",
                style::warn_mark()
            ),
        }
    }
    Ok(out)
}

/// Decide what update command to run and whether it expects a TTY.
/// Explicit `update` strings are assumed non-interactive — packages that need
/// interactive updates should omit `update` and let the recorded install
/// method carry its own `interactive` flag.
fn update_spec_for(cand: &Candidate) -> Result<UpdateSpec> {
    if let Some(cmd) = cand.update_str() {
        return Ok(UpdateSpec {
            command: cmd.to_string(),
            interactive: false,
        });
    }
    let recorded_method = cand.recorded_method();
    if recorded_method == "preexisting" {
        bail!("was preexisting on install — no update command available");
    }
    let methods = cand.install_methods();
    if methods.is_empty() {
        bail!("no `update` or `[[install]]` command declared");
    }
    let method = methods
        .iter()
        .find(|m| m.id == recorded_method)
        .with_context(|| format!("no install method matching recorded id `{recorded_method}`"))?;
    Ok(UpdateSpec {
        command: method.command.clone(),
        interactive: method.interactive,
    })
}

fn apply_outcomes(machine: &mut MachineState, plans: &[Plan], outcomes: &[Outcome]) {
    for (plan, outcome) in plans.iter().zip(outcomes.iter()) {
        if !outcome.success {
            continue;
        }
        match plan.kind {
            Kind::Tool => {
                let version = machine
                    .tools
                    .get(&plan.name)
                    .and_then(|p| p.version.clone());
                machine.record_tool(&plan.name, &plan.method, version);
            }
            Kind::Package => {
                let version = machine
                    .packages
                    .get(&plan.name)
                    .and_then(|p| p.version.clone());
                machine.record_package(&plan.name, &plan.method, version);
            }
        }
    }
}

fn print_summary(outcomes: &[Outcome]) {
    let ok = outcomes.iter().filter(|o| o.success).count();
    let cancelled = outcomes.iter().filter(|o| o.cancelled).count();
    let fail = outcomes.len() - ok - cancelled;
    println!();
    if fail == 0 && cancelled == 0 {
        println!(
            "{} updated {ok} item{}",
            style::ok_mark(),
            if ok == 1 { "" } else { "s" }
        );
        return;
    }
    let mark = if fail > 0 {
        style::fail_mark()
    } else {
        style::cancel_mark()
    };
    let mut line = format!("{mark} updated {ok} / {}", outcomes.len());
    if fail > 0 {
        line.push_str(&format!(" — {fail} failed"));
    }
    if cancelled > 0 {
        line.push_str(&format!(" — {cancelled} cancelled"));
    }
    println!("{line}");
    runner::print_failure_tails(outcomes);
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::registry::packages::InstructionsBlock;
    use crate::registry::Scope;
    use crate::tools::Tool;

    fn package_with(name: &str, update: Option<&str>, methods: Vec<(&str, &str)>) -> Package {
        Package {
            name: name.into(),
            description: None,
            homepage: None,
            tools: vec![Tool::Claude],
            scope: Scope::Both,
            depends_on: Vec::new(),
            artifacts: Vec::new(),
            verify: None,
            prereq: Vec::new(),
            install: methods
                .into_iter()
                .map(|(id, cmd)| InstallMethod {
                    id: id.into(),
                    label: None,
                    command: cmd.into(),
                    requires: Vec::new(),
                    interactive: false,
                })
                .collect(),
            update: update.map(str::to_string),
            uninstall: None,
            instructions: Vec::<InstructionsBlock>::new(),
            permissions: Vec::new(),
        }
    }

    fn machine_install(method: &str) -> MachineInstall {
        MachineInstall {
            installed_at: "t0".into(),
            method: method.into(),
            version: None,
            permissions: Vec::new(),
        }
    }

    fn pkg_candidate(p: Package, method: &str) -> Candidate {
        Candidate::Package(p, machine_install(method))
    }

    #[test]
    fn update_spec_prefers_explicit_update() {
        let p = package_with("x", Some("echo update"), vec![("curl", "echo install")]);
        let spec = update_spec_for(&pkg_candidate(p, "curl")).unwrap();
        assert_eq!(spec.command, "echo update");
        assert!(!spec.interactive);
    }

    #[test]
    fn update_spec_falls_back_to_recorded_install_method() {
        let p = package_with(
            "x",
            None,
            vec![("curl", "echo curl"), ("go", "echo go install")],
        );
        let spec = update_spec_for(&pkg_candidate(p, "go")).unwrap();
        assert_eq!(spec.command, "echo go install");
    }

    #[test]
    fn update_spec_inherits_interactive_from_fallback_method() {
        let mut p = package_with("x", None, vec![("npx", "echo wizard")]);
        p.install[0].interactive = true;
        let spec = update_spec_for(&pkg_candidate(p, "npx")).unwrap();
        assert!(spec.interactive);
    }

    #[test]
    fn update_spec_rejects_preexisting() {
        let p = package_with("x", None, vec![("curl", "echo install")]);
        let err = update_spec_for(&pkg_candidate(p, "preexisting")).unwrap_err();
        assert!(err.to_string().contains("preexisting"));
    }

    #[test]
    fn update_spec_rejects_unknown_method() {
        let p = package_with("x", None, vec![("curl", "echo install")]);
        let err = update_spec_for(&pkg_candidate(p, "go")).unwrap_err();
        assert!(err.to_string().contains("no install method"));
    }

    #[test]
    fn apply_outcomes_updates_machine_package_timestamp() {
        let mut m = MachineState::default();
        m.record_package("x", "curl", None);
        let before = m.packages["x"].installed_at.clone();
        let plans = vec![Plan {
            name: "x".into(),
            kind: Kind::Package,
            method: "curl".into(),
        }];
        let outcomes = vec![Outcome {
            name: "x".into(),
            success: true,
            tail: String::new(),
            error: None,
            cancelled: false,
        }];
        std::thread::sleep(std::time::Duration::from_millis(5));
        apply_outcomes(&mut m, &plans, &outcomes);
        assert_ne!(m.packages["x"].installed_at, before);
        assert_eq!(m.packages["x"].method, "curl");
    }

    #[test]
    fn apply_outcomes_updates_tool_timestamp() {
        let mut m = MachineState::default();
        m.record_tool("claude", "script", None);
        let before = m.tools["claude"].installed_at.clone();
        let plans = vec![Plan {
            name: "claude".into(),
            kind: Kind::Tool,
            method: "script".into(),
        }];
        let outcomes = vec![Outcome {
            name: "claude".into(),
            success: true,
            tail: String::new(),
            error: None,
            cancelled: false,
        }];
        std::thread::sleep(std::time::Duration::from_millis(5));
        apply_outcomes(&mut m, &plans, &outcomes);
        assert_ne!(m.tools["claude"].installed_at, before);
    }

    #[test]
    fn apply_outcomes_skips_failed_jobs() {
        let mut m = MachineState::default();
        m.record_package("x", "curl", None);
        let before = m.packages["x"].installed_at.clone();
        let plans = vec![Plan {
            name: "x".into(),
            kind: Kind::Package,
            method: "curl".into(),
        }];
        let outcomes = vec![Outcome {
            name: "x".into(),
            success: false,
            tail: String::new(),
            error: Some("exit 1".into()),
            cancelled: false,
        }];
        apply_outcomes(&mut m, &plans, &outcomes);
        assert_eq!(m.packages["x"].installed_at, before);
    }
}
