//! Bus de agente: `/run/agent.sock`. Ver `docs/07-agent-bus.md`.
//!
//! Modelo de threading (síncrono, una conexión = N threads pequeños):
//!
//! ```text
//!   [accept loop] ── spawn ──▶ conn{
//!        [reader thread] ── dispatch ──┐
//!        [bus forwarder]  ─────────────┼──▶ tx ──▶ [writer thread] ──▶ socket
//!        [compile worker] (puntual) ───┘
//!   }
//! ```
//!
//! No usamos `tokio` para no arrastrar runtime asíncrono: la cadencia esperada del bus es
//! humana + agente (decenas de mensajes/seg en el peor caso). Cada conexión tiene su propio
//! canal `Sender<Event>`; la `EventBus` global re-emite Modified/Crashed a esos canales.

use std::io::{BufRead, BufReader, Write};
use std::os::fd::AsFd;
use std::os::unix::net::{UnixListener, UnixStream};
use std::path::{Path, PathBuf};
use std::sync::mpsc::{self, Sender};
use std::sync::{Arc, Mutex};
use std::thread;

use nix::sys::socket::{getsockopt, sockopt::PeerCredentials};

use crate::events::EventBus;
use hammer_core::proto::{
    Cap, Command, Event, EvidenceCheckVerdict, EvidenceVerdict, Peer, RecipeInline,
    PROTOCOL_VERSION,
};

/// Política de capacidades: dado el `Peer` (ya autenticado por SO_PEERCRED), devuelve qué
/// caps tiene esa conexión. Por defecto: misma UID que el daemon ⇒ todo menos `inject-real`;
/// otros UIDs ⇒ sólo `query`. La policy más expresiva (lectura de un toml en `/etc/hammer`)
/// es trabajo posterior; la firma del callable basta para encajarla sin tocar este módulo.
pub type CapsPolicy = Arc<dyn Fn(&Peer) -> Vec<Cap> + Send + Sync>;

pub fn default_policy() -> CapsPolicy {
    let my_uid = unsafe { libc::getuid() };
    Arc::new(move |peer: &Peer| {
        if peer.uid == my_uid {
            vec![Cap::Query, Cap::Compile, Cap::Inject, Cap::Init]
        } else {
            vec![Cap::Query]
        }
    })
}

/// Política derivada de un fichero `agent-caps.toml` (SDD 07 §4). El peer no elige sus caps;
/// las resuelve la config por `(uid, gid)`.
pub fn policy_from_config(cfg: hammer_core::AgentCapsConfig) -> CapsPolicy {
    Arc::new(move |peer: &Peer| cfg.caps_for(peer.uid, peer.gid))
}

/// Contexto que necesita el dispatcher para resolver comandos. Compartido por todas las
/// conexiones (clones baratos: paths + Arc).
#[derive(Clone)]
pub struct BusContext {
    pub store_root: PathBuf,
    pub init_control: PathBuf,
    pub events: EventBus,
}

