"""
Módulo de observabilidad LLM con Langfuse.

Diseño fail-safe:
- Si LANGFUSE_ENABLED=false o el paquete no está instalado → no-op puro, cero overhead.
- Si el servidor Langfuse no responde → logging de advertencia, nunca bloquea la app.
- Si el paquete está instalado pero la config es incorrecta → warning, no excepción.

Uso desde providers.py:
    from .langfuse_client import record_generation
    record_generation(trace_name="ops_plan", model="qwen2.5", ...)

Uso desde orchestrator.py:
    from .langfuse_client import start_trace, finish_trace
    trace_id = start_trace("create_command_plan", {"text": text[:200]})
    ...
    finish_trace(trace_id, output=result)
"""

from __future__ import annotations

import logging
import time
from contextvars import ContextVar
from typing import Any

logger = logging.getLogger(__name__)

# Estado global del módulo (singleton liviano)
_client: Any = None
_enabled: bool = False

# ContextVar async-safe: cada corutina tiene su propio trace_id
# (no hay colisiones entre requests concurrentes)
_current_trace_id: ContextVar[str | None] = ContextVar("langfuse_trace_id", default=None)


def set_current_trace_id(trace_id: str | None) -> None:
    """Establece el trace_id activo para la corutina actual."""
    _current_trace_id.set(trace_id)


def get_current_trace_id() -> str | None:
    """Devuelve el trace_id activo para la corutina actual."""
    return _current_trace_id.get()


# ─── Setup ────────────────────────────────────────────────────────────────────

def setup_langfuse(
    enabled: bool,
    public_key: str,
    secret_key: str,
    host: str,
) -> None:
    """Inicializa el cliente Langfuse. Llamar en startup de la app."""
    global _client, _enabled

    if not enabled:
        logger.info("Langfuse deshabilitado (LANGFUSE_ENABLED=false)")
        return

    if not public_key or not secret_key:
        logger.warning(
            "Langfuse habilitado pero LANGFUSE_PUBLIC_KEY o LANGFUSE_SECRET_KEY "
            "están vacías — observabilidad LLM desactivada."
        )
        return

    try:
        from langfuse import Langfuse  # noqa: PLC0415

        _client = Langfuse(
            public_key=public_key,
            secret_key=secret_key,
            host=host,
        )
        _enabled = True
        logger.info("Langfuse conectado → %s", host)
    except ImportError:
        logger.warning(
            "Paquete 'langfuse' no instalado — ejecuta: pip install langfuse>=2.0,<3.0"
        )
    except Exception as exc:
        logger.warning("Langfuse init fallido (no-fatal): %s", exc)


def teardown_langfuse() -> None:
    """Flush de eventos pendientes en shutdown. Llamar en shutdown de la app."""
    if _client is not None:
        try:
            _client.flush()
            logger.info("Langfuse flush OK en shutdown")
        except Exception as exc:
            logger.debug("Langfuse flush en shutdown fallido (no-fatal): %s", exc)


def is_enabled() -> bool:
    return _enabled and _client is not None


# ─── Trazas de alto nivel (por operación del orchestrator) ───────────────────

def start_trace(
    name: str,
    input_data: dict[str, Any] | None = None,
    metadata: dict[str, Any] | None = None,
    tags: list[str] | None = None,
) -> str | None:
    """
    Crea una traza Langfuse para una operación completa (p.ej. 'create_command_plan').
    Devuelve el trace_id o None si Langfuse está desactivado.
    """
    if not is_enabled():
        return None
    try:
        trace = _client.trace(
            name=name,
            input=input_data or {},
            metadata=metadata or {},
            tags=tags or [],
        )
        return trace.id
    except Exception as exc:
        logger.debug("Langfuse start_trace fallido (no-fatal): %s", exc)
        return None


def finish_trace(
    trace_id: str | None,
    output: Any = None,
    status: str = "SUCCESS",
) -> None:
    """Actualiza la traza con el output final."""
    if not is_enabled() or trace_id is None:
        return
    try:
        _client.trace(id=trace_id, output=output, status_message=status)
    except Exception as exc:
        logger.debug("Langfuse finish_trace fallido (no-fatal): %s", exc)


# ─── Generaciones LLM (por llamada al provider) ─────────────────────────────

def record_generation(
    *,
    trace_name: str,
    model: str,
    provider_name: str,
    system_prompt: str,
    user_prompt: str,
    output: str,
    latency_ms: float,
    input_tokens: int | None = None,
    output_tokens: int | None = None,
    trace_id: str | None = None,
    metadata: dict[str, Any] | None = None,
) -> None:
    """
    Registra una generación LLM en Langfuse.

    Pensado para ser llamado desde providers.py tras cada complete().
    Si trace_id se pasa, la generación queda enlazada a esa traza padre.
    """
    if not is_enabled():
        return
    try:
        # Si hay trace_id padre, anidar la generación dentro; sino, crear traza nueva
        if trace_id:
            parent = _client.trace(id=trace_id)
            generation_parent = parent
        else:
            generation_parent = _client.trace(
                name=trace_name,
                input={"system": system_prompt[:1500], "user": user_prompt[:1500]},
                metadata=metadata or {},
            )

        usage_payload: dict[str, Any] = {}
        if input_tokens is not None:
            usage_payload["input"] = input_tokens
        if output_tokens is not None:
            usage_payload["output"] = output_tokens
        if input_tokens is not None and output_tokens is not None:
            usage_payload["total"] = input_tokens + output_tokens

        generation_parent.generation(
            name=f"{provider_name}/{model}",
            model=model,
            input=[
                {"role": "system", "content": system_prompt[:1500]},
                {"role": "user", "content": user_prompt[:1500]},
            ],
            output=output[:3000],
            usage=usage_payload if usage_payload else None,
            metadata={
                "latency_ms": round(latency_ms, 1),
                "provider": provider_name,
                **(metadata or {}),
            },
        )
    except Exception as exc:
        logger.debug("Langfuse record_generation fallido (no-fatal): %s", exc)


# ─── Scores / feedback para evals ────────────────────────────────────────────

def score_generation(
    *,
    trace_id: str,
    score_name: str,
    value: float,
    comment: str | None = None,
) -> None:
    """
    Registra un score numérico sobre una traza (p.ej. feedback del operador).
    value: float 0.0–1.0 (Langfuse lo normaliza internamente)
    """
    if not is_enabled():
        return
    try:
        _client.score(
            trace_id=trace_id,
            name=score_name,
            value=value,
            comment=comment,
        )
    except Exception as exc:
        logger.debug("Langfuse score_generation fallido (no-fatal): %s", exc)


# ─── Utilidad: summary para el dashboard ─────────────────────────────────────

def get_summary() -> dict[str, Any]:
    """Devuelve el estado del cliente para el dashboard y /stackia de Telegram."""
    return {
        "enabled": _enabled,
        "connected": _client is not None,
        "package_available": _is_package_available(),
    }


def _is_package_available() -> bool:
    try:
        import langfuse  # noqa: F401, PLC0415
        return True
    except ImportError:
        return False
