//! **Arranque por grafo** ([ADR 0010](../../docs/adr/0010-arranque-grafo-mirada.md)).
//!
//! El menú de arranque de hammer no es una *lista* de kernels (GRUB) sino la **navegación del grafo
//! content-addressed de estados** del sistema. Este módulo sube el modelo de generaciones in-place
//! ([`crate`]) a ese grafo navegable y define el **contrato de datos** con `mirada` (el compositor de
//! tawasuyu que dibuja el menú sobre KMS):
//!
//! - **hammer → mirada**: [`build`] arma el grafo y [`emit`] lo escribe en
//!   [`BOOT_GRAPH_PATH`] (`/run/hammer/boot-graph.json`), world-readable y regenerable.
//! - **mirada → hammer**: mirada escribe el `id` elegido en [`BOOT_SELECT_PATH`] **o** invoca
//!   `hammer boot activate <id>`; [`activate`] valida el id contra el grafo y **activa** el nodo
//!   (pivota la generación vía el rollback existente).
//!
//! La frontera es *un fichero + un comando*, no una API viva: mirada puede maquetar el menú contra un
//! `boot-graph.json` de ejemplo sin esperar a hammer, y hammer emite el grafo sin esperar a mirada.
//!
//! **Content-addressing.** El `id` de cada nodo es el `of_tree` (BLAKE3) del árbol que la generación
//! proyectó — sin el prefijo `b3:`, para casar con el `<blake3>` del contrato. `parents` forma un DAG
//! (no una lista lineal): el padre de una generación es el `of_tree` de su generación anterior. Activar
//! un nodo es *reproducir/hidratar* un árbol sellado del store, no ejecutar algo nuevo (ADR 0007/0009).

use std::collections::HashMap;
use std::path::Path;

use hammer_core::ArtifactHash;
use hammer_journal::Journal;
use serde::{Deserialize, Serialize};

use crate::{
    current, list, live_chain, rollback, Error, GenerationManifest, Result, RollbackReport,
};

/// Path bien conocido donde hammer publica el grafo para mirada. Regenerable, world-readable.
pub const BOOT_GRAPH_PATH: &str = "/run/hammer/boot-graph.json";

/// Path bien conocido donde mirada deja el `id` del nodo a activar (canal de vuelta alternativo a
/// `hammer boot activate <id>`).
pub const BOOT_SELECT_PATH: &str = "/run/hammer/boot-select";

/// Qué representa un nodo del grafo de arranque. Guía cómo lo dibuja mirada y cómo lo activa hammer.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum NodeKind {
    /// Una generación de upgrade normal (tiene padre).
    Generation,
    /// Un punto de captura explícito (reservado; hoy no se emite).
    Snapshot,
    /// La primera generación / raíz del linaje (sin padre).
    Base,
    /// Nodo sintético "deshacer la generación viva" — mapea al rollback.
    Recovery,
}

/// Un nodo del grafo de arranque: un estado del sistema al que se puede arrancar.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BootNode {
    /// `of_tree` (BLAKE3 hex, sin `b3:`) del árbol de la generación. Content-addressed y estable.
    pub id: String,
    /// Texto corto para el usuario.
    pub label: String,
    pub kind: NodeKind,
    /// Instante del apply en RFC 3339 (UTC).
    pub created: String,
    /// Ids de los padres (DAG). Hoy 0 o 1 por nodo; el formato admite varios.
    pub parents: Vec<String>,
    /// `true` si el nodo se puede arrancar (siempre hoy; el flag existe para estados futuros parciales).
    pub bootable: bool,
    /// Qué cambió respecto del padre (opcional).
    #[serde(skip_serializing_if = "Option::is_none")]
    pub summary: Option<String>,
}

/// El grafo entero — exactamente el JSON del contrato del ADR 0010.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BootGraph {
    pub version: u32,
    /// `id` del nodo vivo (la generación actual). Cadena **vacía** si el sistema nunca aplicó un
    /// upgrade (no hay generación registrada) — el lector de mirada exige el campo presente, así que
    /// se emite siempre y degrada a "sin preselección" ante `""`.
    #[serde(default)]
    pub current: String,
    /// `id` a arrancar si el usuario no elige. Vacío si no hay generación viva (ver [`current`]).
    #[serde(default)]
    pub default: String,
    pub nodes: Vec<BootNode>,
}

/// El `id` content-addressed de un nodo: el `of_tree` sin el prefijo `b3:`.
fn node_id(tree_content: &str) -> String {
    tree_content.trim_start_matches("b3:").to_string()
}

/// Id estable del nodo sintético de recuperación. Content-addressed (BLAKE3 de una etiqueta fija) para
/// respetar el contrato "cada id es un hash", pero reservado: [`activate`] lo reconoce y hace rollback.
pub fn recovery_id() -> String {
    node_id(ArtifactHash::of_bytes(b"hammer:boot:recovery").as_str())
}

