use std::collections::BTreeMap;
use std::fmt;
use std::io::Write;
use std::process::{Command, Stdio};

use anyhow::{bail, Context, Result};
use inquire::{Confirm, Select};

use crate::config::{Config, EntityKind};
use crate::registry::{Artifact, InstallMethod, Package, Prereq, ToolDef};
use crate::state::MachineState;
use crate::ui::style;

/// Common shape of an installable (Package or ToolDef) — lets the install
/// machinery work on either without code duplication.
pub trait Installable {
    fn name(&self) -> &str;
    fn kind(&self) -> EntityKind;
    fn artifacts(&self) -> &[Artifact];
    fn verify(&self) -> Option<&str>;
    fn prereq(&self) -> &[Prereq];
    fn install_methods(&self) -> &[InstallMethod];
}

impl Installable for Package {
    fn name(&self) -> &str {
        &self.name
    }
    fn kind(&self) -> EntityKind {
        EntityKind::Package
    }
    fn artifacts(&self) -> &[Artifact] {
        &self.artifacts
    }
    fn verify(&self) -> Option<&str> {
        self.verify.as_deref()
    }
    fn prereq(&self) -> &[Prereq] {
        &self.prereq
    }
    fn install_methods(&self) -> &[InstallMethod] {
        &self.install
    }
}

impl Installable for ToolDef {
    fn name(&self) -> &str {
        &self.name
    }
    fn kind(&self) -> EntityKind {
        EntityKind::Tool
    }
    fn artifacts(&self) -> &[Artifact] {
        &self.artifacts
    }
    fn verify(&self) -> Option<&str> {
        self.verify.as_deref()
    }
    fn prereq(&self) -> &[Prereq] {
        &self.prereq
    }
    fn install_methods(&self) -> &[InstallMethod] {
        &self.install
    }
}

/// Run `command` through `$SHELL`-equivalent (`AIMGR_TEST_SHELL` in tests, `sh` otherwise).
/// Inherits stdio so the user sees installer output live. Fails on non-zero exit.
pub fn run_shell(command: &str) -> Result<()> {
    log_command(command);
    let shell = shell_binary();
    let status = Command::new(&shell)
        .arg("-c")
        .arg(command)
        .status()
        .with_context(|| format!("spawning shell `{shell}`"))?;
    if !status.success() {
        bail!(
            "command failed (exit {}): {command}",
            status.code().unwrap_or(-1)
        );
    }
    Ok(())
}

/// Quiet success/fail check — used for `verify` and prereq detection.
pub fn verify_shell(command: &str) -> bool {
    log_command(command);
    let shell = shell_binary();
    Command::new(&shell)
        .arg("-c")
        .arg(command)
        .stdout(Stdio::null())
        .stderr(Stdio::null())
        .status()
        .map(|s| s.success())
        .unwrap_or(false)
}

pub fn shell_binary() -> String {
    std::env::var("AIMGR_TEST_SHELL").unwrap_or_else(|_| "sh".into())
}

pub fn log_command(command: &str) {
    if let Ok(path) = std::env::var("AIMGR_TEST_LOG") {
        if let Ok(mut f) = std::fs::OpenOptions::new()
            .create(true)
            .append(true)
            .open(path)
        {
            writeln!(f, "{command}").ok();
        }
    }
}

/// Confirms an un-trusted command; caches approvals in `Config` under the
/// appropriate map for `kind`. Returns `Ok(false)` if the user declined.
/// With `-y` (ops::yes::enabled()), auto-accepts and caches the trust.
pub fn trust_gate(cfg: &mut Config, kind: EntityKind, name: &str, command: &str) -> Result<bool> {
    if cfg.is_trusted(name, command) {
        return Ok(true);
    }
    println!("{} aimgr wants to run for `{name}`:", style::warn_mark());
    println!("  {} {command}", style::dim("$"));
    if crate::ops::yes::enabled() {
        println!("  {} auto-approved by -y", style::ok_mark());
        cfg.trust(kind, name, command);
        return Ok(true);
    }
    let ok = Confirm::new("Proceed?")
        .with_default(false)
        .with_help_message("stored as trusted in ~/.config/aimgr/config.toml")
        .prompt()?;
    if ok {
        cfg.trust(kind, name, command);
    }
    Ok(ok)
}

/// (toml_key, binary_on_path, install_cmd_template_with_{pkg}) — ordered by
/// preference; AUR helpers beat raw pacman, apt-get beats apt for scripting.
const PKG_MANAGERS: &[(&str, &str, &str)] = &[
    ("paru", "paru", "paru -S --needed --noconfirm {pkg}"),
    ("yay", "yay", "yay -S --needed --noconfirm {pkg}"),
    (
        "pacman",
        "pacman",
        "sudo pacman -S --needed --noconfirm {pkg}",
    ),
    ("apt", "apt-get", "sudo apt-get install -y {pkg}"),
    ("dnf", "dnf", "sudo dnf install -y {pkg}"),
    ("zypper", "zypper", "sudo zypper install -y {pkg}"),
    ("brew", "brew", "brew install {pkg}"),
];

