pub mod builtin;
pub mod custom;

use std::collections::BTreeMap;

use anyhow::Result;
use serde::{Deserialize, Serialize};

use crate::registry::{Artifact, InstallMethod, Prereq, Source};
use crate::tools::Tool;

#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(deny_unknown_fields)]
pub struct Package {
    pub name: String,
    #[serde(default)]
    pub description: Option<String>,
    #[serde(default)]
    pub homepage: Option<String>,
    #[serde(default)]
    pub tools: Vec<Tool>,
    #[serde(default)]
    pub scope: Scope,
    #[serde(default)]
    pub depends_on: Vec<String>,
    #[serde(default)]
    pub artifacts: Vec<Artifact>,
    /// Fallback presence probe (shell). Ignored when `artifacts` is non-empty.
    #[serde(default)]
    pub verify: Option<String>,
    #[serde(default)]
    pub prereq: Vec<Prereq>,
    #[serde(default)]
    pub install: Vec<InstallMethod>,
    #[serde(default)]
    pub update: Option<String>,
    #[serde(default)]
    pub uninstall: Option<String>,
    #[serde(default)]
    pub instructions: Vec<InstructionsBlock>,
    #[serde(default)]
    pub permissions: Vec<PermissionGrant>,
}

#[derive(Copy, Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize)]
#[serde(rename_all = "lowercase")]
pub enum Scope {
    Global,
    Project,
    #[default]
    Both,
}

#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(deny_unknown_fields)]
pub struct InstructionsBlock {
    pub content: String,
    /// Tools this block applies to. Empty = all of the package's `tools`.
    #[serde(default)]
    pub tools: Vec<Tool>,
}

/// Permission rules a package wants pre-approved in a tool's settings file
/// (e.g. Claude Code's `Bash(...)` patterns).
#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(deny_unknown_fields)]
pub struct PermissionGrant {
    /// Tool this grant targets. Empty = all of the package's `tools` that have
    /// a known settings file (today: just `claude`).
    #[serde(default)]
    pub tool: Option<Tool>,
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub allow: Vec<String>,
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub ask: Vec<String>,
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub deny: Vec<String>,
}

impl PermissionGrant {
    pub fn is_empty(&self) -> bool {
        self.allow.is_empty() && self.ask.is_empty() && self.deny.is_empty()
    }
}

#[derive(Debug, Clone)]
pub struct Resolved {
    pub package: Package,
    pub source: Source,
}

pub fn list() -> Result<Vec<Resolved>> {
    let mut by_name: BTreeMap<String, Resolved> = BTreeMap::new();
    for package in builtin::all() {
        by_name.insert(
            package.name.clone(),
            Resolved {
                package,
                source: Source::BuiltIn,
            },
        );
    }
    for package in custom::load_all()? {
        let source = if by_name.contains_key(&package.name) {
            Source::CustomOverride
        } else {
            Source::Custom
        };
        by_name.insert(package.name.clone(), Resolved { package, source });
    }
    Ok(by_name.into_values().collect())
}

pub fn resolve(name: &str) -> Result<Option<Resolved>> {
    if let Some(package) = custom::load(name)? {
        let source = if builtin::by_name(name).is_some() {
            Source::CustomOverride
        } else {
            Source::Custom
        };
        return Ok(Some(Resolved { package, source }));
    }
    if let Some(package) = builtin::by_name(name) {
        return Ok(Some(Resolved {
            package,
            source: Source::BuiltIn,
        }));
    }
    Ok(None)
}

impl Package {
    /// Tools this package applies to, filtered by which tools are active in the
    /// provided set. Empty `package.tools` means "any", so all actives apply.
    pub fn applicable_tools(&self, active: &[Tool]) -> Vec<Tool> {
        if self.tools.is_empty() {
            return active.to_vec();
        }
        self.tools
            .iter()
            .copied()
            .filter(|t| active.contains(t))
            .collect()
    }
}
