//! Traductor LLM real contra la Claude API (anthropic-version `2023-06-01`).
//!
//! Implementa [`IntentTranslator`] exactamente como el `MockTranslator`, así que el bucle
//! del [`Orchestrator`](crate::Orchestrator) no cambia. Lo que ofrece de extra es que el
//! contenido de la intención NL llega a un modelo real (Claude Opus 4.8 por defecto) que
//! produce el `.swm` directamente — no se requiere catálogo pre-armado.
//!
//! Diseño:
//!
//! - **Síncrono** y mínimo: usamos `ureq` (gated detrás del feature `llm-claude`) porque
//!   el cliente del bus también es síncrono. Tokio entraría sólo si el resto del crate lo
//!   pidiera.
//! - **HTTP inyectable**: el trait [`HttpClient`] permite tests sin red contra una fake
//!   que devuelve respuestas pre-armadas y verifica el cuerpo enviado.
//! - **Modelo por defecto**: `claude-opus-4-8`, con adaptive thinking activado y
//!   `effort: "high"`. Cambiar el modelo es modificar el `ClaudeConfig`.
//! - **Schema enforcement**: el modelo recibe un system prompt que documenta el shape
//!   exacto del `Swm` con ejemplos; pedimos respuesta JSON pura. El parser local
//!   (`serde_json::from_str::<Swm>`) actúa como gate adicional, y `verify_schema()`
//!   se llama antes de devolver. Si el modelo divaga, el caller obtiene un
//!   `TranslateError::Model` con el cuerpo bruto para debugging.
//!
//! El bucle agéntico (con o sin auto-reparación) reusa el mismo `Orchestrator`. La única
//! diferencia es la construcción del traductor:
//!
//! ```ignore
//! let cfg = ClaudeConfig::from_env()?;
//! let t = ClaudeTranslator::new(cfg);
//! let orch = Orchestrator::new(t, base, apply, compile);
//! orch.run("instala grep con regex Perl por defecto")?;
//! ```

use crate::translator::{IntentTranslator, SystemContext, TranslateError};
use hammer_core::Swm;

/// Inyección HTTP. La implementación por defecto ([`UreqHttp`]) usa `ureq` síncrono;
/// los tests pasan una fake.
pub trait HttpClient: Send + Sync {
    fn post_json(
        &self,
        url: &str,
        headers: &[(&str, &str)],
        body: &str,
    ) -> Result<String, TranslateError>;
}

#[cfg(feature = "llm-claude")]
pub struct UreqHttp;

#[cfg(feature = "llm-claude")]
impl HttpClient for UreqHttp {
    fn post_json(
        &self,
        url: &str,
        headers: &[(&str, &str)],
        body: &str,
    ) -> Result<String, TranslateError> {
        let mut req = ureq::post(url);
        for (k, v) in headers {
            req = req.set(k, v);
        }
        let resp = req
            .send_string(body)
            .map_err(|e| TranslateError::Model(format!("http: {e}")))?;
        resp.into_string()
            .map_err(|e| TranslateError::Model(format!("body: {e}")))
    }
}

#[derive(Debug, Clone)]
pub struct ClaudeConfig {
    pub api_key: String,
    pub model: String,
    pub max_tokens: u32,
    pub base_url: String,
    /// Nivel de esfuerzo del modelo (`low|medium|high|max`). Default `high`.
    pub effort: String,
}

impl ClaudeConfig {
    /// Construye desde env vars: `ANTHROPIC_API_KEY` (obligatoria), `HAMMER_LLM_MODEL`,
    /// `HAMMER_LLM_BASE_URL`, `HAMMER_LLM_EFFORT`.
    pub fn from_env() -> Result<Self, TranslateError> {
        let api_key = std::env::var("ANTHROPIC_API_KEY").map_err(|_| {
            TranslateError::Model(
                "ANTHROPIC_API_KEY no está en el entorno; exporta tu key o usa MockTranslator"
                    .into(),
            )
        })?;
        Ok(Self {
            api_key,
            model: std::env::var("HAMMER_LLM_MODEL")
                .unwrap_or_else(|_| "claude-opus-4-8".into()),
            max_tokens: 8192,
            base_url: std::env::var("HAMMER_LLM_BASE_URL")
                .unwrap_or_else(|_| "https://api.anthropic.com".into()),
            effort: std::env::var("HAMMER_LLM_EFFORT").unwrap_or_else(|_| "high".into()),
        })
    }
}

pub struct ClaudeTranslator {
    cfg: ClaudeConfig,
    http: Box<dyn HttpClient>,
}

impl ClaudeTranslator {
    /// Constructor por defecto: usa [`UreqHttp`] (sólo disponible con feature `llm-claude`).
    #[cfg(feature = "llm-claude")]
    pub fn new(cfg: ClaudeConfig) -> Self {
        Self { cfg, http: Box::new(UreqHttp) }
    }