#[derive(Debug, thiserror::Error)]
pub enum Error {
    #[error("io: {0}")]
    Io(#[from] std::io::Error),
    #[error("bind {0}: {1}")]
    Bind(PathBuf, std::io::Error),
    #[error("peer creds: {0}")]
    Peer(String),
}

pub type Result<T> = std::result::Result<T, Error>;

/// Arranca el listener bloqueante. Pensado para correr en su propio thread desde `main`.
pub fn serve_agent_bus(
    sock_path: &Path,
    policy: CapsPolicy,
    ctx: BusContext,
) -> Result<()> {
    // Limpia un socket olvidado de una ejecución previa. Si el path está ocupado por algo
    // que no es un socket, dejamos que `bind` falle — no queremos rm-rf por accidente.
    let _ = std::fs::remove_file(sock_path);
    let listener = UnixListener::bind(sock_path)
        .map_err(|e| Error::Bind(sock_path.to_path_buf(), e))?;
    tracing::info!(sock = %sock_path.display(), "bus: escuchando");

    for stream in listener.incoming() {
        match stream {
            Ok(s) => {
                let policy = policy.clone();
                let ctx = ctx.clone();
                thread::Builder::new()
                    .name("bus-conn".into())
                    .spawn(move || {
                        if let Err(e) = handle_connection(s, policy, ctx) {
                            tracing::warn!(error = %e, "bus: conexión terminó con error");
                        }
                    })
                    .ok();
            }
            Err(e) => tracing::warn!(error = %e, "bus: accept falló"),
        }
    }
    Ok(())
}

fn read_peer(stream: &UnixStream) -> Result<Peer> {
    let cred = getsockopt(&stream.as_fd(), PeerCredentials)
        .map_err(|e| Error::Peer(e.to_string()))?;
    Ok(Peer {
        uid: cred.uid(),
        gid: cred.gid(),
        pid: cred.pid(),
    })
}

fn handle_connection(stream: UnixStream, policy: CapsPolicy, ctx: BusContext) -> Result<()> {
    let peer = read_peer(&stream)?;
    let caps = policy(&peer);
    tracing::info!(?peer, ?caps, "bus: nueva conexión");

    // Canal de eventos hacia el writer. Todos los productores (dispatcher, bus forwarder,
    // workers de COMPILE) clonan este Sender.
    let (tx, rx) = mpsc::channel::<Event>();

    // Writer thread: serializa todo lo que recibe del canal y lo escribe al socket.
    let stream_writer = stream.try_clone()?;
    let _writer_handle = thread::Builder::new()
        .name("bus-conn-writer".into())
        .spawn(move || writer_loop(stream_writer, rx))
        .ok();

    // Bus forwarder thread: cualquier evento global (Modified/Crashed) se re-emite a este
    // peer. Se cierra solo cuando el global drop el último Sender (en la práctica, nunca)
    // o cuando nuestro tx local cae (peer cerró).
    let bus_rx = ctx.events.subscribe();
    let bus_tx = tx.clone();
    thread::Builder::new()
        .name("bus-conn-fwd".into())
        .spawn(move || {
            for ev in bus_rx {
                if bus_tx.send(ev).is_err() {
                    break;
                }
            }
        })
        .ok();

    // Handshake. La primera línea debe ser Hello.
    let mut reader = BufReader::new(stream);
    let mut first = String::new();
    reader.read_line(&mut first)?;
    match serde_json::from_str::<Command>(first.trim()) {
        Ok(Command::Hello { ver, client }) => {
            tracing::info!(ver, client = %client, "bus: hello");
            let _ = tx.send(Event::Welcome {
                ver: PROTOCOL_VERSION,
                caps: caps.clone(),
                peer: peer.clone(),
            });
        }
        Ok(other) => {
            let _ = tx.send(Event::Error {
                code: "no_hello".into(),
                msg: format!("se esperaba 'hello', llegó '{}'", t_of(&other)),
            });
            return Ok(());
        }
        Err(e) => {
            let _ = tx.send(Event::Error {
                code: "bad_handshake".into(),
                msg: format!("hello inválido: {e}"),
            });
            return Ok(());
        }
    }

    // Read loop. Cada línea es un Command. La conexión se cierra cuando se cierra EOF, o
    // cuando un error de IO lo fuerza.
    let caps_set: std::collections::BTreeSet<Cap> = caps.iter().copied().collect();
    let caps_set = Arc::new(caps_set);
    for line in reader.lines() {
        let line = match line {
            Ok(l) => l,
            Err(_) => break,
        };
        if line.trim().is_empty() {
            continue;
        }
        let cmd: Command = match serde_json::from_str(&line) {
            Ok(c) => c,
            Err(e) => {
                let _ = tx.send(Event::Error {
                    code: "bad_command".into(),
                    msg: format!("JSON inválido: {e}"),
                });
                continue;
            }
        };
        dispatch(cmd, &tx, &caps_set, &ctx);
    }
    Ok(())
}

fn writer_loop(mut stream: UnixStream, rx: mpsc::Receiver<Event>) {
    for ev in rx {
        let line = match serde_json::to_string(&ev) {
            Ok(s) => s,
            Err(e) => {
                tracing::error!(error = %e, "bus: serializando event");
                continue;
            }
        };
        if stream.write_all(line.as_bytes()).is_err() {
            break;
        }
        if stream.write_all(b"\n").is_err() {
            break;
        }
        // El bus es de baja cadencia; flush por mensaje no es relevante en costes y mejora
        // mucho la latencia percibida del cliente.
        let _ = stream.flush();
    }
}

fn dispatch(
    cmd: Command,
    tx: &Sender<Event>,
    caps: &Arc<std::collections::BTreeSet<Cap>>,
    ctx: &BusContext,
) {
    if let Some(needed) = Cap::required_for(&cmd) {
        if !caps.contains(&needed) {
            let _ = tx.send(Event::Error {
                code: "no_cap".into(),
                msg: format!("comando requiere capacidad '{needed:?}', no concedida"),
            });
            return;
        }
    }
    match cmd {
        Command::Hello { .. } => {
            // Doble hello: lo ignoramos con un Error suave.
            let _ = tx.send(Event::Error {
                code: "duplicate_hello".into(),
                msg: "ya hicimos hello en esta conexión".into(),
            });
        }
        Command::Compile { recipe } => {
            // Build pesado: a un worker. El read-loop sigue libre para más comandos.
            let tx = tx.clone();
            let store_root = ctx.store_root.clone();
            thread::Builder::new()
                .name("bus-compile".into())
                .spawn(move || run_compile(recipe, store_root, tx))
                .ok();
        }
        Command::Inject { artifact, target, overlay: _ } => {
            let store = match hammer_core::Store::open(&ctx.store_root) {
                Ok(s) => s,
                Err(e) => {
                    let _ = tx.send(Event::Error {
                        code: "store".into(),
                        msg: e.to_string(),
                    });
                    return;
                }
            };
            let artifact_dir = match store.find_by_hash(&artifact) {
                Ok(p) => p,
                Err(e) => {
                    let _ = tx.send(Event::Error {
                        code: "not_found".into(),
                        msg: e.to_string(),
                    });
                    return;
                }
            };
            let report = match hammer_build::run_hydrate(
                &artifact_dir,
                Path::new(&target),
                hammer_core::LinkMode::Static,
                None,
            ) {
                Ok(r) => r,
                Err(e) => {
                    let _ = tx.send(Event::Error {
                        code: "hydrate_failed".into(),
                        msg: e.to_string(),
                    });
                    return;
                }
            };
            let _ = tx.send(Event::Injected {
                artifact,
                target,
                files: report.files.len(),
            });
        }
        Command::Query { what, path, name, expr } => {
            let value = match what.as_str() {
                "file" => {
                    let Some(p) = path else {
                        let _ = tx.send(Event::Error {
                            code: "bad_query".into(),
                            msg: "query 'file' requiere 'path'".into(),
                        });
                        return;
                    };
                    query_file(&p)
                }
                "artifact" => {
                    let Some(h) = name.as_deref().or(path.as_deref()) else {
                        let _ = tx.send(Event::Error {
                            code: "bad_query".into(),
                            msg: "query 'artifact' requiere 'name' (el hash)".into(),
                        });
                        return;
                    };
                    query_artifact(&ctx.store_root, h)
                }
                "expr" => {
                    let Some(e) = expr.as_deref() else {
                        let _ = tx.send(Event::Error {
                            code: "bad_query".into(),
                            msg: "query 'expr' requiere 'expr' (la expresión)".into(),
                        });
                        return;
                    };
                    // El daemon evalúa con el `fs_root` del sistema host (None ⇒ rutas
                    // absolutas tal y como vienen). Si el .swm necesitara otra base, el
                    // cliente puede evaluar localmente con un EvalContext distinto.
                    hammer_core::query::eval_str(e, &hammer_core::query::EvalContext::new())
                }
                other => {
                    let _ = tx.send(Event::Error {
                        code: "unknown_query".into(),
                        msg: format!("'what'='{other}' no soportado"),
                    });
                    return;
                }
            };
            let _ = tx.send(Event::QueryResult { what, value });
        }
        Command::Init { cmd } => {
            match crate::control::send_to_fifo(&ctx.init_control, &cmd) {
                Ok(()) => {
                    let _ = tx.send(Event::InitAck { cmd });
                }
                Err(e) => {
                    let _ = tx.send(Event::Error {
                        code: "init_send_failed".into(),
                        msg: e.to_string(),
                    });
                }
            }
        }
    }
}

fn run_compile(recipe: RecipeInline, store_root: PathBuf, tx: Sender<Event>) {
    let name = recipe.name.clone();
    let mutation = hammer_core::swm::Mutation::SourcePatch {
        repo: recipe.repo,
        commit: recipe.commit,
        tarball: recipe.tarball,
        sha256: recipe.sha256,
        strip_components: None,
        patch: recipe.patch,
        patch_url: None,
        build: hammer_core::swm::SwmBuild {
            compiler: recipe.compiler,
            target: recipe.target,
            link: recipe.link,
            flags: recipe.flags,
            phases: Default::default(),
            zig_version: None,
        },
        target_bin: format!("/usr/bin/{name}"),
        expected_hash: None,
        deps: Default::default(),
        // [H1c] La evidencia declarada por el cliente viaja a la mutación para ejecutarse tras
        // sellar el artefacto (ver `evidence_verdict`).
        evidence: recipe.evidence,
        // [H4b] El camino del daemon (agente propone → construye) no declara superficies: el gate
        // de compatibilidad vive en `install` (CLI) contra el estado instalado. Vacío aquí.
        slots: Default::default(),
    };
    let store = match hammer_core::Store::open(&store_root) {
        Ok(s) => s,
        Err(e) => {
            let _ = tx.send(Event::BuildFailed {
                recipe: name,
                reason: format!("store: {e}"),
                log_tail: None,
            });
            return;
        }
    };
    let cfg = hammer_build::BuildConfig::from_env_or_defaults(store.root());
    match hammer_build::build_source_patch(&mutation, &cfg, &store, None, None) {
        Ok(h) => {
            // [H1c] El artefacto está sellado; ahora EJECUTAMOS la evidencia declarada y
            // adjuntamos el veredicto. El gate ("no proponer" si falla) vive en el orquestador
            // —el lab sólo construye y reporta—; por eso enviamos `BuildReady` con veredicto
            // aunque algún check falle.
            let verdict = evidence_verdict(&mutation, &cfg, &store);
            let _ = tx.send(Event::BuildReady {
                recipe: name,
                artifact: h.as_str().to_string(),
                verdict,
            });
        }
        Err(e) => {
            // Si el fallo trae la cola del log del lab, la separamos de la razón para que la
            // IA pueda leer el error real del compilador, no sólo el mensaje de Rust.
            let (reason, log_tail) = match hammer_build::sandbox::BuildFailure::from_error(&e) {
                Some(bf) => (bf.reason.clone(), bf.log_tail.clone()),
                None => (e.to_string(), None),
            };
            let _ = tx.send(Event::BuildFailed {
                recipe: name,
                reason,
                log_tail,
            });
        }
    }
}

/// [H1c] Ejecuta la evidencia declarada de una mutación `source_patch` y la reduce al veredicto
/// serializable del bus. Devuelve:
/// - `None` si la receta no declara evidencia (nada que correr).
/// - `Some(all_passed=true/false, …)` con el resultado por check.
/// - `Some(all_passed=false, …)` sintético si la evidencia ni siquiera pudo ejecutarse (falta una
///   build-dep, error de sandbox…): eso también debe bloquear la propuesta, no pasar en silencio.
fn evidence_verdict(
    mutation: &hammer_core::swm::Mutation,
    cfg: &hammer_build::BuildConfig,
    store: &hammer_core::Store,
) -> Option<EvidenceVerdict> {
    let has_evidence = matches!(
        mutation,
        hammer_core::swm::Mutation::SourcePatch { evidence, .. } if !evidence.is_empty()
    );
    if !has_evidence {
        return None;
    }
    match run_evidence_inner(mutation, cfg, store) {
        Ok(v) => Some(v),
        Err(e) => {
            tracing::warn!(error = %e, "evidence: no se pudo ejecutar; veredicto = fallo");
            Some(EvidenceVerdict {
                all_passed: false,
                max_level: None,
                checks: vec![EvidenceCheckVerdict {
                    kind: hammer_core::EvidenceKind::CmdExit,
                    cmd: "<runner>".into(),
                    passed: false,
                    detail: format!("la evidencia no pudo ejecutarse: {e}"),
                }],
            })
        }
    }
}

fn run_evidence_inner(
    mutation: &hammer_core::swm::Mutation,
    cfg: &hammer_build::BuildConfig,
    store: &hammer_core::Store,
) -> hammer_core::Result<EvidenceVerdict> {
    let catalog_dir = hammer_build::swm_bridge::catalog_dir_for(cfg, None);
    std::fs::create_dir_all(&catalog_dir)?;
    let recipe = hammer_build::swm_bridge::recipe_from_source_patch(mutation, None, &catalog_dir)?;
    let report = hammer_build::run_evidence(&recipe, cfg, store)?;
    Ok(EvidenceVerdict {
        all_passed: report.all_passed(),
        max_level: report.max_level_passed(),
        checks: report
            .results
            .iter()
            .map(|r| EvidenceCheckVerdict {
                kind: r.kind,
                cmd: r.cmd.clone(),
                passed: r.outcome.passed,
                detail: r.outcome.detail.clone(),
            })
            .collect(),
    })
}

fn query_file(path: &str) -> serde_json::Value {
    let p = Path::new(path);
    let meta = match std::fs::symlink_metadata(p) {
        Ok(m) => m,
        Err(e) => return serde_json::json!({"exists": false, "error": e.to_string()}),
    };
    use std::os::unix::fs::MetadataExt;
    serde_json::json!({
        "exists": true,
        "size": meta.size(),
        "mode": meta.mode(),
        "uid": meta.uid(),
        "gid": meta.gid(),
        "is_dir": meta.is_dir(),
        "is_symlink": meta.file_type().is_symlink(),
    })
}

fn query_artifact(store_root: &Path, hash: &str) -> serde_json::Value {
    let store = match hammer_core::Store::open(store_root) {
        Ok(s) => s,
        Err(e) => return serde_json::json!({"error": e.to_string()}),
    };
    match store.find_by_hash(hash) {
        Ok(p) => serde_json::json!({"exists": true, "path": p.display().to_string()}),
        Err(_) => serde_json::json!({"exists": false}),
    }
}

fn t_of(cmd: &Command) -> &'static str {
    match cmd {
        Command::Hello { .. } => "hello",
        Command::Compile { .. } => "compile",
        Command::Inject { .. } => "inject",
        Command::Query { .. } => "query",
        Command::Init { .. } => "init",
    }
}

