//! Firma Ed25519 del `.swm` y `TrustStore` local. Ver `docs/09-trust-model.md` §3 y §5.
//!
//! La firma cubre **el contenido del manifiesto** (`swm_version` + `base` + `mutations`), no la
//! propia firma. Sirve para autoría e integridad en tránsito — **no** sustituye a la
//! verificación reproducible: `hammer apply` siempre reproduce y compara, firme o no.
//!
//! Formato en disco (texto, una línea base64 cada uno):
//!   - clave privada: `<name>.ed25519`      → base64(semilla de 32 bytes), modo 0600
//!   - clave pública: `<name>.ed25519.pub`  → base64(clave pública de 32 bytes)

use std::collections::BTreeMap;
use std::path::Path;

use base64::Engine;
use ed25519_dalek::{Signer, SigningKey, Verifier, VerifyingKey, SECRET_KEY_LENGTH};
use serde::Serialize;

use crate::swm::{Base, Mutation, Signature, Swm};

const ALG: &str = "ed25519";
const PRIV_EXT: &str = ".ed25519";
const PUB_EXT: &str = ".ed25519.pub";

fn b64() -> base64::engine::GeneralPurpose {
    base64::engine::general_purpose::STANDARD
}

/// Estado de la firma de un `.swm` respecto a un [`TrustStore`].
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum SigStatus {
    /// Firma válida y la clave del autor está en el TrustStore.
    Trusted { by: String },
    /// Firma presente pero el autor no está entre las claves de confianza locales.
    UnknownKey { by: String },
    /// Hay firma pero no valida (contenido alterado, clave equivocada, o `alg` no soportado).
    BadSig { by: String, reason: String },
    /// El `.swm` no trae firma.
    Unsigned,
}

impl SigStatus {
    /// ¿La firma es válida y de un autor confiado? Recuerda: NO implica permiso para promover
    /// (eso depende de la verificación reproducible + el `commit` humano).
    pub fn is_trusted(&self) -> bool {
        matches!(self, SigStatus::Trusted { .. })
    }

    /// Etiqueta corta para UI/logs.
    pub fn label(&self) -> &'static str {
        match self {
            SigStatus::Trusted { .. } => "trusted",
            SigStatus::UnknownKey { .. } => "unknown-key",
            SigStatus::BadSig { .. } => "bad-sig",
            SigStatus::Unsigned => "unsigned",
        }
    }
}

/// Par de claves Ed25519 envoltorio fino sobre `ed25519-dalek`.
pub struct KeyPair {
    signing: SigningKey,
}

impl KeyPair {
    /// Genera un par nuevo con entropía del sistema (`getrandom`).
    pub fn generate() -> crate::Result<KeyPair> {
        let mut seed = [0u8; SECRET_KEY_LENGTH];
        getrandom::getrandom(&mut seed)
            .map_err(|e| crate::Error::Other(anyhow::anyhow!("getrandom: {e}")))?;
        Ok(KeyPair {
            signing: SigningKey::from_bytes(&seed),
        })
    }

    /// La clave pública en base64 (lo que va a `<name>.ed25519.pub`).
    pub fn public_b64(&self) -> String {
        b64().encode(self.signing.verifying_key().to_bytes())
    }

    /// La clave privada (semilla) en base64. Trátala como secreto.
    pub fn private_b64(&self) -> String {
        b64().encode(self.signing.to_bytes())
    }

    /// Carga una clave privada desde su forma base64 (contenido de `<name>.ed25519`).
    pub fn from_private_b64(s: &str) -> crate::Result<KeyPair> {
        let bytes = b64()
            .decode(s.trim())
            .map_err(|e| crate::Error::Other(anyhow::anyhow!("clave privada base64: {e}")))?;
        let seed: [u8; SECRET_KEY_LENGTH] = bytes.as_slice().try_into().map_err(|_| {
            crate::Error::Other(anyhow::anyhow!(
                "clave privada: se esperaban {SECRET_KEY_LENGTH} bytes, hay {}",
                bytes.len()
            ))
        })?;
        Ok(KeyPair {
            signing: SigningKey::from_bytes(&seed),
        })
    }

    /// Escribe ambos ficheros en `dir`: `<name>.ed25519` (0600) y `<name>.ed25519.pub`.
    /// Devuelve la ruta de la clave pública.
    pub fn write_to(&self, dir: &Path, name: &str) -> crate::Result<std::path::PathBuf> {
        std::fs::create_dir_all(dir)?;
        let priv_path = dir.join(format!("{name}{PRIV_EXT}"));
        let pub_path = dir.join(format!("{name}{PUB_EXT}"));
        std::fs::write(&priv_path, format!("{}\n", self.private_b64()))?;
        std::fs::write(&pub_path, format!("{}\n", self.public_b64()))?;
        // La privada es secreta: rwx para el dueño solamente.
        use std::os::unix::fs::PermissionsExt;
        std::fs::set_permissions(&priv_path, std::fs::Permissions::from_mode(0o600))?;
        Ok(pub_path)
    }

    /// Firma un `.swm` y devuelve la `Signature` (autoría `by`).
    pub fn sign(&self, swm: &Swm, by: &str) -> Signature {
        self.sign_raw(&signing_bytes(swm), by)
    }

