use std::fmt;
use std::path::PathBuf;

use directories::BaseDirs;
use serde::{Deserialize, Serialize};

/// A concrete thing a package puts on the machine. Used in place of the
/// free-form `verify` shell probe when present: presence is a simple
/// filesystem-or-PATH check, and each artifact is reported individually
/// when something's missing.
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
#[serde(tag = "kind", rename_all = "kebab-case", deny_unknown_fields)]
pub enum Artifact {
    /// Binary available on `$PATH`.
    Cli { name: String },
    /// Claude Code skill at `~/.claude/skills/<name>`.
    ClaudeSkill { name: String },
    /// Claude Code rule at `~/.claude/rules/<name>.md`.
    ClaudeRule { name: String },
    /// Arbitrary file or directory. `~/` and `$HOME/` prefixes expand.
    Path { path: String },
}

/// How to evaluate an artifact's presence — either look up a binary on PATH
/// or check whether a filesystem path exists.
pub enum Probe {
    Cli(String),
    Path(PathBuf),
}

impl Artifact {
    pub fn probe(&self) -> Probe {
        match self {
            Artifact::Cli { name } => Probe::Cli(name.clone()),
            Artifact::ClaudeSkill { name } => {
                Probe::Path(home_join(&format!(".claude/skills/{name}")))
            }
            Artifact::ClaudeRule { name } => {
                Probe::Path(home_join(&format!(".claude/rules/{name}.md")))
            }
            Artifact::Path { path } => Probe::Path(expand(path)),
        }
    }

    /// True iff the artifact is present on this machine.
    pub fn present(&self) -> bool {
        match self.probe() {
            Probe::Cli(name) => cli_on_path(&name),
            Probe::Path(p) => p.exists(),
        }
    }

    /// Short label for UI output (e.g. "skill find-docs").
    pub fn label(&self) -> String {
        match self {
            Artifact::Cli { name } => format!("cli {name}"),
            Artifact::ClaudeSkill { name } => format!("skill {name}"),
            Artifact::ClaudeRule { name } => format!("rule {name}"),
            Artifact::Path { path } => format!("path {path}"),
        }
    }

    /// Human-readable resolved target (`$PATH` lookup or concrete path).
    pub fn resolved(&self) -> String {
        match self.probe() {
            Probe::Cli(name) => format!("$PATH/{name}"),
            Probe::Path(p) => p.display().to_string(),
        }
    }
}

impl fmt::Display for Artifact {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{} → {}", self.label(), self.resolved())
    }
}

fn home_join(rel: &str) -> PathBuf {
    match BaseDirs::new() {
        Some(b) => b.home_dir().join(rel),
        None => PathBuf::from(rel),
    }
}

fn expand(path: &str) -> PathBuf {
    for prefix in ["~/", "$HOME/"] {
        if let Some(rest) = path.strip_prefix(prefix) {
            return home_join(rest);
        }
    }
    PathBuf::from(path)
}

fn cli_on_path(name: &str) -> bool {
    let Ok(paths) = std::env::var("PATH") else {
        return false;
    };
    for dir in std::env::split_paths(&paths) {
        let candidate = dir.join(name);
        if candidate.is_file() {
            return true;
        }
    }
    false
}

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

    #[test]
    fn cli_kind_probes_path() {
        let a = Artifact::Cli { name: "sh".into() };
        assert!(matches!(a.probe(), Probe::Cli(_)));
        assert!(a.present(), "`sh` should be on PATH in CI");
    }

    #[test]
    fn cli_missing_binary_not_present() {
        let a = Artifact::Cli {
            name: "definitely-not-a-real-binary-xyz".into(),
        };
        assert!(!a.present());
    }

    #[test]
    fn claude_skill_resolves_under_home() {
        let a = Artifact::ClaudeSkill {
            name: "find-docs".into(),
        };
        let p = match a.probe() {
            Probe::Path(p) => p,
            _ => panic!("expected path probe"),
        };
        assert!(p.ends_with(".claude/skills/find-docs"));
    }

    #[test]
    fn path_tilde_expands() {
        let a = Artifact::Path {
            path: "~/foo/bar".into(),
        };
        let p = match a.probe() {
            Probe::Path(p) => p,
            _ => panic!("expected path probe"),
        };
        assert!(p.ends_with("foo/bar"));
        assert!(p.is_absolute());
    }

    #[test]
    fn path_absolute_unchanged() {
        let a = Artifact::Path {
            path: "/tmp/xyz-aimgr-test".into(),
        };
        let p = match a.probe() {
            Probe::Path(p) => p,
            _ => panic!("expected path probe"),
        };
        assert_eq!(p, PathBuf::from("/tmp/xyz-aimgr-test"));
    }

    #[test]
    fn parse_tagged_kinds() {
        let t = r#"
            [[artifacts]]
            kind = "claude-skill"
            name = "find-docs"

            [[artifacts]]
            kind = "claude-rule"
            name = "context7"

            [[artifacts]]
            kind = "cli"
            name = "seek"

            [[artifacts]]
            kind = "path"
            path = "~/.claude/plugins/plannotator"
        "#;
        #[derive(Deserialize)]
        struct Wrap {
            artifacts: Vec<Artifact>,
        }
        let w: Wrap = toml::from_str(t).unwrap();
        assert_eq!(w.artifacts.len(), 4);
        assert!(matches!(w.artifacts[0], Artifact::ClaudeSkill { .. }));
        assert!(matches!(w.artifacts[2], Artifact::Cli { .. }));
    }
}