    /// Constructor con HTTP inyectado. Útil para tests y para apuntar a un proxy/mock.
    pub fn with_http(cfg: ClaudeConfig, http: Box<dyn HttpClient>) -> Self {
        Self { cfg, http }
    }
}

const SYSTEM_PROMPT: &str = r#"Eres un traductor que convierte intenciones humanas en lenguaje natural a manifiestos `.swm` (Software Mutación) del sistema hammer. El receptor parseará tu respuesta con serde_json (Rust) y rechazará cualquier output que no sea JSON puro válido.

# Shape del Swm (versión 1)

```json
{
  "swm_version": 1,
  "base": {
    "distro_version": "<copia el distro_version del SystemContext>",
    "pins": {}
  },
  "mutations": [ ... ]
}
```

# Variantes de mutación

1. config_edit — edita un archivo de texto vía unified diff.
   {"type":"config_edit","file":"/abs/path","inline_diff":"- old line\n+ new line\n"}
   - `file` debe ser ruta absoluta.
   - `inline_diff` usa el formato de diff unified clásico (líneas `-`, `+` y espacio).

2. file_drop — coloca un archivo en el sistema.
   {"type":"file_drop","path":"/abs/path","content_hash":"b3:<hex>","content_b64":"<base64>"}
   - `content_hash` es BLAKE3 del contenido descodificado, con el prefijo `b3:`.
   - SOLO emite `file_drop` si puedes calcular el hash correcto. Si no, salta esta variante.

3. source_patch — compila desde fuente y la inyecta.
   {"type":"source_patch","repo":"git://...","commit":"<sha>","build":{"compiler":"zig-cc","target":"x86_64-linux-musl","link":"static","flags":[]},"target_bin":"/usr/bin/foo"}
   - Origen git (repo+commit) O tarball (tarball+sha256), exactamente uno; p. ej.
     {"type":"source_patch","tarball":"https://ftp.gnu.org/gnu/grep/grep-3.11.tar.gz","sha256":"<hex>","build":{...},"target_bin":"/usr/bin/grep"}
   - `target_bin` ruta absoluta del binario producido.

4. init_rule — registra un servicio.
   {"type":"init_rule","action":"start","service":"nginx","command":"/usr/sbin/nginx -g 'daemon off;'"}

# Reglas

- TODAS las rutas son absolutas (comienzan con `/`).
- Copia `distro_version` y `pins` desde el SystemContext que te pase el usuario.
- Si la intención es ambigua o requiere información que no tienes (p. ej. el hash de un binario que no puedes computar), prefiere `config_edit` o `source_patch` sobre `file_drop`.
- Responde EXCLUSIVAMENTE con el JSON del Swm. Sin texto antes, sin texto después, sin markdown, sin backticks. La primera línea debe ser `{` y la última `}`.
"#;

impl IntentTranslator for ClaudeTranslator {
    fn translate(&self, intent: &str, ctx: &SystemContext) -> Result<Swm, TranslateError> {
        let body = build_request_body(&self.cfg, intent, ctx)?;
        let url = format!(
            "{}/v1/messages",
            self.cfg.base_url.trim_end_matches('/')
        );
        let auth = format!("{}", self.cfg.api_key);
        let headers: [(&str, &str); 3] = [
            ("x-api-key", auth.as_str()),
            ("anthropic-version", "2023-06-01"),
            ("content-type", "application/json"),
        ];
        let raw = self.http.post_json(&url, &headers, &body)?;
        parse_response(&raw)
    }
}

fn build_request_body(
    cfg: &ClaudeConfig,
    intent: &str,
    ctx: &SystemContext,
) -> Result<String, TranslateError> {
    let ctx_json = serde_json::json!({
        "distro_version": ctx.base.distro_version,
        "pins": ctx.base.pins,
        "extras": ctx.extras,
    });
    let user_msg = format!(
        "SystemContext:\n{}\n\nIntención del humano:\n{}\n\nRecuerda: responde sólo con el JSON del Swm.",
        serde_json::to_string_pretty(&ctx_json).unwrap_or_else(|_| "{}".into()),
        intent,
    );
    let body = serde_json::json!({
        "model": cfg.model,
        "max_tokens": cfg.max_tokens,
        "system": SYSTEM_PROMPT,
        "thinking": { "type": "adaptive" },
        "output_config": { "effort": cfg.effort },
        "messages": [
            { "role": "user", "content": user_msg }
        ]
    });
    serde_json::to_string(&body).map_err(|e| TranslateError::Model(format!("request body: {e}")))
}