    /// Firma bytes canónicos arbitrarios. Es el primitivo que comparten la firma del `.swm` y la
    /// firma de un índice de repo (release): el llamador decide qué bytes canónicos firmar.
    pub fn sign_raw(&self, msg: &[u8], by: &str) -> Signature {
        let sig = self.signing.sign(msg);
        Signature {
            by: by.to_string(),
            alg: ALG.to_string(),
            sig: b64().encode(sig.to_bytes()),
        }
    }
}

/// Verifica una `Signature` sobre bytes canónicos arbitrarios contra un [`TrustStore`]. El
/// primitivo compartido por `Swm::verify_signature` y `RepoIndex::verify_signature`.
pub fn verify_raw(msg: &[u8], sig: &Signature, trust: &TrustStore) -> SigStatus {
    if sig.alg != ALG {
        return SigStatus::BadSig {
            by: sig.by.clone(),
            reason: format!("alg no soportado: {} (esperado {ALG})", sig.alg),
        };
    }
    let vk = match trust.get(&sig.by) {
        None => return SigStatus::UnknownKey { by: sig.by.clone() },
        Some(vk) => vk,
    };
    let raw = match b64().decode(sig.sig.trim()) {
        Ok(r) => r,
        Err(e) => {
            return SigStatus::BadSig {
                by: sig.by.clone(),
                reason: format!("firma base64: {e}"),
            }
        }
    };
    let bytes: [u8; 64] = match raw.as_slice().try_into() {
        Ok(b) => b,
        Err(_) => {
            return SigStatus::BadSig {
                by: sig.by.clone(),
                reason: format!("firma: se esperaban 64 bytes, hay {}", raw.len()),
            }
        }
    };
    let signature = ed25519_dalek::Signature::from_bytes(&bytes);
    match vk.verify(msg, &signature) {
        Ok(()) => SigStatus::Trusted { by: sig.by.clone() },
        Err(_) => SigStatus::BadSig {
            by: sig.by.clone(),
            reason: "la firma no corresponde al contenido firmado".into(),
        },
    }
}

/// Claves públicas en las que el usuario confía para *autoría*. Ver `docs/09-trust-model.md` §5.
#[derive(Debug, Default)]
pub struct TrustStore {
    keys: BTreeMap<String, VerifyingKey>,
}

impl TrustStore {
    pub fn new() -> TrustStore {
        TrustStore::default()
    }

    /// Carga todas las claves `*.ed25519.pub` de un directorio (p. ej.
    /// `/var/lib/hammer/trust/`). El nombre del autor es el del fichero sin sufijo. Un
    /// directorio inexistente da un store vacío (no es error: nadie confiado todavía).
    pub fn load(dir: &Path) -> crate::Result<TrustStore> {
        let mut store = TrustStore::new();
        let rd = match std::fs::read_dir(dir) {
            Ok(rd) => rd,
            Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(store),
            Err(e) => return Err(e.into()),
        };
        for entry in rd {
            let entry = entry?;
            let fname = entry.file_name();
            let fname = fname.to_string_lossy();
            if let Some(name) = fname.strip_suffix(PUB_EXT) {
                let content = std::fs::read_to_string(entry.path())?;
                let vk = parse_public_b64(content.trim()).map_err(|e| {
                    crate::Error::Other(anyhow::anyhow!("{}: {e}", entry.path().display()))
                })?;
                store.keys.insert(name.to_string(), vk);
            }
        }
        Ok(store)
    }

    /// Añade/reemplaza una clave de confianza desde su base64.
    pub fn insert_b64(&mut self, name: &str, public_b64: &str) -> crate::Result<()> {
        let vk = parse_public_b64(public_b64.trim())
            .map_err(|e| crate::Error::Other(anyhow::anyhow!(e)))?;
        self.keys.insert(name.to_string(), vk);
        Ok(())
    }

    pub fn get(&self, name: &str) -> Option<&VerifyingKey> {
        self.keys.get(name)
    }

    pub fn len(&self) -> usize {
        self.keys.len()
    }

    pub fn is_empty(&self) -> bool {
        self.keys.is_empty()
    }
}

fn parse_public_b64(s: &str) -> Result<VerifyingKey, String> {
    let bytes = b64().decode(s).map_err(|e| format!("clave pública base64: {e}"))?;
    let arr: [u8; 32] = bytes
        .as_slice()
        .try_into()
        .map_err(|_| format!("clave pública: se esperaban 32 bytes, hay {}", bytes.len()))?;
    VerifyingKey::from_bytes(&arr).map_err(|e| format!("clave pública inválida: {e}"))
}

/// Bytes canónicos firmados: el contenido del manifiesto SIN la firma, en JSON estable.
/// `base.pins` es un `BTreeMap` (orden por clave) y el resto son campos de orden fijo, así que
/// firmante y verificador obtienen exactamente los mismos bytes.
fn signing_bytes(swm: &Swm) -> Vec<u8> {
    #[derive(Serialize)]
    struct Signable<'a> {
        swm_version: u32,
        base: &'a Base,
        mutations: &'a [Mutation],
    }
    let signable = Signable {
        swm_version: swm.swm_version,
        base: &swm.base,
        mutations: &swm.mutations,
    };
    // serde_json sobre tipos sin floats ni mapas no ordenados ⇒ salida determinista.
    serde_json::to_vec(&signable).expect("Signable siempre serializa")
}

