//! Traductor de intención (NL) → `.swm`. Ver `docs/08-ai-integration.md` §5.
//!
//! Contrato del trait: dado un texto humano + un `SystemContext` (lo que el orquestador
//! pudo recabar vía QUERY: pins, herramientas presentes, distro_version, etc.), producir un
//! `Swm` válido.
//!
//! Phase 6 entrega un [`MockTranslator`] que mapea intentos exactos contra un catálogo
//! YAML de SWMs pre-armados. Eso permite probar el bucle plan→build→try→verify→propose en
//! tests y en el host del autor sin LLM. La integración con un modelo real (Claude API,
//! local llama, etc.) se conecta detrás de este mismo trait — el bucle no cambia.

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

use hammer_core::{BaseRef, Swm};

#[derive(Debug, thiserror::Error)]
pub enum TranslateError {
    #[error("intent no reconocida en el catálogo: '{0}'")]
    UnknownIntent(String),
    #[error("io: {0}")]
    Io(#[from] std::io::Error),
    #[error("catálogo inválido: {0}")]
    Catalog(String),
    #[error("modelo: {0}")]
    Model(String),
}

/// Lo que el orquestador le pasa al traductor para contextualizar la intención. Un LLM
/// usaría esto para decidir, p. ej., "¿el sistema ya tiene grep 3.12? entonces no necesito
/// recompilar; sólo edito la config". Para el `MockTranslator` es decorativo.
#[derive(Debug, Clone)]
pub struct SystemContext {
    pub base: BaseRef,
    /// Información libre que el orquestador haya recolectado (rutas y metadatos de archivos
    /// referenciados por el intent, salida de QUERY, etc.). Estructura JSON arbitraria.
    pub extras: serde_json::Value,
}

pub trait IntentTranslator {
    fn translate(&self, intent: &str, ctx: &SystemContext) -> Result<Swm, TranslateError>;
}

/// Catálogo intento→.swm leído de un YAML:
///
/// ```yaml
/// version: 1
/// intents:
///   - intent: "set static IP 192.168.1.100"
///     swm_path: "swms/static-ip.swm"   # ruta relativa al directorio del catálogo
///     # OR
///     swm_inline: { swm_version: 1, ... }
/// ```
///
/// El propósito es tener un "test cassette": el agente humano captura una intención que la
/// IA real ya resolvió, congela el `.swm` resultante, y a partir de ahí los CI/tests del
/// bucle agéntico son reproducibles.
#[derive(Debug, Clone, serde::Deserialize)]
pub struct IntentCatalog {
    #[serde(default = "default_version")]
    pub version: u32,
    pub intents: Vec<CatalogEntry>,
}

#[derive(Debug, Clone, serde::Deserialize)]
pub struct CatalogEntry {
    pub intent: String,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub swm_path: Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub swm_inline: Option<Swm>,
}

fn default_version() -> u32 {
    1
}

impl IntentCatalog {
    pub fn load_from_path(path: &Path) -> Result<Self, TranslateError> {
        let text = std::fs::read_to_string(path)?;
        let cat: IntentCatalog = serde_yaml::from_str(&text)
            .map_err(|e| TranslateError::Catalog(e.to_string()))?;
        if cat.version != 1 {
            return Err(TranslateError::Catalog(format!(
                "version del catálogo no soportada: {} (esperada 1)",
                cat.version
            )));
        }
        Ok(cat)
    }

    /// Resuelve un `intent` exacto contra el catálogo. Las entradas `swm_path` se resuelven
    /// relativas al `base_dir` (típicamente el directorio del catálogo).
    pub fn resolve(&self, intent: &str, base_dir: &Path) -> Result<Swm, TranslateError> {
        let entry = self
            .intents
            .iter()
            .find(|e| e.intent == intent)
            .ok_or_else(|| TranslateError::UnknownIntent(intent.into()))?;
        if let Some(s) = &entry.swm_inline {
            return Ok(s.clone());
        }
        if let Some(p) = &entry.swm_path {
            let full = base_dir.join(p);
            let text = std::fs::read_to_string(&full)?;
            return Swm::from_yaml(&text)
                .map_err(|e| TranslateError::Catalog(format!("swm {}: {e}", full.display())));
        }
        Err(TranslateError::Catalog(format!(
            "entry '{intent}' no declara swm_path ni swm_inline"
        )))
    }
}

/// Traductor offline: matchea la intención exacta contra un mapa precargado.
#[derive(Debug, Default)]
pub struct MockTranslator {
    map: HashMap<String, Swm>,
}

impl MockTranslator {
    pub fn empty() -> Self {
        Self { map: HashMap::new() }
    }

