//! El **repositorio de paquetes** (Etapa F): un directorio con `.swm` + un `index.json` que
//! mapea `nombre → paquete`. Es el catálogo que `hammer install <nombre>` resuelve.
//!
//! Un `.swm` es un manifiesto de mutación sin identidad propia (no lleva un campo "nombre de
//! paquete"); la **identidad** (nombre+versión) la asigna el repo cuando se publica. Esto
//! mantiene el formato honesto (describe una transformación, no se arroga ser "el paquete X")
//! y deja al repo ser el namespace. `hammer pack --repo` publica con el nombre/versión de la
//! receta; `install` resuelve por nombre.
//!
//! El índice es CONTENIDO PLANO (serde_json), no un binario: legible, diffeable, firmable más
//! adelante como una release. El transporte del repo (filesystem / sshfs / mirror) es ortogonal
//! — ver `hammer-mirror` para el CAS del store; aquí el repo es simplemente un directorio.

use std::path::{Path, PathBuf};

use serde::{Deserialize, Serialize};

use crate::sign::{verify_raw, KeyPair, SigStatus, TrustStore};
use crate::swm::Signature;

/// Nombre canónico del índice dentro del directorio del repo.
pub const INDEX_FILE: &str = "index.json";

/// El índice del repo: la lista de paquetes publicados. Un nombre aparece una sola vez
/// (la última publicación gana, ver [`RepoIndex::upsert`]).
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct RepoIndex {
    #[serde(default)]
    pub packages: Vec<PackageEntry>,
    /// Firma del CATÁLOGO entero (release): cubre la lista de paquetes, no cada `.swm` por
    /// separado. Firmar el índice ancla qué paquetes existen, sus versiones y sus hashes — un
    /// atacante no puede añadir/quitar/intercambiar entradas sin invalidar la firma. La firma de
    /// cada `.swm` (autoría del paquete) es ortogonal y sigue existiendo.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub signature: Option<Signature>,
}

/// Una entrada del índice: la identidad de un paquete + el puntero a su `.swm` y los metadatos
/// que `install` necesita para decidir ANTES de leer el `.swm` (base compatible, autoría).
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct PackageEntry {
    pub name: String,
    pub version: String,
    /// Ruta del `.swm` relativa al directorio del repo.
    pub file: String,
    /// `base.distro_version` del `.swm` — permite filtrar incompatibles sin abrir el manifiesto.
    pub distro_version: String,
    /// Build-deps del paquete por NOMBRE (de la receta original) — las que el receptor debe tener
    /// como paquetes en el repo para reproducir. `install` resuelve este grafo desde el índice sin
    /// abrir cada `.swm`. Vacío ⇒ paquete autocontenido.
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub deps: Vec<String>,
    /// `expected_hash` del `source_patch` (si lo lleva): el ancla "verificar, no confiar".
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub expected_hash: Option<String>,
    /// Autor de la firma del `.swm` (si está firmado). `install --trust` lo valida de verdad
    /// contra la clave; este campo es sólo el metadato legible del índice.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub signed_by: Option<String>,
}

impl RepoIndex {
    /// Carga el índice del repo. Un directorio sin `index.json` es un repo VACÍO válido (no es
    /// error) — así `pack --repo <dir-nuevo>` arranca un repo desde cero.
    pub fn load(repo_dir: impl AsRef<Path>) -> crate::Result<RepoIndex> {
        let path = repo_dir.as_ref().join(INDEX_FILE);
        match std::fs::read_to_string(&path) {
            Ok(text) => serde_json::from_str(&text)
                .map_err(|e| crate::Error::Serde(format!("{}: {e}", path.display()))),
            Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(RepoIndex::default()),
            Err(e) => Err(crate::Error::Io(e)),
        }
    }

