"""
Cliente Ollama de Leonex.

Wrapper minimalista del API HTTP local de Ollama (http://localhost:11434).
Cero dependencias externas — usa solo urllib de la stdlib.

Ollama corre modelos LLM 100% en local: sin tokens, sin internet, sin coste.
Solo necesitas el ejecutable instalado y un modelo descargado.

Setup recomendado para Leonex (PC con 8GB RAM):
    1. Descarga e instala Ollama desde https://ollama.com/download
    2. En PowerShell: ollama pull llama3.2:3b      (modelo ~2GB)
    3. Test: ollama run llama3.2:3b "Hola"
    4. Ollama ya escucha en localhost:11434 automaticamente

Modelos sugeridos por tamano:
    llama3.2:3b           ~2 GB  rapido, ligero, suficiente para explicaciones
    qwen2.5:3b            ~2 GB  alternativa, mejor en español
    phi3.5:3.8b           ~2 GB  Microsoft, bueno en razonamiento
    qwen2.5-coder:7b      ~4.5 GB mejor en codigo (no necesario para explicar DAGs)
    llama3.1:8b           ~4.7 GB mas potente pero come 6GB RAM

Uso programatico:
    from ollama_client import is_available, generate
    if is_available():
        text = generate("llama3.2:3b", "Explicame el DAG: ...")
"""

from __future__ import annotations

import json
import socket
import urllib.error
import urllib.request
from typing import Optional


DEFAULT_BASE_URL = "http://localhost:11434"
DEFAULT_MODEL = "llama3.2:3b"
DEFAULT_TIMEOUT_S = 120        # generate puede tardar segundos a minutos


def _http_post(url: str, payload: dict, timeout: int = DEFAULT_TIMEOUT_S) -> Optional[dict]:
    data = json.dumps(payload).encode("utf-8")
    req = urllib.request.Request(
        url, data=data, method="POST",
        headers={"Content-Type": "application/json"},
    )
    try:
        with urllib.request.urlopen(req, timeout=timeout) as resp:
            body = resp.read().decode("utf-8", errors="replace")
            return json.loads(body)
    except (urllib.error.URLError, socket.timeout, json.JSONDecodeError, OSError):
        return None


def _http_get(url: str, timeout: int = 5) -> Optional[dict]:
    try:
        with urllib.request.urlopen(url, timeout=timeout) as resp:
            body = resp.read().decode("utf-8", errors="replace")
            return json.loads(body)
    except (urllib.error.URLError, socket.timeout, json.JSONDecodeError, OSError):
        return None


def is_available(base_url: str = DEFAULT_BASE_URL) -> bool:
    """True si Ollama responde en la URL local."""
    return _http_get(f"{base_url}/api/tags", timeout=3) is not None


def list_models(base_url: str = DEFAULT_BASE_URL) -> list[str]:
    """Lista modelos disponibles localmente (los que ya hayas descargado con `ollama pull`)."""
    data = _http_get(f"{base_url}/api/tags", timeout=5)
    if not data:
        return []
    return [m.get("name", "") for m in data.get("models", []) if m.get("name")]


def has_model(model: str, base_url: str = DEFAULT_BASE_URL) -> bool:
    """True si el modelo concreto esta disponible (descargado)."""
    models = list_models(base_url)
    return any(m == model or m.startswith(model + ":") or m == f"{model}:latest" for m in models)


def generate(model: str, prompt: str,
             base_url: str = DEFAULT_BASE_URL,
             timeout: int = DEFAULT_TIMEOUT_S,
             system: Optional[str] = None,
             temperature: float = 0.3) -> Optional[str]:
    """
    Genera texto con un modelo local. Devuelve la respuesta como str, o None si falla.

    Parametros:
        model: nombre del modelo (ej. "llama3.2:3b").
        prompt: texto del usuario.
        system: prompt de sistema opcional (instrucciones de rol).
        temperature: 0.0 = deterministico, 1.0 = creativo. Default 0.3.
    """
    payload = {
        "model": model,
        "prompt": prompt,
        "stream": False,
        "options": {"temperature": temperature},
    }
    if system:
        payload["system"] = system
    data = _http_post(f"{base_url}/api/generate", payload, timeout=timeout)
    if data is None:
        return None
    return data.get("response")


def pull_model(model: str, base_url: str = DEFAULT_BASE_URL,
               timeout: int = 1800) -> bool:
    """
    Descarga un modelo si no esta presente. Bloqueante. Devuelve True si OK.

    `ollama pull` desde la CLI es generalmente mas conveniente que esto.
    """
    payload = {"name": model, "stream": False}
    data = _http_post(f"{base_url}/api/pull", payload, timeout=timeout)
    return data is not None and data.get("status") == "success"


def detect_environment() -> dict:
    """Devuelve diagnostico rapido del estado de Ollama para el dashboard."""
    available = is_available()
    info = {
        "available": available,
        "base_url": DEFAULT_BASE_URL,
        "models": [],
        "recommended_model": DEFAULT_MODEL,
        "has_recommended": False,
    }
    if available:
        info["models"] = list_models()
        info["has_recommended"] = has_model(DEFAULT_MODEL)
    return info


if __name__ == "__main__":
    import sys
    env = detect_environment()
    print(f"Ollama base_url   : {env['base_url']}")
    print(f"Ollama disponible : {env['available']}")
    if env["available"]:
        print(f"Modelos locales   : {env['models'] or '(ninguno)'}")
        print(f"Modelo recomendado: {env['recommended_model']} "
              f"{'(presente)' if env['has_recommended'] else '(NO descargado)'}")
        if env["has_recommended"]:
            print()
            print("Test de generacion...")
            txt = generate(env["recommended_model"],
                           "En una frase, ¿que es el algoritmo PC de Spirtes-Glymour?")
            if txt:
                print("Respuesta:", txt.strip()[:300])
            else:
                print("[X] generacion fallo")
    else:
        print()
        print("Ollama NO esta corriendo. Para activarlo:")
        print("  1. Descarga e instala https://ollama.com/download")
        print("  2. En PowerShell: ollama pull llama3.2:3b")
        print("  3. Vuelve a ejecutar este script")