fn detect_pkg_manager(install: &BTreeMap<String, String>) -> Option<(&'static str, String)> {
    for (key, bin, tmpl) in PKG_MANAGERS {
        if let Some(pkg) = install.get(*key) {
            if verify_shell(&format!("command -v {bin}")) {
                return Some((key, tmpl.replace("{pkg}", pkg)));
            }
        }
    }
    None
}

/// Check each prereq; if missing, install via the first matching package
/// manager on PATH after a trust-gate prompt. Prints a manual hint and
/// continues when no pkg manager matches; bails if the user declines.
pub fn ensure_prereqs(
    parent_kind: EntityKind,
    parent_name: &str,
    prereqs: &[Prereq],
    cfg: &mut Config,
) -> Result<()> {
    for p in prereqs {
        let satisfied = p.check.as_deref().is_some_and(verify_shell);
        if satisfied {
            println!("  {} prereq `{}` already present", style::ok_mark(), p.name);
            continue;
        }
        if p.install.is_empty() {
            println!(
                "  {} prereq `{}` not detected and no install hint provided",
                style::warn_mark(),
                p.name
            );
            continue;
        }
        let Some((mgr, cmd)) = detect_pkg_manager(&p.install) else {
            println!(
                "  {} prereq `{}` not detected — no matching package manager on PATH. Install manually:",
                style::warn_mark(),
                p.name
            );
            for (pkg, val) in &p.install {
                println!("      {pkg}: {val}");
            }
            continue;
        };
        println!(
            "  {} prereq `{}` not detected — installing via {mgr}",
            style::warn_mark(),
            p.name
        );
        if !trust_gate(cfg, parent_kind, parent_name, &cmd)? {
            bail!("user declined prereq install for `{}`", p.name);
        }
        run_shell(&cmd)?;
        if let Some(check) = p.check.as_deref() {
            if !verify_shell(check) {
                eprintln!(
                    "{} prereq `{}` still not detected after install — continuing anyway",
                    style::warn_mark(),
                    p.name
                );
            }
        }
    }
    Ok(())
}

/// Pick the install method by id, or prompt if none given and >1 available.
pub fn pick_install_method<'a, I: Installable>(
    item: &'a I,
    preferred: Option<&str>,
) -> Result<&'a InstallMethod> {
    let methods = item.install_methods();
    if methods.is_empty() {
        bail!("`{}` has no [[install]] method", item.name());
    }
    if let Some(id) = preferred {
        return methods
            .iter()
            .find(|m| m.id == id)
            .with_context(|| format!("`{}` has no install method with id `{id}`", item.name()));
    }
    if methods.len() == 1 {
        return Ok(&methods[0]);
    }
    let options: Vec<InstallChoice<'a>> = methods.iter().map(InstallChoice).collect();
    let chosen = Select::new("Pick an install method:", options).prompt()?;
    Ok(chosen.0)
}

#[derive(Clone)]
struct InstallChoice<'a>(&'a InstallMethod);

impl fmt::Display for InstallChoice<'_> {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match &self.0.label {
            Some(l) => write!(f, "{} — {l}", self.0.id),
            None => f.write_str(&self.0.id),
        }
    }
}

/// Summary of each artifact's presence on disk (or PATH). Empty when no artifacts declared.
pub fn artifact_report<I: Installable>(item: &I) -> Vec<(Artifact, bool)> {
    item.artifacts()
        .iter()
        .cloned()
        .map(|a| {
            let present = a.present();
            (a, present)
        })
        .collect()
}

/// True iff all declared artifacts are present.
pub fn all_artifacts_present<I: Installable>(item: &I) -> bool {
    !item.artifacts().is_empty() && item.artifacts().iter().all(|a| a.present())
}

/// Probe presence without running anything. Artifacts win over `verify` when
/// declared; falls back to `verify_shell` otherwise. Returns `None` when
/// neither is configured.
pub fn probe_present<I: Installable>(item: &I) -> Option<bool> {
    if !item.artifacts().is_empty() {
        return Some(item.artifacts().iter().all(|a| a.present()));
    }
    item.verify().map(verify_shell)
}

