use std::collections::BTreeSet;
use std::fs;
use std::path::{Path, PathBuf};

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

use crate::config::Config;
use crate::project;
use crate::registry::artifact::Probe;
use crate::registry::{packages, tools, Artifact};
use crate::state::{MachineState, ProjectIntent, ProjectState};
use crate::ui::style;

use super::{exec, permissions as perms, sync};

pub fn run(name: &str, yes: bool) -> Result<()> {
    if tools::resolve(name)?.is_some() {
        bail!("`{name}` is a tool, not a package — use `aimgr uninstall {name}`");
    }

    let mut machine = MachineState::load()?;
    let resolved = packages::resolve(name)?;

    // Reconcile every registered project *plus* the one we're standing in.
    // The machine registry can drift (e.g. state rebuilt), so purge must not
    // rely on it alone — otherwise blocks linger in unregistered projects.
    let mut roots: BTreeSet<PathBuf> = machine.projects.iter().cloned().collect();
    if let Ok(cwd) = std::env::current_dir() {
        if let Some(root) = project::discover_root(&cwd) {
            roots.insert(root);
        }
    }

    for root in &roots {
        let mut intent = ProjectIntent::load(root)?;
        let before = intent.packages.len();
        intent.packages.retain(|p| p != name);
        let intent_changed = intent.packages.len() != before;
        if intent_changed {
            intent.save(root)?;
        }
        // Reconcile when the intent changed, or when the project's recorded
        // state still references the package (a stale block to unwire).
        let state_referenced = ProjectState::load(root)
            .map(|s| s.packages.contains_key(name))
            .unwrap_or(false);
        if intent_changed || state_referenced {
            sync::reconcile_project(root, false)?;
        }
    }

    let mut cfg = Config::load()?;
    if cfg.remove_package(name) {
        cfg.save()?;
    }

    if let Some(r) = &resolved {
        if let Some(cmd) = &r.package.uninstall {
            let ok = yes
                || Confirm::new(&format!("Run uninstaller for `{name}`?\n  $ {cmd}"))
                    .with_default(false)
                    .prompt()?;
            if ok {
                if let Err(e) = exec::run_shell(cmd) {
                    eprintln!("{} uninstall failed (continuing): {e}", style::warn_mark());
                }
            }
        }
        remove_local_artifacts(&r.package.artifacts);
    }

    if let Some(rec) = machine.packages.get(name) {
        if !rec.permissions.is_empty() {
            if let Err(e) = perms::unapply(&rec.permissions) {
                eprintln!(
                    "{} unapplying permissions for `{name}`: {e}",
                    style::warn_mark()
                );
            }
        }
    }
    machine.forget_package(name);
    machine.save()?;
    println!("{} purged `{name}`", style::ok_mark());
    Ok(())
}

/// Remove file-backed artifacts (skills, rules, declared paths) that the
/// package's own uninstaller didn't clear. `cli` artifacts are skipped — those
/// live on $PATH and need a real package-manager uninstall.
fn remove_local_artifacts(artifacts: &[Artifact]) {
    for art in artifacts {
        if matches!(art, Artifact::Cli { .. }) {
            continue;
        }
        let Probe::Path(path) = art.probe() else {
            continue;
        };
        if !path.exists() {
            continue;
        }
        match remove_path(&path) {
            Ok(()) => println!(
                "  {} removed {} ({})",
                style::ok_mark(),
                art.label(),
                path.display()
            ),
            Err(e) => eprintln!(
                "  {} could not remove {} ({}): {e}",
                style::warn_mark(),
                art.label(),
                path.display()
            ),
        }
    }
}

fn remove_path(path: &Path) -> std::io::Result<()> {
    let meta = fs::symlink_metadata(path)?;
    if meta.is_dir() {
        fs::remove_dir_all(path)
    } else {
        fs::remove_file(path)
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use tempfile::tempdir;

    #[test]
    fn removes_skill_dir_rule_file_and_path_keeps_cli() {
        let tmp = tempdir().unwrap();
        let skill_dir = tmp.path().join("skill-pkg");
        let rule_file = tmp.path().join("rule.md");
        let extra_file = tmp.path().join("extra/data.txt");
        fs::create_dir_all(skill_dir.join("inner")).unwrap();
        fs::write(skill_dir.join("inner/SKILL.md"), "x").unwrap();
        fs::write(&rule_file, "y").unwrap();
        fs::create_dir_all(extra_file.parent().unwrap()).unwrap();
        fs::write(&extra_file, "z").unwrap();

        let artifacts = vec![
            Artifact::Path {
                path: skill_dir.to_string_lossy().into_owned(),
            },
            Artifact::Path {
                path: rule_file.to_string_lossy().into_owned(),
            },
            Artifact::Path {
                path: extra_file.to_string_lossy().into_owned(),
            },
            Artifact::Cli {
                name: "definitely-not-real-bin-xyz".into(),
            },
        ];
        remove_local_artifacts(&artifacts);

        assert!(
            !skill_dir.exists(),
            "skill dir should be removed recursively"
        );
        assert!(!rule_file.exists(), "rule file should be removed");
        assert!(!extra_file.exists(), "declared path should be removed");
    }

    #[test]
    fn missing_artifact_is_silently_skipped() {
        let tmp = tempdir().unwrap();
        let ghost = tmp.path().join("never-existed");
        let artifacts = vec![Artifact::Path {
            path: ghost.to_string_lossy().into_owned(),
        }];
        // Should not panic / error.
        remove_local_artifacts(&artifacts);
        assert!(!ghost.exists());
    }
}