    /// Escribe el índice (creando el directorio del repo si hace falta). JSON con saltos para
    /// que el repo sea diffeable.
    pub fn save(&self, repo_dir: impl AsRef<Path>) -> crate::Result<()> {
        let dir = repo_dir.as_ref();
        std::fs::create_dir_all(dir)?;
        let text = serde_json::to_string_pretty(self)
            .map_err(|e| crate::Error::Serde(e.to_string()))?;
        std::fs::write(dir.join(INDEX_FILE), text)?;
        Ok(())
    }

    /// Resuelve un nombre a su entrada (la última publicada).
    pub fn find(&self, name: &str) -> Option<&PackageEntry> {
        self.packages.iter().find(|p| p.name == name)
    }

    /// Inserta o reemplaza la entrada de un nombre (publicar es idempotente por nombre: una
    /// nueva versión del mismo paquete sustituye a la anterior en el índice). Devuelve el
    /// `.swm` viejo que quedó huérfano, si la ruta cambió (para que el caller lo borre).
    pub fn upsert(&mut self, entry: PackageEntry) -> Option<String> {
        // Cualquier cambio al catálogo invalida una firma de release previa: hay que re-firmar.
        self.signature = None;
        if let Some(slot) = self.packages.iter_mut().find(|p| p.name == entry.name) {
            let old_file = slot.file.clone();
            let orphan = (old_file != entry.file).then_some(old_file);
            *slot = entry;
            orphan
        } else {
            self.packages.push(entry);
            self.packages.sort_by(|a, b| a.name.cmp(&b.name));
            None
        }
    }

    /// Ruta absoluta del `.swm` de una entrada, relativa al directorio del repo.
    pub fn swm_path(repo_dir: impl AsRef<Path>, entry: &PackageEntry) -> PathBuf {
        repo_dir.as_ref().join(&entry.file)
    }

    /// Bytes canónicos que firma el release: SÓLO la lista de paquetes (excluye la propia firma).
    /// `PackageEntry` es todo strings/vecs en orden fijo y `packages` está ordenado por nombre
    /// (ver `upsert`), así que firmante y verificador obtienen los mismos bytes.
    fn signing_bytes(&self) -> Vec<u8> {
        serde_json::to_vec(&self.packages).expect("Vec<PackageEntry> siempre serializa")
    }

    /// Firma el catálogo con `kp` (autoría `by`). Reemplaza cualquier firma previa.
    pub fn sign(&mut self, kp: &KeyPair, by: &str) {
        let sig = kp.sign_raw(&self.signing_bytes(), by);
        self.signature = Some(sig);
    }

    /// Verifica la firma del catálogo contra un [`TrustStore`]. `Unsigned` si no hay firma.
    pub fn verify_signature(&self, trust: &TrustStore) -> SigStatus {
        match &self.signature {
            None => SigStatus::Unsigned,
            Some(sig) => verify_raw(&self.signing_bytes(), sig, trust),
        }
    }