fn parse_response(raw: &str) -> Result<Swm, TranslateError> {
    let v: serde_json::Value = serde_json::from_str(raw).map_err(|e| {
        TranslateError::Model(format!("respuesta no es JSON ({e}): {raw}"))
    })?;
    // Error envelope de la API: { "type": "error", "error": { "type": "...", "message": "..." } }
    if v.get("type").and_then(|t| t.as_str()) == Some("error") {
        let err = v
            .get("error")
            .map(|e| e.to_string())
            .unwrap_or_else(|| "error sin detalle".into());
        return Err(TranslateError::Model(format!("API: {err}")));
    }
    // stop_reason "refusal" ⇒ el modelo rechazó. Lo reportamos como Model.
    if v.get("stop_reason").and_then(|s| s.as_str()) == Some("refusal") {
        let det = v
            .get("stop_details")
            .map(|d| d.to_string())
            .unwrap_or_else(|| "sin stop_details".into());
        return Err(TranslateError::Model(format!("refusal: {det}")));
    }
    let text = extract_first_text(&v)?;
    let cleaned = strip_code_fence(&text);
    let swm: Swm = serde_json::from_str(cleaned).map_err(|e| {
        TranslateError::Model(format!(
            "JSON del modelo no parsea como Swm ({e}). Texto:\n{text}"
        ))
    })?;
    swm.verify_schema()
        .map_err(|e| TranslateError::Model(format!("schema del Swm: {e}")))?;
    Ok(swm)
}

fn extract_first_text(v: &serde_json::Value) -> Result<String, TranslateError> {
    let content = v
        .get("content")
        .and_then(|c| c.as_array())
        .ok_or_else(|| TranslateError::Model(format!("'content' ausente o no array: {v}")))?;
    for block in content {
        if block.get("type").and_then(|t| t.as_str()) == Some("text") {
            if let Some(s) = block.get("text").and_then(|t| t.as_str()) {
                return Ok(s.to_string());
            }
        }
    }
    Err(TranslateError::Model(format!(
        "no encontré un bloque de texto en content: {v}"
    )))
}

