"""
04 - Agente de 2 herramientas (soberano, modelo que cabe en GPU).

Mejoras sobre el 03:
  (C.1) System prompt afinado: se le dice cómo razonar sobre los resultados
        (p. ej. ante varias versiones, la más reciente es el número más alto)
        y se le pide que LEA la fuente antes de afirmar fechas/datos.
  (C.2) Segunda herramienta 'leer_pagina(url)': descarga el contenido real de
        una página. Así el modelo no depende de los títulos sueltos de la
        búsqueda; busca -> elige la mejor fuente -> la lee entera -> responde.

Modelo: qwen2.5-coder:14b-instruct-q4_K_M (~9 GB -> cabe entero en la GPU de
un M4 Pro de 24 GB, sin volcado a CPU, inferencia rápida).

Requisito: SearXNG levantado ->  cd searxng && docker compose up -d
"""

import json
import re

import requests
import ollama
from bs4 import BeautifulSoup

MODELO = "qwen2.5:14b-instruct-q4_K_M"  # generalista: tool calling nativo fiable
SEARXNG_URL = "http://localhost:8888/search"


# --- Herramienta 1: buscar -----------------------------------------------------

def buscar_web(query: str) -> str:
    print(f"   [buscar_web] '{query}'")
    resp = requests.get(SEARXNG_URL, params={"q": query, "format": "json"}, timeout=20)
    resp.raise_for_status()
    resultados = resp.json().get("results", [])[:5]
    if not resultados:
        return "No se encontraron resultados."
    return "\n\n".join(
        f"[{i+1}] {r.get('title','')}\n{r.get('content','')}\nURL: {r.get('url','')}"
        for i, r in enumerate(resultados)
    )


# --- Herramienta 2: leer el contenido real de una página -----------------------

def leer_pagina(url: str) -> str:
    print(f"   [leer_pagina] {url}")
    try:
        resp = requests.get(url, timeout=20, headers={"User-Agent": "Mozilla/5.0"})
        resp.raise_for_status()
    except Exception as e:
        return f"Error al descargar la página: {e}"
    soup = BeautifulSoup(resp.text, "lxml")
    for tag in soup(["script", "style", "nav", "footer", "header"]):
        tag.decompose()
    texto = " ".join(soup.get_text(separator=" ").split())
    return texto[:4000]  # se recorta para no saturar el contexto


FUNCIONES = {"buscar_web": buscar_web, "leer_pagina": leer_pagina}

TOOLS = [
    {
        "type": "function",
        "function": {
            "name": "buscar_web",
            "description": (
                "Busca en internet y devuelve una lista de resultados con título, "
                "resumen y URL. Úsala para localizar fuentes relevantes."
            ),
            "parameters": {
                "type": "object",
                "properties": {
                    "query": {"type": "string", "description": "Términos de búsqueda."}
                },
                "required": ["query"],
            },
        },
    },
    {
        "type": "function",
        "function": {
            "name": "leer_pagina",
            "description": (
                "Descarga y devuelve el texto real de una página web dada su URL. "
                "Úsala tras buscar, para leer la mejor fuente y confirmar datos "
                "concretos (fechas, versiones, cifras) antes de responder."
            ),
            "parameters": {
                "type": "object",
                "properties": {
                    "url": {"type": "string", "description": "URL de la página a leer."}
                },
                "required": ["url"],
            },
        },
    },
]

SYSTEM = (
    "Eres un asistente riguroso con acceso a internet mediante herramientas.\n"
    "Procedimiento:\n"
    "1. Si la pregunta trata de hechos recientes, versiones, fechas o cifras, "
    "usa buscar_web.\n"
    "2. No te fíes solo de los títulos/resúmenes: elige la fuente más fiable "
    "(prioriza sitios oficiales) y usa leer_pagina para leer su contenido real.\n"
    "3. Si aparecen varios números de versión, la MÁS RECIENTE es la de número "
    "más alto (p. ej. 3.14 es más nueva que 3.12).\n"
    "4. Responde de forma concreta y cita las URLs usadas.\n"
    "Nunca inventes fechas ni datos: si no los confirmaste leyendo, dilo."
)


def llamadas_fugadas_en_texto(contenido: str):
    """Algunos modelos (qwen, coder...) escriben la llamada como texto en vez de
    usar el campo tool_calls. Aquí se rescatan esos JSON {"name":..,"arguments":..}
    -con o sin etiquetas <tool_call>- para que el agente sea robusto."""
    if not contenido:
        return []
    llamadas = []
    for bloque in re.findall(r'\{[^{}]*"name"[^{}]*\{[^{}]*\}[^{}]*\}', contenido):
        try:
            obj = json.loads(bloque)
            if "name" in obj and "arguments" in obj:
                llamadas.append((obj["name"], obj["arguments"]))
        except json.JSONDecodeError:
            continue
    return llamadas


def responder(pregunta: str, max_turnos: int = 8):
    messages = [
        {"role": "system", "content": SYSTEM},
        {"role": "user", "content": pregunta},
    ]
    for _ in range(max_turnos):
        respuesta = ollama.chat(
            model=MODELO, messages=messages, tools=TOOLS,
            options={"temperature": 0},  # determinista, evita tokens basura
        )
        messages.append(respuesta.message)

        # 1) Llamadas por el canal correcto (campo estructurado tool_calls)
        pendientes = [
            (tc.function.name, tc.function.arguments)
            for tc in (respuesta.message.tool_calls or [])
        ]
        # 2) Si no hay, se rescatan llamadas fugadas al texto
        if not pendientes:
            pendientes = llamadas_fugadas_en_texto(respuesta.message.content)

        # Sin llamadas en ningún canal -> es la respuesta final
        if not pendientes:
            print("\nRESPUESTA FINAL:\n")
            print(respuesta.message.content)
            return

        for nombre, args in pendientes:
            funcion = FUNCIONES.get(nombre)
            resultado = (
                funcion(**args) if funcion
                else f"Error: herramienta desconocida '{nombre}'"
            )
            messages.append({"role": "tool", "name": nombre, "content": str(resultado)})
    print("Se alcanzó el máximo de turnos sin respuesta final.")


if __name__ == "__main__":
    responder("¿Qué versión estable de Python es la más reciente y cuándo salió?")