// Re-export para que el caller no tenga que importar Mutex/Arc directamente.
#[allow(dead_code)]
pub(crate) type SharedCaps = Arc<Mutex<std::collections::BTreeSet<Cap>>>;

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

    #[test]
    fn default_policy_grants_query_to_others() {
        let p = default_policy();
        // Un peer con UID distinto al actual debe quedar como sólo-query.
        let foreign = Peer {
            uid: u32::MAX,
            gid: 0,
            pid: 1,
        };
        assert_eq!(p(&foreign), vec![Cap::Query]);
    }

    #[test]
    fn config_policy_resolves_by_uid_gid() {
        let cfg = hammer_core::AgentCapsConfig::from_toml(
            r#"
default = ["query"]
[[rule]]
uid = 0
caps = ["query", "compile", "inject", "inject-real", "init"]
"#,
        )
        .unwrap();
        let p = policy_from_config(cfg);
        let root = Peer { uid: 0, gid: 0, pid: 1 };
        let other = Peer { uid: 1234, gid: 1234, pid: 2 };
        assert!(p(&root).contains(&Cap::InjectReal));
        assert_eq!(p(&other), vec![Cap::Query]);
    }

    #[test]
    fn t_of_matches_all_commands() {
        // No es exhaustivo de Cargo, pero verifica que no nos olvidemos del nombre del tag.
        assert_eq!(t_of(&Command::Hello { ver: 1, client: "".into() }), "hello");
        assert_eq!(
            t_of(&Command::Compile {
                recipe: RecipeInline {
                    name: "x".into(),
                    repo: Some("g".into()),
                    commit: Some("c".into()),
                    tarball: None,
                    sha256: None,
                    patch: None,
                    compiler: "zig-cc".into(),
                    target: "t".into(),
                    link: "static".into(),
                    flags: vec![],
                    evidence: Default::default(),
                },
            }),
            "compile"
        );
    }

    #[test]
    fn query_file_reports_missing() {
        let v = query_file("/no/existe/seguramente/ahora");
        assert_eq!(v["exists"], serde_json::json!(false));
    }
}
