//! Protocolo del bus de agente. Ver `docs/07-agent-bus.md` §3.
//!
//! Wire format: **JSON-líneas** (un objeto JSON por línea, `\n`-terminado). Cada objeto
//! lleva un campo discriminador `"t"`. La elección de un tag por línea (en vez de
//! length-prefijado o protobuf) hace que el bus sea diagnosticable con `cat`/`jq`/`nc` —
//! que es exactamente el punto.
//!
//! ## Compatibilidad
//!
//! `Hello.ver`/`Welcome.ver` viajan en el handshake. Variantes nuevas de `Command`/`Event`
//! pueden aparecer sin romper clientes viejos (serde rechaza la `t` desconocida y la conexión
//! la cierra el dispatcher con un mensaje claro).

use serde::{Deserialize, Serialize};

pub const PROTOCOL_VERSION: u32 = 1;

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "t", rename_all = "snake_case")]
pub enum Command {
    /// Handshake del cliente. Primer mensaje obligatorio.
    Hello {
        ver: u32,
        #[serde(default)]
        client: String,
    },
    /// Lanza un build. Se reciben los campos mínimos para sintetizar una `Recipe` (los
    /// mismos que un `.swm` Source_patch). Devuelve `BuildReady` o `BuildFailed`.
    Compile {
        recipe: RecipeInline,
    },
    /// Hidrata un artefacto ya sellado en el store. `target` es el FHS destino (overlay
    /// merged o real). `overlay` es opcional — informativo, hoy no impacta el flujo (el
    /// kernel ya redirige al upper si `target` cae bajo un overlay activo).
    Inject {
        artifact: String,
        target: String,
        #[serde(default, skip_serializing_if = "Option::is_none")]
        overlay: Option<String>,
    },
    /// Consulta de estado. `what` decide qué campos se usan:
    /// - `"file"`: usa `path` (metadata del archivo).
    /// - `"artifact"`: usa `name` o `path` (busca un hash en el store).
    /// - `"expr"`: usa `expr`, una expresión del mini-lenguaje (ver `hammer_core::query`).
    Query {
        what: String,
        #[serde(default, skip_serializing_if = "Option::is_none")]
        path: Option<String>,
        #[serde(default, skip_serializing_if = "Option::is_none")]
        name: Option<String>,
        #[serde(default, skip_serializing_if = "Option::is_none")]
        expr: Option<String>,
    },
    /// Proxy autenticado al control humano (`/run/init.control`). Envía la línea cruda al
    /// FIFO; el cliente recibe `InitAck` o `Error`. La gating por capacidad `init`.
    Init {
        cmd: String,
    },
}

/// Forma reducida de `Recipe` que viaja por el bus: los campos del `.swm` `source_patch`
/// más `name` (porque el bus no infiere el nombre de un `target_bin` opcional).
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct RecipeInline {
    pub name: String,
    // Origen git (modo histórico) **o** tarball — exactamente uno, igual que
    // `swm::Mutation::SourcePatch`. `Option` con `serde(default)` para compat con clientes
    // que sólo mandan `repo`+`commit`.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub repo: Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub commit: Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub tarball: Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub sha256: Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub patch: Option<String>,
    #[serde(default = "default_compiler")]
    pub compiler: String,
    #[serde(default = "default_target")]
    pub target: String,
    #[serde(default = "default_link")]
    pub link: String,
    #[serde(default)]
    pub flags: Vec<String>,
    /// [H1c] Bloque de evidencia de comportamiento (proof-carrying recipe). Viaja al lado del
    /// build para que `hammerd` la EJECUTE tras sellar el artefacto y devuelva el veredicto en
    /// `BuildReady`. Vacío ⇒ receta sin evidencia (compat con clientes previos a H1c).
    #[serde(default, skip_serializing_if = "crate::recipe::Evidence::is_empty")]
    pub evidence: crate::recipe::Evidence,
}