/// Limpia el típico envoltorio ```json ... ```. Best-effort: si no encuentra fence,
/// devuelve el texto sin cambios. También recorta whitespace de los extremos.
fn strip_code_fence(s: &str) -> &str {
    let trimmed = s.trim();
    let inside = trimmed
        .strip_prefix("```json")
        .or_else(|| trimmed.strip_prefix("```"))
        .map(|rest| {
            rest.trim_start_matches('\n')
                .trim_end_matches("```")
                .trim_end_matches('\n')
                .trim_end()
        })
        .unwrap_or(trimmed);
    inside.trim()
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::collections::BTreeMap;
    use std::sync::{Arc, Mutex};

    use hammer_core::BaseRef;

    /// Fake HTTP que captura el request y devuelve un body pre-armado.
    struct FakeHttp {
        captured: Arc<Mutex<Option<(String, Vec<(String, String)>, String)>>>,
        response: String,
    }

    impl HttpClient for FakeHttp {
        fn post_json(
            &self,
            url: &str,
            headers: &[(&str, &str)],
            body: &str,
        ) -> Result<String, TranslateError> {
            *self.captured.lock().unwrap() = Some((
                url.to_string(),
                headers.iter().map(|(k, v)| (k.to_string(), v.to_string())).collect(),
                body.to_string(),
            ));
            Ok(self.response.clone())
        }
    }

    fn cfg() -> ClaudeConfig {
        ClaudeConfig {
            api_key: "sk-test".into(),
            model: "claude-opus-4-8".into(),
            max_tokens: 4096,
            base_url: "https://api.anthropic.com".into(),
            effort: "high".into(),
        }
    }

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

    fn ok_response(text: &str) -> String {
        serde_json::json!({
            "id": "msg_test",
            "type": "message",
            "role": "assistant",
            "model": "claude-opus-4-8",
            "content": [{"type": "text", "text": text}],
            "stop_reason": "end_turn",
            "usage": {"input_tokens": 1, "output_tokens": 1}
        })
        .to_string()
    }

    fn swm_text() -> String {
        let s = serde_json::json!({
            "swm_version": 1,
            "base": {"distro_version": "2026-06-06", "pins": {}},
            "mutations": [{
                "type": "config_edit",
                "file": "/etc/network.conf",
                "inline_diff": "- DHCP=yes\n+ STATIC=1\n"
            }]
        });
        serde_json::to_string(&s).unwrap()
    }

    #[test]
    fn happy_path_emits_v1_messages_post_with_correct_headers() {
        let captured = Arc::new(Mutex::new(None));
        let http = FakeHttp {
            captured: captured.clone(),
            response: ok_response(&swm_text()),
        };
        let t = ClaudeTranslator::with_http(cfg(), Box::new(http));
        let swm = t.translate("set static IP", &empty_ctx()).unwrap();
        assert_eq!(swm.mutations.len(), 1);

        let (url, headers, body) = captured.lock().unwrap().clone().unwrap();
        assert_eq!(url, "https://api.anthropic.com/v1/messages");
        assert!(headers.iter().any(|(k, v)| k == "x-api-key" && v == "sk-test"));
        assert!(headers
            .iter()
            .any(|(k, v)| k == "anthropic-version" && v == "2023-06-01"));
        assert!(headers
            .iter()
            .any(|(k, v)| k == "content-type" && v == "application/json"));
        let parsed: serde_json::Value = serde_json::from_str(&body).unwrap();
        assert_eq!(parsed["model"], "claude-opus-4-8");
        assert_eq!(parsed["thinking"]["type"], "adaptive");
        assert_eq!(parsed["output_config"]["effort"], "high");
        let user_msg = parsed["messages"][0]["content"].as_str().unwrap();
        assert!(user_msg.contains("set static IP"), "{user_msg}");
        assert!(user_msg.contains("2026-06-06"), "{user_msg}");
    }

    #[test]
    fn unwraps_markdown_code_fence() {
        let fenced = format!("```json\n{}\n```", swm_text());
        let http = FakeHttp {
            captured: Arc::new(Mutex::new(None)),
            response: ok_response(&fenced),
        };
        let t = ClaudeTranslator::with_http(cfg(), Box::new(http));
        let swm = t.translate("x", &empty_ctx()).unwrap();
        assert_eq!(swm.mutations.len(), 1);
    }

    #[test]
    fn refusal_surfaces_as_model_error() {
        let resp = serde_json::json!({
            "id": "msg_x",
            "type": "message",
            "content": [],
            "stop_reason": "refusal",
            "stop_details": {"category": "cyber", "explanation": "no"},
            "usage": {"input_tokens": 1, "output_tokens": 0}
        });
        let http = FakeHttp {
            captured: Arc::new(Mutex::new(None)),
            response: resp.to_string(),
        };
        let t = ClaudeTranslator::with_http(cfg(), Box::new(http));
        let err = t.translate("x", &empty_ctx()).unwrap_err();
        match err {
            TranslateError::Model(m) => assert!(m.contains("refusal"), "{m}"),
            other => panic!("esperaba Model, llegó {other:?}"),
        }
    }

    #[test]
    fn api_error_envelope_is_reported() {
        let resp = serde_json::json!({
            "type": "error",
            "error": {"type": "invalid_request_error", "message": "bad model"}
        });
        let http = FakeHttp {
            captured: Arc::new(Mutex::new(None)),
            response: resp.to_string(),
        };
        let t = ClaudeTranslator::with_http(cfg(), Box::new(http));
        let err = t.translate("x", &empty_ctx()).unwrap_err();
        match err {
            TranslateError::Model(m) => assert!(m.contains("invalid_request_error"), "{m}"),
            other => panic!("esperaba Model, llegó {other:?}"),
        }
    }

    #[test]
    fn malformed_swm_surfaces_as_model_error() {
        let resp = ok_response("{\"not\": \"a swm\"}");
        let http = FakeHttp {
            captured: Arc::new(Mutex::new(None)),
            response: resp,
        };
        let t = ClaudeTranslator::with_http(cfg(), Box::new(http));
        let err = t.translate("x", &empty_ctx()).unwrap_err();
        match err {
            TranslateError::Model(m) => assert!(m.contains("Swm"), "{m}"),
            other => panic!("esperaba Model, llegó {other:?}"),
        }
    }

    #[test]
    fn schema_verify_catches_bad_swm() {
        // swm_version inválida: el JSON parsea pero verify_schema rechaza.
        let bad = serde_json::json!({
            "swm_version": 9,
            "base": {"distro_version": "x", "pins": {}},
            "mutations": []
        });
        let http = FakeHttp {
            captured: Arc::new(Mutex::new(None)),
            response: ok_response(&bad.to_string()),
        };
        let t = ClaudeTranslator::with_http(cfg(), Box::new(http));
        let err = t.translate("x", &empty_ctx()).unwrap_err();
        match err {
            TranslateError::Model(m) => assert!(m.contains("swm_version"), "{m}"),
            other => panic!("esperaba Model, llegó {other:?}"),
        }
    }

    #[test]
    fn strip_code_fence_handles_variants() {
        assert_eq!(strip_code_fence("```json\n{\"a\":1}\n```"), "{\"a\":1}");
        assert_eq!(strip_code_fence("```\n{\"a\":1}\n```"), "{\"a\":1}");
        assert_eq!(strip_code_fence("{\"a\":1}"), "{\"a\":1}");
        assert_eq!(strip_code_fence("  {\"a\":1}\n\n"), "{\"a\":1}");
    }
}