/// Resume los cambios de una generación para el `summary` (+añadidos ~reemplazados -retirados).
fn summarize(m: &GenerationManifest) -> Option<String> {
    if m.changes.is_empty() {
        return None;
    }
    let (mut a, mut r, mut d) = (0u32, 0u32, 0u32);
    for c in &m.changes {
        match c.op {
            crate::ChangeOp::Added => a += 1,
            crate::ChangeOp::Replaced => r += 1,
            crate::ChangeOp::Removed => d += 1,
        }
    }
    Some(format!("+{a} ~{r} -{d}"))
}

/// El nombre legible del árbol: la cola de `tree_dir` (`<hash>-<name>` ⇒ `<name>`).
fn tree_name(tree_dir: &str) -> &str {
    tree_dir.split_once('-').map_or(tree_dir, |(_, name)| name)
}

/// Convierte segundos-desde-epoch a RFC 3339 (UTC) sin dependencias externas
/// (algoritmo `civil_from_days` de Howard Hinnant). Determinista y testeable.
fn rfc3339(secs: u64) -> String {
    let days = (secs / 86_400) as i64;
    let rem = secs % 86_400;
    let (h, mi, s) = (rem / 3600, (rem % 3600) / 60, rem % 60);

    let z = days + 719_468;
    let era = if z >= 0 { z } else { z - 146_096 } / 146_097;
    let doe = z - era * 146_097; // [0, 146096]
    let yoe = (doe - doe / 1460 + doe / 36_524 - doe / 146_096) / 365; // [0, 399]
    let y = yoe + era * 400;
    let doy = doe - (365 * yoe + yoe / 4 - yoe / 100); // [0, 365]
    let mp = (5 * doy + 2) / 153; // [0, 11]
    let d = doy - (153 * mp + 2) / 5 + 1; // [1, 31]
    let m = if mp < 10 { mp + 3 } else { mp - 9 }; // [1, 12]
    let year = if m <= 2 { y + 1 } else { y };
    format!("{year:04}-{m:02}-{d:02}T{h:02}:{mi:02}:{s:02}Z")
}

/// Arma el [`BootGraph`] a partir del registro de generaciones en `state_root`.
///
/// Cada generación es un nodo (id = `of_tree`); su padre es el `of_tree` de la generación anterior.
/// La primera generación del linaje (sin padre) es [`NodeKind::Base`]. Si hay una generación viva, se
/// añade un nodo sintético [`NodeKind::Recovery`] colgando de ella ("deshacer la generación viva").
pub fn build(state_root: &Path) -> Result<BootGraph> {
    let gens = list(state_root)?;
    let cur = current(state_root)?;

    // Índice u64 → of_tree, para traducir `parent` (u64) al id del padre (hash).
    let by_gen: HashMap<u64, &GenerationManifest> = gens.iter().map(|m| (m.id, m)).collect();
    let hash_of = |gen: u64| -> Option<String> { by_gen.get(&gen).map(|m| node_id(&m.tree_content)) };

    let mut nodes: Vec<BootNode> = Vec::with_capacity(gens.len() + 1);
    for m in &gens {
        let parents = m.parent.and_then(hash_of).into_iter().collect::<Vec<_>>();
        let kind = if m.parent.is_none() { NodeKind::Base } else { NodeKind::Generation };
        nodes.push(BootNode {
            id: node_id(&m.tree_content),
            label: format!("gen {} · {}", m.id, tree_name(&m.tree_dir)),
            kind,
            created: rfc3339(m.created_at),
            parents,
            bootable: true,
            summary: summarize(m),
        });
    }

    let current_id = cur.and_then(hash_of);

    // Nodo de recuperación: sólo tiene sentido si hay algo vivo que deshacer.
    if let Some(cur_hash) = &current_id {
        let created = cur.and_then(|g| by_gen.get(&g)).map_or(0, |m| m.created_at);
        nodes.push(BootNode {
            id: recovery_id(),
            label: "recuperación (deshacer la generación viva)".to_string(),
            kind: NodeKind::Recovery,
            created: rfc3339(created),
            parents: vec![cur_hash.clone()],
            bootable: true,
            summary: Some("rollback a la generación anterior".to_string()),
        });
    }

    // `current`/`default` van siempre presentes (String); vacío = "sin generación viva".
    let current_id = current_id.unwrap_or_default();
    Ok(BootGraph { version: 1, current: current_id.clone(), default: current_id, nodes })
}

/// Escribe el grafo en `out` (default [`BOOT_GRAPH_PATH`]) de forma atómica (temporal + rename).
/// Crea el directorio padre (`/run/hammer`) si falta.
pub fn emit(state_root: &Path, out: &Path) -> Result<()> {
    let graph = build(state_root)?;
    let bytes = serde_json::to_vec_pretty(&graph)?;
    if let Some(dir) = out.parent() {
        std::fs::create_dir_all(dir)?;
    }
    let tmp = out.with_extension("json.tmp");
    std::fs::write(&tmp, &bytes)?;
    std::fs::rename(&tmp, out)?;
    Ok(())
}