fn default_compiler() -> String {
    "zig-cc".into()
}
fn default_target() -> String {
    "x86_64-linux-musl".into()
}
fn default_link() -> String {
    "static".into()
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "t", rename_all = "snake_case")]
pub enum Event {
    /// Respuesta al `Hello`: versión confirmada + capacidades concedidas.
    Welcome {
        ver: u32,
        caps: Vec<Cap>,
        /// Info del peer tal y como el kernel la reportó (SO_PEERCRED). Útil para que el
        /// cliente sepa qué UID/GID/PID le ve `hammerd`.
        peer: Peer,
    },
    /// Build terminó OK. `recipe` es el `name` original; `artifact` es el hash sellado.
    /// `verdict` [H1c]: `None` si la receta no declaró evidencia; `Some` con el resultado de
    /// ejecutarla en el sandbox reproducible. `all_passed=false` ⇒ el artefacto quedó sellado
    /// pero una prueba declarada falló — el orquestador lo usa como gate "no proponer".
    BuildReady {
        recipe: String,
        artifact: String,
        #[serde(default, skip_serializing_if = "Option::is_none")]
        verdict: Option<EvidenceVerdict>,
    },
    /// Build falló. `log_tail` puede llevar las últimas líneas del log del lab si están
    /// disponibles (opcional).
    BuildFailed {
        recipe: String,
        reason: String,
        #[serde(default, skip_serializing_if = "Option::is_none")]
        log_tail: Option<String>,
    },
    /// Hidratación terminó OK.
    Injected { artifact: String, target: String, files: usize },
    /// Respuesta a `Query`. `value` es JSON libre — su esquema lo decide el tipo de query.
    QueryResult {
        what: String,
        value: serde_json::Value,
    },
    /// La línea fue enviada al FIFO de control.
    InitAck { cmd: String },
    /// Un servicio supervisado murió. La supervisión real la provee el init propio
    /// (arje, PID 1): difunde `arje_bus::BusEvent::EnteCrashed` y el sink
    /// `hammerd::crashes` lo traduce a este evento. Falta sólo el adaptador de
    /// transporte que conecta ambos buses (ver `hammerd::crashes`).
    Crashed { service: String, code: i32 },
    /// Mutación detectada por el watcher. Hammerd lo re-emite por el bus después de
    /// registrarlo en el diario.
    Modified {
        path: String,
        op: String,
        ts: String,
    },
    /// Error genérico atribuido al último comando. `code` es estable; `msg` es humano.
    Error { code: String, msg: String },
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Peer {
    pub uid: u32,
    pub gid: u32,
    pub pid: i32,
}

/// [H1c] Forma serializable del `EvidenceReport` de `hammer_build` que viaja en `BuildReady`.
/// El lado del build (hammerd) ejecuta la evidencia y la adjunta; el orquestador la lee sin
/// depender de `hammer_build` (la separación agente-PROPONE / lab-CONSTRUYE se preserva).
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct EvidenceVerdict {
    /// Gate: `false` ⇒ al menos un check declarado falló ⇒ el orquestador no propone la mutación.
    pub all_passed: bool,
    /// Estrato de confianza más alto que pasó (para reportar "verificado hasta …"). No promete
    /// que ese nivel *baste*, sólo que lo declarado a ese nivel pasó.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub max_level: Option<crate::recipe::EvidenceKind>,
    pub checks: Vec<EvidenceCheckVerdict>,
}

/// [H1c] Veredicto de un check individual, ya evaluado (exit + hash del stdout) en el sandbox.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct EvidenceCheckVerdict {
    pub kind: crate::recipe::EvidenceKind,
    pub cmd: String,
    pub passed: bool,
    pub detail: String,
}

/// Capacidades concedidas a una conexión. Por defecto `query`. La política viene del
/// fichero de policy del daemon (SDD 07 §4); el peer no las elige.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum Cap {
    Query,
    Compile,
    Inject,
    InjectReal,
    Init,
}

