//! Política de capacidades del bus de agente, declarativa. Ver `docs/07-agent-bus.md` §4 y
//! `docs/09-trust-model.md`.
//!
//! El daemon concede caps a cada conexión según su `Peer` (UID/GID ya autenticados vía
//! `SO_PEERCRED`). Hasta ahora esa política vivía hardcodeada en código; este módulo la lee de
//! `/etc/hammer/agent-caps.toml` para que el operador la ajuste sin recompilar. El peer NUNCA
//! elige sus caps: las decide el daemon a partir de esta config.
//!
//! ```toml
//! # Caps para cualquier UID/GID no cubierto por una regla.
//! default = ["query"]
//!
//! # Reglas evaluadas en orden; gana la PRIMERA que casa. Una regla casa si todos los campos
//! # que declara (`uid` y/o `gid`) coinciden con el peer.
//! [[rule]]
//! uid = 0
//! caps = ["query", "compile", "inject", "inject-real", "init"]
//!
//! [[rule]]
//! gid = 994            # p. ej. grupo `hammer`
//! caps = ["query", "compile", "inject", "init"]
//! ```

use serde::Deserialize;

use crate::proto::Cap;

/// Config declarativa de la política de capacidades del bus.
#[derive(Debug, Clone, Deserialize)]
pub struct AgentCapsConfig {
    /// Caps para peers que no casan ninguna regla.
    #[serde(default = "default_caps")]
    pub default: Vec<Cap>,
    /// Reglas, evaluadas en orden (la primera que casa gana).
    #[serde(default)]
    pub rule: Vec<CapRule>,
}

/// Una regla: a quién aplica (por `uid` y/o `gid`) y qué caps concede.
#[derive(Debug, Clone, Deserialize)]
pub struct CapRule {
    #[serde(default)]
    pub uid: Option<u32>,
    #[serde(default)]
    pub gid: Option<u32>,
    pub caps: Vec<Cap>,
}

fn default_caps() -> Vec<Cap> {
    vec![Cap::Query]
}

impl Default for AgentCapsConfig {
    fn default() -> Self {
        AgentCapsConfig {
            default: default_caps(),
            rule: Vec::new(),
        }
    }
}

impl CapRule {
    /// ¿Casa esta regla con `(uid, gid)`? Casa si todos los campos declarados coinciden. Una
    /// regla sin `uid` ni `gid` NO casa (sería un catch-all redundante con `default`, y casi
    /// siempre un error de config); se ignora con cuidado en `caps_for`.
    fn matches(&self, uid: u32, gid: u32) -> bool {
        if self.uid.is_none() && self.gid.is_none() {
            return false;
        }
        self.uid.map(|u| u == uid).unwrap_or(true) && self.gid.map(|g| g == gid).unwrap_or(true)
    }
}

impl AgentCapsConfig {
    pub fn from_toml(s: &str) -> crate::Result<AgentCapsConfig> {
        toml::from_str(s).map_err(|e| crate::Error::Serde(e.to_string()))
    }

    /// Carga la config desde `path`. Devuelve `Ok(None)` si el fichero no existe — el daemon
    /// decide entonces su política por defecto.
    pub fn load(path: &std::path::Path) -> crate::Result<Option<AgentCapsConfig>> {
        match std::fs::read_to_string(path) {
            Ok(s) => Ok(Some(Self::from_toml(&s)?)),
            Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(None),
            Err(e) => Err(e.into()),
        }
    }

    /// Caps concedidas a un peer `(uid, gid)`: la primera regla que casa, o `default`.
    pub fn caps_for(&self, uid: u32, gid: u32) -> Vec<Cap> {
        for r in &self.rule {
            if r.matches(uid, gid) {
                return r.caps.clone();
            }
        }
        self.default.clone()
    }
}

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

    const CFG: &str = r#"
default = ["query"]

[[rule]]
uid = 0
caps = ["query", "compile", "inject", "inject-real", "init"]

[[rule]]
gid = 994
caps = ["query", "compile", "inject", "init"]

[[rule]]
uid = 1000
gid = 1000
caps = ["query", "compile"]
"#;

    #[test]
    fn root_gets_everything() {
        let c = AgentCapsConfig::from_toml(CFG).unwrap();
        assert_eq!(
            c.caps_for(0, 0),
            vec![Cap::Query, Cap::Compile, Cap::Inject, Cap::InjectReal, Cap::Init]
        );
    }

    #[test]
    fn first_matching_rule_wins() {
        let c = AgentCapsConfig::from_toml(CFG).unwrap();
        // uid 1000 + gid 994: la regla del gid 994 viene ANTES que la de uid+gid 1000,
        // así que gana la del grupo.
        assert_eq!(
            c.caps_for(1000, 994),
            vec![Cap::Query, Cap::Compile, Cap::Inject, Cap::Init]
        );
    }

    #[test]
    fn both_fields_must_match() {
        let c = AgentCapsConfig::from_toml(CFG).unwrap();
        // uid 1000 + gid 1000: no casa root, no casa gid 994; casa la regla uid=1000,gid=1000.
        assert_eq!(c.caps_for(1000, 1000), vec![Cap::Query, Cap::Compile]);
    }

    #[test]
    fn unmatched_falls_back_to_default() {
        let c = AgentCapsConfig::from_toml(CFG).unwrap();
        assert_eq!(c.caps_for(31337, 31337), vec![Cap::Query]);
    }

    #[test]
    fn empty_config_defaults_to_query() {
        let c = AgentCapsConfig::from_toml("").unwrap();
        assert_eq!(c.caps_for(0, 0), vec![Cap::Query]);
    }

    #[test]
    fn rule_without_uid_or_gid_is_ignored() {
        let c = AgentCapsConfig::from_toml(
            r#"
default = ["query"]
[[rule]]
caps = ["init", "inject-real"]
"#,
        )
        .unwrap();
        // La regla vacía no casa a nadie ⇒ cae al default.
        assert_eq!(c.caps_for(0, 0), vec![Cap::Query]);
    }

    #[test]
    fn load_missing_file_is_none() {
        let got = AgentCapsConfig::load(std::path::Path::new("/no/existe.toml")).unwrap();
        assert!(got.is_none());
    }
}