    pub fn from_catalog(cat: &IntentCatalog, base_dir: &Path) -> Result<Self, TranslateError> {
        let mut map = HashMap::new();
        for entry in &cat.intents {
            let swm = match (&entry.swm_inline, &entry.swm_path) {
                (Some(s), None) => s.clone(),
                (None, Some(p)) => {
                    let full = base_dir.join(p);
                    let text = std::fs::read_to_string(&full)?;
                    Swm::from_yaml(&text).map_err(|e| {
                        TranslateError::Catalog(format!("swm {}: {e}", full.display()))
                    })?
                }
                _ => {
                    return Err(TranslateError::Catalog(format!(
                        "entry '{}' debe declarar exactamente uno de swm_inline/swm_path",
                        entry.intent
                    )));
                }
            };
            map.insert(entry.intent.clone(), swm);
        }
        Ok(Self { map })
    }

    pub fn with_intent(mut self, intent: impl Into<String>, swm: Swm) -> Self {
        self.map.insert(intent.into(), swm);
        self
    }
}

impl IntentTranslator for MockTranslator {
    fn translate(&self, intent: &str, _ctx: &SystemContext) -> Result<Swm, TranslateError> {
        self.map
            .get(intent)
            .cloned()
            .ok_or_else(|| TranslateError::UnknownIntent(intent.into()))
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use hammer_core::swm::{Base, Mutation};
    use std::collections::BTreeMap;

    fn empty_ctx() -> SystemContext {
        SystemContext {
            base: BaseRef {
                distro_version: "2026-06-06".into(),
                pins: BTreeMap::new(),
            },
            extras: serde_json::Value::Null,
        }
    }

    fn swm_demo() -> Swm {
        Swm {
            swm_version: 1,
            base: Base { distro_version: "2026-06-06".into(), pins: BTreeMap::new() },
            mutations: vec![Mutation::ConfigEdit {
                file: "/etc/network.conf".into(),
                inline_diff: "- DHCP=yes\n+ STATIC=1\n".into(),
            }],
            signature: None,
        }
    }

    #[test]
    fn mock_translator_returns_known_swm() {
        let t = MockTranslator::empty().with_intent("set static ip", swm_demo());
        let s = t.translate("set static ip", &empty_ctx()).unwrap();
        assert_eq!(s.mutations.len(), 1);
    }

    #[test]
    fn mock_translator_rejects_unknown() {
        let t = MockTranslator::empty();
        let err = t.translate("paint the wall green", &empty_ctx()).unwrap_err();
        assert!(matches!(err, TranslateError::UnknownIntent(_)));
    }

    #[test]
    fn catalog_load_inline() {
        let d = tempfile::tempdir().unwrap();
        let cat_path = d.path().join("catalog.yaml");
        std::fs::write(
            &cat_path,
            r#"
version: 1
intents:
  - intent: "set static ip"
    swm_inline:
      swm_version: 1
      base:
        distro_version: "2026-06-06"
      mutations:
        - type: config_edit
          file: "/etc/network.conf"
          inline_diff: "- DHCP=yes\n+ STATIC=1\n"
"#,
        )
        .unwrap();
        let cat = IntentCatalog::load_from_path(&cat_path).unwrap();
        let t = MockTranslator::from_catalog(&cat, d.path()).unwrap();
        let s = t.translate("set static ip", &empty_ctx()).unwrap();
        assert_eq!(s.mutations.len(), 1);
    }

    #[test]
    fn catalog_load_from_swm_path() {
        let d = tempfile::tempdir().unwrap();
        let swm_path = d.path().join("net.swm");
        std::fs::write(&swm_path, swm_demo().to_yaml().unwrap()).unwrap();
        let cat_path = d.path().join("catalog.yaml");
        std::fs::write(
            &cat_path,
            r#"
version: 1
intents:
  - intent: "set static ip"
    swm_path: "net.swm"
"#,
        )
        .unwrap();
        let cat = IntentCatalog::load_from_path(&cat_path).unwrap();
        let t = MockTranslator::from_catalog(&cat, d.path()).unwrap();
        let s = t.translate("set static ip", &empty_ctx()).unwrap();
        assert_eq!(s.mutations.len(), 1);
    }

    #[test]
    fn catalog_rejects_unsupported_version() {
        let d = tempfile::tempdir().unwrap();
        let cat_path = d.path().join("catalog.yaml");
        std::fs::write(&cat_path, "version: 9\nintents: []\n").unwrap();
        let err = IntentCatalog::load_from_path(&cat_path).unwrap_err();
        assert!(matches!(err, TranslateError::Catalog(_)));
    }

    #[test]
    fn catalog_rejects_entry_without_swm() {
        let d = tempfile::tempdir().unwrap();
        let cat_path = d.path().join("catalog.yaml");
        std::fs::write(
            &cat_path,
            "version: 1\nintents:\n  - intent: empty\n",
        )
        .unwrap();
        let cat = IntentCatalog::load_from_path(&cat_path).unwrap();
        let err = MockTranslator::from_catalog(&cat, d.path()).unwrap_err();
        assert!(matches!(err, TranslateError::Catalog(_)));
    }
}