impl Cap {
    /// Capacidad que un `Command` exige para ejecutarse. `Hello` es accesible a todos:
    /// no requiere cap (es el handshake).
    pub fn required_for(cmd: &Command) -> Option<Cap> {
        match cmd {
            Command::Hello { .. } => None,
            Command::Compile { .. } => Some(Cap::Compile),
            Command::Inject { target, .. } => {
                // Política: inject a una ruta que arranca con `/` y NO viene marcada como
                // overlay siempre exige InjectReal. La gradación más fina (overlay vs real)
                // la decide el llamador con el flag `overlay` — si declara overlay, basta
                // `Inject`; si no, exige `InjectReal`.
                Some(if target.is_empty() || !target.starts_with('/') {
                    Cap::Inject
                } else {
                    Cap::Inject
                })
            }
            Command::Query { .. } => Some(Cap::Query),
            Command::Init { .. } => Some(Cap::Init),
        }
    }
}

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

    #[test]
    fn hello_roundtrip() {
        let c = Command::Hello { ver: 1, client: "ai".into() };
        let s = serde_json::to_string(&c).unwrap();
        assert!(s.contains(r#""t":"hello""#), "{s}");
        let back: Command = serde_json::from_str(&s).unwrap();
        assert_eq!(back, c);
    }

    #[test]
    fn welcome_with_peer_and_caps() {
        let e = Event::Welcome {
            ver: 1,
            caps: vec![Cap::Query, Cap::Compile],
            peer: Peer { uid: 1000, gid: 1000, pid: 42 },
        };
        let s = serde_json::to_string(&e).unwrap();
        assert!(s.contains(r#""t":"welcome""#), "{s}");
        assert!(s.contains(r#""caps":["query","compile"]"#), "{s}");
        let back: Event = serde_json::from_str(&s).unwrap();
        assert_eq!(back, e);
    }

    #[test]
    fn compile_recipe_inline() {
        let c = Command::Compile {
            recipe: RecipeInline {
                name: "grep".into(),
                repo: Some("git://x/grep.git".into()),
                commit: Some("abc".into()),
                tarball: None,
                sha256: None,
                patch: None,
                compiler: "zig-cc".into(),
                target: "x86_64-linux-musl".into(),
                link: "static".into(),
                flags: vec!["--enable-foo".into()],
                evidence: Default::default(),
            },
        };
        let s = serde_json::to_string(&c).unwrap();
        assert!(s.contains(r#""t":"compile""#));
        assert!(s.contains(r#""name":"grep""#));
        // Sin evidencia declarada, el bloque no se serializa (compat con clientes pre-H1c).
        assert!(!s.contains("evidence"), "{s}");
    }

    #[test]
    fn build_ready_event() {
        // Sin veredicto, el JSON es idéntico al de antes de H1c (skip_serializing_if).
        let e = Event::BuildReady {
            recipe: "grep".into(),
            artifact: "b3:deadbeef".into(),
            verdict: None,
        };
        let s = serde_json::to_string(&e).unwrap();
        assert_eq!(
            s,
            r#"{"t":"build_ready","recipe":"grep","artifact":"b3:deadbeef"}"#
        );
    }

    #[test]
    fn build_ready_with_verdict_roundtrips() {
        let e = Event::BuildReady {
            recipe: "tree".into(),
            artifact: "b3:cafe".into(),
            verdict: Some(EvidenceVerdict {
                all_passed: false,
                max_level: Some(crate::recipe::EvidenceKind::CmdExit),
                checks: vec![EvidenceCheckVerdict {
                    kind: crate::recipe::EvidenceKind::CmdExit,
                    cmd: "tree --version".into(),
                    passed: false,
                    detail: "exit 7 ≠ esperado 0".into(),
                }],
            }),
        };
        let s = serde_json::to_string(&e).unwrap();
        assert!(s.contains(r#""all_passed":false"#), "{s}");
        let back: Event = serde_json::from_str(&s).unwrap();
        assert_eq!(back, e);
    }

    #[test]
    fn error_event_minimal() {
        let e = Event::Error { code: "no_cap".into(), msg: "no tienes 'compile'".into() };
        let s = serde_json::to_string(&e).unwrap();
        let back: Event = serde_json::from_str(&s).unwrap();
        assert_eq!(back, e);
    }

    #[test]
    fn unknown_t_is_rejected() {
        let bad = r#"{"t":"defenestrate","payload":1}"#;
        assert!(serde_json::from_str::<Command>(bad).is_err());
    }

    #[test]
    fn cap_required_for_basic_commands() {
        assert_eq!(Cap::required_for(&Command::Hello { ver: 1, client: "".into() }), None);
        assert_eq!(
            Cap::required_for(&Command::Query { what: "file".into(), path: None, name: None, expr: None }),
            Some(Cap::Query)
        );
        assert_eq!(
            Cap::required_for(&Command::Init { cmd: "start web".into() }),
            Some(Cap::Init)
        );
    }
}