    /// Resuelve el cierre transitivo de build-deps de `name` en orden TOPOLÓGICO (cada dep antes
    /// que quien la necesita; `name` queda al final). Es el orden en que `install` debe procesar:
    /// poblar el catálogo con las deps antes de construir el dependiente. Error si una dep falta
    /// del índice (no se puede reproducir sin ella) o si hay un ciclo.
    pub fn resolve_closure<'a>(&'a self, name: &str) -> crate::Result<Vec<&'a PackageEntry>> {
        let mut order: Vec<&'a PackageEntry> = Vec::new();
        let mut done: Vec<&'a str> = Vec::new(); // ya emitidos
        let mut stack: Vec<&'a str> = Vec::new(); // en la rama DFS actual (detección de ciclo)
        self.visit(name, &mut order, &mut done, &mut stack)?;
        Ok(order)
    }

    fn visit<'a>(
        &'a self,
        name: &str,
        order: &mut Vec<&'a PackageEntry>,
        done: &mut Vec<&'a str>,
        stack: &mut Vec<&'a str>,
    ) -> crate::Result<()> {
        if done.iter().any(|d| *d == name) {
            return Ok(());
        }
        if stack.iter().any(|s| *s == name) {
            return Err(crate::Error::Recipe(format!(
                "ciclo de dependencias en el repo: {} → {name}",
                stack.join(" → ")
            )));
        }
        let entry = self.find(name).ok_or_else(|| {
            crate::Error::Recipe(format!(
                "dependencia '{name}' no está en el repo (necesaria por {})",
                stack.last().copied().unwrap_or("(raíz)")
            ))
        })?;
        // Guardamos refs con la vida de `self` (no del parámetro `name`): el `.swm` y su nombre
        // viven en el índice. `name` sólo sirve para buscar y comparar.
        stack.push(entry.name.as_str());
        for dep in &entry.deps {
            self.visit(dep, order, done, stack)?;
        }
        stack.pop();
        done.push(entry.name.as_str());
        order.push(entry);
        Ok(())
    }
}

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

    fn entry(name: &str, ver: &str) -> PackageEntry {
        entry_deps(name, ver, &[])
    }

    fn entry_deps(name: &str, ver: &str, deps: &[&str]) -> PackageEntry {
        PackageEntry {
            name: name.into(),
            version: ver.into(),
            file: format!("{name}-{ver}.swm"),
            distro_version: "dev".into(),
            expected_hash: None,
            signed_by: None,
            deps: deps.iter().map(|s| s.to_string()).collect(),
        }
    }

    #[test]
    fn load_missing_index_is_empty_repo() {
        let d = tempfile::tempdir().unwrap();
        let idx = RepoIndex::load(d.path()).unwrap();
        assert!(idx.packages.is_empty());
    }

    #[test]
    fn save_load_roundtrip_and_find() {
        let d = tempfile::tempdir().unwrap();
        let mut idx = RepoIndex::default();
        idx.upsert(entry("ripgrep", "14.1.1"));
        idx.upsert(entry("findutils", "0.9.1"));
        idx.save(d.path()).unwrap();

        let back = RepoIndex::load(d.path()).unwrap();
        assert_eq!(back.packages.len(), 2);
        assert_eq!(back.find("ripgrep").unwrap().version, "14.1.1");
        assert!(back.find("nope").is_none());
        // Orden estable (alfabético) ⇒ índice diffeable.
        assert_eq!(back.packages[0].name, "findutils");
    }

    #[test]
    fn upsert_replaces_and_reports_orphan() {
        let mut idx = RepoIndex::default();
        idx.upsert(entry("rg", "1.0.0"));
        // Nueva versión ⇒ archivo distinto ⇒ el viejo queda huérfano.
        let orphan = idx.upsert(entry("rg", "2.0.0"));
        assert_eq!(orphan.as_deref(), Some("rg-1.0.0.swm"));
        assert_eq!(idx.packages.len(), 1, "no duplica por nombre");
        assert_eq!(idx.find("rg").unwrap().version, "2.0.0");
    }

    #[test]
    fn upsert_same_file_no_orphan() {
        let mut idx = RepoIndex::default();
        idx.upsert(entry("rg", "1.0.0"));
        let orphan = idx.upsert(entry("rg", "1.0.0"));
        assert!(orphan.is_none());
    }

    fn names<'a>(v: &[&'a PackageEntry]) -> Vec<&'a str> {
        v.iter().map(|e| e.name.as_str()).collect()
    }

    #[test]
    fn closure_topological_deps_before_dependents() {
        // openssh → zlib, openssl ; openssl no tiene deps. Orden: deps antes, target al final.
        let mut idx = RepoIndex::default();
        idx.upsert(entry("zlib", "1.3"));
        idx.upsert(entry("openssl", "3.5"));
        idx.upsert(entry_deps("openssh", "10.3", &["zlib", "openssl"]));
        let order = names(&idx.resolve_closure("openssh").unwrap());
        // zlib y openssl preceden a openssh; openssh es el último.
        assert_eq!(*order.last().unwrap(), "openssh");
        let p = |n| order.iter().position(|x| *x == n).unwrap();
        assert!(p("zlib") < p("openssh") && p("openssl") < p("openssh"));
    }

    #[test]
    fn closure_dedups_diamond() {
        // a→b,c ; b→d ; c→d. d aparece una sola vez, antes que b y c.
        let mut idx = RepoIndex::default();
        idx.upsert(entry("d", "1"));
        idx.upsert(entry_deps("b", "1", &["d"]));
        idx.upsert(entry_deps("c", "1", &["d"]));
        idx.upsert(entry_deps("a", "1", &["b", "c"]));
        let order = names(&idx.resolve_closure("a").unwrap());
        assert_eq!(order.iter().filter(|x| **x == "d").count(), 1);
        let p = |n| order.iter().position(|x| *x == n).unwrap();
        assert!(p("d") < p("b") && p("d") < p("c") && p("b") < p("a"));
    }

    #[test]
    fn closure_missing_dep_errors() {
        let mut idx = RepoIndex::default();
        idx.upsert(entry_deps("openssh", "10.3", &["openssl"])); // openssl ausente
        let err = idx.resolve_closure("openssh").unwrap_err().to_string();
        assert!(err.contains("openssl") && err.contains("no está en el repo"), "{err}");
    }

    #[test]
    fn closure_cycle_errors() {
        let mut idx = RepoIndex::default();
        idx.upsert(entry_deps("a", "1", &["b"]));
        idx.upsert(entry_deps("b", "1", &["a"]));
        let err = idx.resolve_closure("a").unwrap_err().to_string();
        assert!(err.contains("ciclo"), "{err}");
    }

    #[test]
    fn release_sign_then_verify_trusted() {
        let kp = crate::sign::KeyPair::generate().unwrap();
        let mut idx = RepoIndex::default();
        idx.upsert(entry("rg", "14.1.1"));
        idx.upsert(entry("findutils", "0.9.1"));
        idx.sign(&kp, "sergio");

        let mut trust = TrustStore::new();
        trust.insert_b64("sergio", &kp.public_b64()).unwrap();
        assert_eq!(idx.verify_signature(&trust), SigStatus::Trusted { by: "sergio".into() });
    }

    #[test]
    fn release_signature_survives_save_load() {
        let d = tempfile::tempdir().unwrap();
        let kp = crate::sign::KeyPair::generate().unwrap();
        let mut idx = RepoIndex::default();
        idx.upsert(entry("rg", "14.1.1"));
        idx.sign(&kp, "sergio");
        idx.save(d.path()).unwrap();

        let back = RepoIndex::load(d.path()).unwrap();
        let mut trust = TrustStore::new();
        trust.insert_b64("sergio", &kp.public_b64()).unwrap();
        assert!(back.verify_signature(&trust).is_trusted());
    }

    #[test]
    fn upsert_invalidates_release_signature() {
        let kp = crate::sign::KeyPair::generate().unwrap();
        let mut idx = RepoIndex::default();
        idx.upsert(entry("rg", "1"));
        idx.sign(&kp, "sergio");
        assert!(idx.signature.is_some());
        // Publicar otro paquete invalida la firma (catálogo cambió ⇒ re-firmar).
        idx.upsert(entry("fd", "1"));
        assert!(idx.signature.is_none(), "upsert debe invalidar la firma de release");
    }

    #[test]
    fn release_tamper_is_bad_sig() {
        let kp = crate::sign::KeyPair::generate().unwrap();
        let mut idx = RepoIndex::default();
        idx.upsert(entry("rg", "1"));
        idx.sign(&kp, "sergio");
        // Manipular una entrada SIN re-firmar (p. ej. cambiar el hash esperado de un paquete).
        idx.packages[0].version = "666".into();

        let mut trust = TrustStore::new();
        trust.insert_b64("sergio", &kp.public_b64()).unwrap();
        assert!(matches!(idx.verify_signature(&trust), SigStatus::BadSig { .. }));
    }
}