impl Swm {
    /// Verifica la firma del manifiesto contra un [`TrustStore`]. Ver SDD 09 §5.
    pub fn verify_signature(&self, trust: &TrustStore) -> SigStatus {
        match &self.signature {
            None => SigStatus::Unsigned,
            Some(sig) => verify_raw(&signing_bytes(self), sig, trust),
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::swm::{Base, Mutation, Swm};

    fn demo_swm() -> Swm {
        Swm {
            swm_version: 1,
            base: Base {
                distro_version: "2026-06-06".into(),
                pins: [("grep".to_string(), "a1b2".to_string())].into_iter().collect(),
            },
            mutations: vec![Mutation::ConfigEdit {
                file: "/etc/network.conf".into(),
                inline_diff: "- DHCP=yes\n+ IP=10.0.0.1\n".into(),
            }],
            signature: None,
        }
    }

    #[test]
    fn sign_then_verify_trusted() {
        let kp = KeyPair::generate().unwrap();
        let mut swm = demo_swm();
        swm.signature = Some(kp.sign(&swm, "sergio"));

        let mut trust = TrustStore::new();
        trust.insert_b64("sergio", &kp.public_b64()).unwrap();

        assert_eq!(
            swm.verify_signature(&trust),
            SigStatus::Trusted { by: "sergio".into() }
        );
    }

    #[test]
    fn unsigned_is_unsigned() {
        assert_eq!(demo_swm().verify_signature(&TrustStore::new()), SigStatus::Unsigned);
    }

    #[test]
    fn unknown_key() {
        let kp = KeyPair::generate().unwrap();
        let mut swm = demo_swm();
        swm.signature = Some(kp.sign(&swm, "desconocido"));
        // TrustStore vacío ⇒ autor desconocido.
        match swm.verify_signature(&TrustStore::new()) {
            SigStatus::UnknownKey { by } => assert_eq!(by, "desconocido"),
            other => panic!("esperaba UnknownKey, obtuve {other:?}"),
        }
    }

    #[test]
    fn tampered_content_is_bad_sig() {
        let kp = KeyPair::generate().unwrap();
        let mut swm = demo_swm();
        swm.signature = Some(kp.sign(&swm, "sergio"));

        // Alteramos el contenido DESPUÉS de firmar.
        if let Mutation::ConfigEdit { inline_diff, .. } = &mut swm.mutations[0] {
            *inline_diff = "- DHCP=yes\n+ IP=10.0.0.2\n".into(); // .1 → .2
        }

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

    #[test]
    fn wrong_key_is_bad_sig() {
        let kp = KeyPair::generate().unwrap();
        let other = KeyPair::generate().unwrap();
        let mut swm = demo_swm();
        swm.signature = Some(kp.sign(&swm, "sergio"));

        // El TrustStore tiene a "sergio" pero con OTRA clave pública.
        let mut trust = TrustStore::new();
        trust.insert_b64("sergio", &other.public_b64()).unwrap();
        assert!(matches!(
            swm.verify_signature(&trust),
            SigStatus::BadSig { .. }
        ));
    }

    #[test]
    fn keypair_roundtrip_through_disk() {
        let dir = tempfile::tempdir().unwrap();
        let kp = KeyPair::generate().unwrap();
        kp.write_to(dir.path(), "sergio").unwrap();

        // Recarga privada y verifica que firma con la misma identidad.
        let priv_s = std::fs::read_to_string(dir.path().join("sergio.ed25519")).unwrap();
        let loaded = KeyPair::from_private_b64(&priv_s).unwrap();
        assert_eq!(loaded.public_b64(), kp.public_b64());

        // La pública en disco carga en un TrustStore por nombre de fichero.
        let trust = TrustStore::load(dir.path()).unwrap();
        assert_eq!(trust.len(), 1);
        assert!(trust.get("sergio").is_some());

        // Permisos 0600 en la privada.
        use std::os::unix::fs::PermissionsExt;
        let mode = std::fs::metadata(dir.path().join("sergio.ed25519"))
            .unwrap()
            .permissions()
            .mode()
            & 0o777;
        assert_eq!(mode, 0o600);
    }

    #[test]
    fn load_missing_dir_is_empty() {
        let t = TrustStore::load(Path::new("/no/existe/aqui")).unwrap();
        assert!(t.is_empty());
    }

    #[test]
    fn signing_bytes_ignore_signature_field() {
        // Firmar no debe depender del campo signature: dos SWM idénticos salvo signature
        // producen los mismos bytes firmados.
        let mut a = demo_swm();
        let b = demo_swm();
        a.signature = Some(Signature {
            by: "x".into(),
            alg: "ed25519".into(),
            sig: "AAAA".into(),
        });
        assert_eq!(signing_bytes(&a), signing_bytes(&b));
    }
}