/// Reporte de una [`activate`].
#[derive(Debug, Clone)]
pub struct ActivateReport {
    /// Id (hash) que se pidió activar.
    pub target: String,
    pub kind: NodeKind,
    /// `true` si el nodo pedido ya era el vivo (no se hizo nada).
    pub already_current: bool,
    /// Generaciones revertidas, de la más nueva a la más vieja (vacío si no hubo rollback).
    pub rolled_back: Vec<u64>,
}

/// Activa el nodo `id` del grafo: deja el sistema en ese estado.
///
/// - Si `id` es el vivo ⇒ no-op ([`ActivateReport::already_current`]).
/// - Si `id` es un **ancestro** de la generación viva ⇒ rollback paso a paso hasta él (cada paso es un
///   [`rollback`] atómico del modelo E4).
/// - Si `id` es el nodo de **recuperación** ⇒ un rollback de la generación viva.
/// - Si `id` existe pero **no** está en la cadena viva (un "redo" hacia una generación huérfana) ⇒
///   error honesto: eso requiere re-aplicar el árbol con `hammer upgrade apply`.
pub fn activate(
    target_root: &Path,
    state_root: &Path,
    journal: Option<&Journal>,
    id: &str,
) -> Result<ActivateReport> {
    let want = node_id(id); // tolera que llegue con o sin `b3:`

    // Recuperación: un rollback de la generación viva.
    if want == recovery_id() {
        let RollbackReport { reverted, .. } = rollback(target_root, state_root, journal)?;
        return Ok(ActivateReport {
            target: want,
            kind: NodeKind::Recovery,
            already_current: false,
            rolled_back: vec![reverted],
        });
    }

    let cur = current(state_root)?
        .ok_or_else(|| Error::Other("no hay generación viva a la que anclar la activación".into()))?;
    let cur_manifest = crate::get(state_root, cur)?
        .ok_or_else(|| Error::Other(format!("manifiesto de la generación viva {cur} ausente")))?;

    // ¿Ya es la viva?
    if node_id(&cur_manifest.tree_content) == want {
        return Ok(ActivateReport {
            target: want,
            kind: NodeKind::Generation,
            already_current: true,
            rolled_back: Vec::new(),
        });
    }

    // Buscar el nodo pedido dentro de la cadena viva (ancestros de la generación actual). El índice en
    // la cadena (0 = actual) es exactamente cuántos rollbacks hay que hacer para llegar a él.
    let chain = live_chain(state_root)?;
    let mut steps: Option<usize> = None;
    for (i, gen) in chain.iter().enumerate() {
        let m = crate::get(state_root, *gen)?
            .ok_or_else(|| Error::Other(format!("manifiesto de la generación {gen} ausente")))?;
        if node_id(&m.tree_content) == want {
            steps = Some(i);
            break;
        }
    }

    let steps = steps.ok_or_else(|| {
        Error::Other(format!(
            "el nodo {want} no es un ancestro de la generación viva; \
             activar hacia adelante (redo) aún no se soporta — re-aplicá el árbol con `hammer upgrade apply`"
        ))
    })?;

    let mut rolled_back = Vec::with_capacity(steps);
    for _ in 0..steps {
        let RollbackReport { reverted, .. } = rollback(target_root, state_root, journal)?;
        rolled_back.push(reverted);
    }

    Ok(ActivateReport {
        target: want,
        kind: NodeKind::Generation,
        already_current: false,
        rolled_back,
    })
}

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

    #[test]
    fn rfc3339_epoch_and_known_dates() {
        assert_eq!(rfc3339(0), "1970-01-01T00:00:00Z");
        // 2026-07-06T12:00:00Z
        assert_eq!(rfc3339(1_783_339_200), "2026-07-06T12:00:00Z");
        // 2000-01-01T00:00:00Z (post-leap-year boundary)
        assert_eq!(rfc3339(946_684_800), "2000-01-01T00:00:00Z");
    }

    #[test]
    fn recovery_id_is_stable_and_hex() {
        let a = recovery_id();
        assert_eq!(a, recovery_id());
        assert_eq!(a.len(), 64); // BLAKE3 hex, sin `b3:`
        assert!(a.chars().all(|c| c.is_ascii_hexdigit()));
    }

    #[test]
    fn empty_state_emits_present_string_fields() {
        // Sistema sin upgrades: el grafo debe traer `current`/`default` PRESENTES (cadena vacía),
        // no omitidos — el lector de mirada los exige como String obligatorio.
        let tmp = tempfile::tempdir().unwrap();
        let g = build(tmp.path()).unwrap();
        assert!(g.nodes.is_empty());
        assert_eq!(g.current, "");
        assert_eq!(g.default, "");
        let json = serde_json::to_string(&g).unwrap();
        assert!(json.contains("\"current\""), "current debe emitirse aun vacío: {json}");
        assert!(json.contains("\"default\""), "default debe emitirse aun vacío: {json}");
    }

    #[test]
    fn tree_name_strips_hash_prefix() {
        assert_eq!(tree_name("abc123-product-rootfs"), "product-rootfs");
        assert_eq!(tree_name("noseparator"), "noseparator");
    }
}
