"""
05 - Cuatro herramientas: ¿sabe el modelo ELEGIR la adecuada?

Se le dan 4 herramientas muy distintas y preguntas de distinta
naturaleza. Se observa qué herramienta(s) elige en cada caso. La traza
[--> usa: X] deja VER cada decisión del modelo.

Herramientas:
  - buscar_web(query)   -> internet (SearXNG local)
  - leer_pagina(url)    -> contenido real de una URL
  - calcular(expresion) -> aritmética, en local, sin red
  - fecha_y_hora()      -> fecha/hora del sistema, sin argumentos

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

import ast
import json
import operator
import re
from datetime import datetime

import requests
import ollama
from bs4 import BeautifulSoup

MODELO = "qwen2.5:14b-instruct-q4_K_M"
SEARXNG_URL = "http://localhost:8888/search"


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

def buscar_web(query: str) -> str:
    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 página ------------------------------------------------

def leer_pagina(url: str) -> str:
    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()
    return " ".join(soup.get_text(separator=" ").split())[:4000]


# --- Herramienta 3: calcular (evaluador aritmético seguro, sin eval) -----------

_OPS = {
    ast.Add: operator.add, ast.Sub: operator.sub, ast.Mult: operator.mul,
    ast.Div: operator.truediv, ast.Pow: operator.pow, ast.Mod: operator.mod,
    ast.USub: operator.neg,
}

def _eval(nodo):
    if isinstance(nodo, ast.Constant):
        return nodo.value
    if isinstance(nodo, ast.BinOp):
        return _OPS[type(nodo.op)](_eval(nodo.left), _eval(nodo.right))
    if isinstance(nodo, ast.UnaryOp):
        return _OPS[type(nodo.op)](_eval(nodo.operand))
    raise ValueError("expresión no permitida")

def calcular(expresion: str) -> str:
    try:
        return str(_eval(ast.parse(expresion, mode="eval").body))
    except Exception as e:
        return f"Error al calcular '{expresion}': {e}"


# --- Herramienta 4: fecha y hora -----------------------------------------------

def fecha_y_hora() -> str:
    return datetime.now().strftime("%Y-%m-%d %H:%M:%S (%A)")


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

TOOLS = [
    {"type": "function", "function": {
        "name": "buscar_web",
        "description": "Busca en internet y devuelve resultados (título, resumen, URL). "
                       "Úsala para hechos recientes, noticias, versiones, datos que no sepas.",
        "parameters": {"type": "object",
                       "properties": {"query": {"type": "string", "description": "Términos de búsqueda."}},
                       "required": ["query"]}}},
    {"type": "function", "function": {
        "name": "leer_pagina",
        "description": "Descarga el texto real de una página web dada su URL. Úsala tras buscar, "
                       "para confirmar datos concretos en la fuente.",
        "parameters": {"type": "object",
                       "properties": {"url": {"type": "string", "description": "URL a leer."}},
                       "required": ["url"]}}},
    {"type": "function", "function": {
        "name": "calcular",
        "description": "Evalúa una expresión aritmética (+,-,*,/,**,%) y devuelve el resultado. "
                       "Úsala SIEMPRE para cálculos numéricos en lugar de calcular tú mismo.",
        "parameters": {"type": "object",
                       "properties": {"expresion": {"type": "string",
                                       "description": "Expresión, ej. '1234 * 5678'."}},
                       "required": ["expresion"]}}},
    {"type": "function", "function": {
        "name": "fecha_y_hora",
        "description": "Devuelve la fecha y hora actuales del sistema. Úsala cuando necesites "
                       "saber 'hoy', 'ahora', el día de la semana o para cálculos con fechas.",
        "parameters": {"type": "object", "properties": {}}}},
]

SYSTEM = (
    "Eres un asistente riguroso con varias herramientas. Elige la adecuada a cada "
    "pregunta. No calcules ni adivines fechas de memoria: usa calcular y fecha_y_hora. "
    "Para hechos de internet usa buscar_web y confirma en la fuente con leer_pagina. "
    "Si una tarea requiere varios pasos, encadena varias herramientas. "
    "Responde de forma concreta."
)


def llamadas_fugadas_en_texto(contenido: str):
    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):
    print(f"\n{'='*70}\nPREGUNTA: {pregunta}\n{'='*70}")
    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},
        )
        messages.append(respuesta.message)

        pendientes = [(tc.function.name, tc.function.arguments)
                      for tc in (respuesta.message.tool_calls or [])]
        if not pendientes:
            pendientes = llamadas_fugadas_en_texto(respuesta.message.content)

        if not pendientes:
            print(f"\nRESPUESTA FINAL:\n{respuesta.message.content}")
            return

        for nombre, args in pendientes:
            print(f"   --> usa: {nombre}({dict(args)})")
            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__":
    # Cuatro preguntas que deberían activar herramientas distintas:
    responder("¿Cuánto es 1234 * 5678 + 99?")                       # -> calcular
    responder("¿Qué día de la semana es hoy?")                      # -> fecha_y_hora
    responder("¿Cuál es la última versión estable de Python?")      # -> buscar_web (+ leer_pagina)
    responder("¿Cuántos años hace que naciste si naces en 1991 "    # -> fecha_y_hora + calcular
              "y estamos en el año actual?")