fn warn_missing_artifacts<I: Installable>(item: &I) {
    let report = artifact_report(item);
    let missing: Vec<_> = report.iter().filter(|(_, ok)| !ok).collect();
    if missing.is_empty() {
        return;
    }
    eprintln!(
        "{} `{}` installed but {} artifact(s) missing — recording anyway:",
        style::warn_mark(),
        item.name(),
        missing.len()
    );
    for (art, _) in &report {
        let mark = if art.present() {
            style::ok_mark().to_string()
        } else {
            style::fail_mark().to_string()
        };
        eprintln!("  {mark} {art}");
    }
}

/// Outcome of running the install pipeline for an item — returned to callers
/// who decide which `MachineState` map to record into.
pub struct EnsureOutcome {
    pub method: String,
}

/// What `plan_install` decided. `AlreadyPresent` short-circuits with a
/// `"preexisting"` record; `Install` hands back the approved method so callers
/// can execute it serially or as part of a parallel batch.
pub enum PlannedAction {
    AlreadyPresent,
    Install(InstallMethod),
}

/// Prep phase of the install pipeline: probe → prereq ensure → trust gate →
/// method pick. Does NOT run the shell command. Callers execute the returned
/// method themselves (inline via [`run_shell`] or batched via `ops::runner`).
/// When `force` is true, the presence probe is skipped so the installer re-runs
/// even if artifacts are already on disk.
pub fn plan_install<I: Installable>(
    item: &I,
    cfg: &mut Config,
    preferred_method: Option<&str>,
    force: bool,
) -> Result<PlannedAction> {
    if !force && probe_present(item) == Some(true) {
        return Ok(PlannedAction::AlreadyPresent);
    }
    let method = pick_install_method(item, preferred_method)?.clone();
    if !item.prereq().is_empty() {
        println!("{} for `{}`:", style::bold("Prerequisites"), item.name());
        ensure_prereqs(item.kind(), item.name(), item.prereq(), cfg)?;
    }
    if !trust_gate(cfg, item.kind(), item.name(), &method.command)? {
        bail!("user declined install for `{}`", item.name());
    }
    Ok(PlannedAction::Install(method))
}

/// Post-install probe: warn (but don't fail) when declared artifacts are
/// missing or the fallback `verify` shell check fails.
pub fn post_install_verify<I: Installable>(item: &I) {
    if !item.artifacts().is_empty() {
        if !all_artifacts_present(item) {
            warn_missing_artifacts(item);
        }
    } else if let Some(verify) = item.verify() {
        if !verify_shell(verify) {
            eprintln!(
                "{} `{}` installed but verify failed ({verify}) — recording anyway. \
                 If the tool works, consider declaring `[[artifacts]]` instead (or updating \
                 `verify` to something reachable from a non-interactive shell — alias/rc \
                 files aren't loaded).",
                style::warn_mark(),
                item.name()
            );
        }
    }
}

/// Runs the install pipeline for any `Installable` (probe → trust gate →
/// shell command → verify). Does NOT touch `MachineState`; callers wrap this
/// and record into either the `tools` or `packages` map themselves.
pub fn ensure_installed<I: Installable>(
    item: &I,
    cfg: &mut Config,
    preferred_method: Option<&str>,
    force: bool,
) -> Result<EnsureOutcome> {
    let method = match plan_install(item, cfg, preferred_method, force)? {
        PlannedAction::AlreadyPresent => {
            return Ok(EnsureOutcome {
                method: "preexisting".into(),
            });
        }
        PlannedAction::Install(m) => m,
    };
    run_shell(&method.command)?;
    post_install_verify(item);
    Ok(EnsureOutcome { method: method.id })
}

/// Convenience: install a package and record it in the machine state. When
/// `force` is true, both the machine-state short-circuit and the presence
/// probe are skipped so the installer always runs.
pub fn ensure_package_installed(
    package: &Package,
    machine: &mut MachineState,
    cfg: &mut Config,
    preferred_method: Option<&str>,
    force: bool,
) -> Result<()> {
    if !force && machine.packages.contains_key(&package.name) {
        return Ok(());
    }
    let outcome = ensure_installed(package, cfg, preferred_method, force)?;
    machine.record_package(&package.name, &outcome.method, None);
    Ok(())
}

/// Convenience: install a tool and record it in the machine state. When
/// `force` is true, both the machine-state short-circuit and the presence
/// probe are skipped so the installer always runs.
pub fn ensure_tool_installed(
    tool: &ToolDef,
    machine: &mut MachineState,
    cfg: &mut Config,
    preferred_method: Option<&str>,
    force: bool,
) -> Result<()> {
    if !force && machine.tools.contains_key(&tool.name) {
        return Ok(());
    }
    let outcome = ensure_installed(tool, cfg, preferred_method, force)?;
    machine.record_tool(&tool.name, &outcome.method, None);
    Ok(())
}
