//! `hammerd` — daemon de hammer. Tres responsabilidades:
//!   1. Diario de mutaciones: fanotify sobre /bin,/sbin,/lib,/etc → `hammer-journal`.
//!   2. FIFO de control humano: `/run/init.control` (texto crudo, una línea por comando).
//!   3. Bus de agente: `/run/agent.sock` (JSON-líneas, SO_PEERCRED). Ver `docs/07-agent-bus.md`.
//!
//! Cada subsistema vive en su thread. Si uno falla en init (típicamente el watcher sin
//! CAP_SYS_ADMIN), los demás siguen arrancando: el daemon en dev puede correr sin sudo y aún
//! así probar el bus.

use std::path::PathBuf;
use std::thread;

use clap::Parser;

mod arje_link;
mod bus;
mod control;
mod crashes;
mod events;
mod watcher;

#[derive(Parser)]
#[command(name = "hammerd", version, about = "Daemon de hammer: bus de agente + diario.")]
struct Args {
    /// Socket del bus de agente.
    #[arg(long, default_value = "/run/agent.sock")]
    agent_sock: String,
    /// FIFO de control humano del init.
    #[arg(long, default_value = "/run/init.control")]
    init_control: PathBuf,
    /// Directorio del diario de mutaciones.
    #[arg(long, default_value = "/var/lib/hammer/journal")]
    journal: String,
    /// Directorio raíz de overlays (para que el watcher pueda filtrar eventos bajo overlays
    /// activos). Default coincide con `hammer try`.
    #[arg(long, default_value = hammer_overlay::DEFAULT_STATE_ROOT)]
    overlay_state_root: String,
    /// Directorios extra a vigilar; si vacío, usa los defaults del FHS (SDD 05 §2).
    #[arg(long = "watch")]
    extra_watch: Vec<PathBuf>,
    /// Store CAS donde sellar artefactos de COMPILE.
    #[arg(long, default_value = "/store")]
    store: PathBuf,
    /// Modo dev: desactiva el watcher fanotify (útil cuando hammerd corre sin CAP_SYS_ADMIN
    /// y sólo queremos probar el bus). Igual que arrancar sin permisos, pero explícito.
    #[arg(long)]
    no_watcher: bool,
    /// Fichero de política de capacidades del bus. Si existe, sustituye a la política por
    /// defecto (misma-UID ⇒ casi todo; otros ⇒ query). Ver `docs/07-agent-bus.md` §4.
    #[arg(long, default_value = "/etc/hammer/agent-caps.toml")]
    agent_caps: PathBuf,
}

fn main() -> anyhow::Result<()> {
    tracing_subscriber::fmt()
        .with_writer(std::io::stderr)
        .with_env_filter(
            tracing_subscriber::EnvFilter::try_from_default_env()
                .unwrap_or_else(|_| "info".into()),
        )
        .init();

    let args = Args::parse();
    tracing::info!(
        agent_sock = %args.agent_sock,
        init_control = %args.init_control.display(),
        journal = %args.journal,
        store = %args.store.display(),
        "hammerd: arranque"
    );

    let event_bus = events::EventBus::new();

    // Puente con el bus del init (arje): si ENTE_BUS_SOCK está definido, se suscribe a los
    // eventos de ciclo de vida y publica los crashes en el bus de agente. No-op si no hay init.
    arje_link::spawn_if_configured(event_bus.clone());

    // FIFO de control humano: lo creamos siempre que se pueda (no es fatal).
    let init_fifo_ok = match control::ensure_fifo(&args.init_control) {
        Ok(path) => {
            let reader_path = path.clone();
            thread::Builder::new()
                .name("init-ctl-reader".into())
                .spawn(move || {
                    if let Err(e) = control::run_reader(&reader_path) {
                        tracing::warn!(error = %e, "init-ctl-reader: terminó");
                    }
                })
                .ok();
            true
        }
        Err(e) => {
            tracing::warn!(error = %e, "init-control: no pude crear FIFO; INIT del bus fallará");
            false
        }
    };

    // Watcher fanotify: opcional. Si falla (no caps), el daemon sigue con el bus solo.
    if !args.no_watcher {
        let journal = hammer_journal::Journal::open(&args.journal)?;
        let mut dirs = watcher::default_watch_dirs();
        dirs.extend(args.extra_watch.clone());
        let overlay_root = PathBuf::from(&args.overlay_state_root);
        let bus_for_watcher = event_bus.clone();
        match watcher::Watcher::start_with_events(
            &dirs,
            journal,
            Some(overlay_root),
            Some(bus_for_watcher),
        ) {
            Ok(w) => {
                tracing::info!("watcher fanotify activo");
                thread::Builder::new()
                    .name("watcher".into())
                    .spawn(move || {
                        if let Err(e) = w.run_forever() {
                            tracing::warn!(error = %e, "watcher: terminó con error");
                        }
                    })
                    .ok();
            }
            Err(e) => {
                tracing::warn!(
                    error = %e,
                    "watcher fanotify NO arrancó; daemon sigue (sin diario en este run)"
                );
            }
        }
    } else {
        tracing::info!("--no-watcher: omitiendo fanotify");
    }

    // Bus de agente: bloqueante en este thread. Si falla, fin del daemon.
    // Política de caps: del fichero si existe, si no la heurística por-UID por defecto.
    let policy = match hammer_core::AgentCapsConfig::load(&args.agent_caps) {
        Ok(Some(cfg)) => {
            tracing::info!(path = %args.agent_caps.display(), rules = cfg.rule.len(),
                "bus: política de caps cargada de fichero");
            bus::policy_from_config(cfg)
        }
        Ok(None) => {
            tracing::info!(path = %args.agent_caps.display(),
                "bus: sin fichero de caps; uso la política por defecto (por-UID)");
            bus::default_policy()
        }
        Err(e) => {
            // Config presente pero inválida: fallar cerrado sería más seguro, pero romper el
            // arranque del daemon por un typo es peor en la práctica. Avisamos fuerte y caemos
            // a la política por defecto (restrictiva para no-dueños).
            tracing::error!(path = %args.agent_caps.display(), error = %e,
                "bus: fichero de caps inválido; uso la política por defecto");
            bus::default_policy()
        }
    };
    let ctx = bus::BusContext {
        store_root: args.store,
        init_control: if init_fifo_ok {
            args.init_control
        } else {
            // Apuntamos a un path inexistente: cualquier INIT fallará con "init_send_failed"
            // y un mensaje claro. Mejor que callar.
            PathBuf::from("/dev/null/init-control-missing")
        },
        events: event_bus,
    };
    bus::serve_agent_bus(std::path::Path::new(&args.agent_sock), policy, ctx)?;
    Ok(())
}
