"""Educational agent. Copyright (c) 2026 Academia IA & Ingenieria contributors.
SPDX-License-Identifier: MIT. See LICENSE-AGENTE.txt.
Demo: python agente_abierto.py --demo
Tests: python agente_abierto.py --test
Local model: python agente_abierto.py --model qwen2.5:3b --question "Suma 20 y 30 minutos"
"""
import argparse
import json
import math
import unittest
from urllib.request import Request, urlopen

def ejecutar(propuesta, aprobar=lambda texto: False):
    if not isinstance(propuesta, dict) or set(propuesta) != {'herramienta', 'argumentos'}:
        raise ValueError('Formato de propuesta inválido')
    args = propuesta['argumentos']
    if not isinstance(args, dict): raise ValueError('Argumentos inválidos')
    if propuesta['herramienta'] == 'sumar_minutos':
        valores = args.get('valores')
        if set(args) != {'valores'} or not isinstance(valores, list) or not 1 <= len(valores) <= 20:
            raise ValueError('Se requieren 1–20 valores')
        for v in valores:
            if type(v) not in (int, float) or not 0 <= v <= 1440 or not math.isfinite(v):
                raise ValueError('Minutos fuera de rango')
        return {'total': sum(valores)}
    if propuesta['herramienta'] == 'preparar_borrador':
        texto = args.get('texto')
        if set(args) != {'texto'} or not isinstance(texto, str) or not 1 <= len(texto) <= 500:
            raise ValueError('Texto fuera de límite')
        if not aprobar(texto): return {'estado': 'no_aprobado'}
        return {'estado': 'borrador_local', 'texto': texto, 'enviado': False}
    raise ValueError('Herramienta no permitida')

def decidir_local(pregunta, modelo):
    if not 1 <= len(pregunta) <= 600: raise ValueError('Pregunta de 1–600 caracteres')
    reglas = ('Devuelve sólo JSON con herramienta y argumentos. Herramientas: '
              'sumar_minutos con {"valores":[numeros]}, o preparar_borrador con {"texto":"texto"}. '
              'No agregues campos. No puedes enviar mensajes ni ejecutar programas.')
    payload = {'model': modelo, 'stream': False, 'format': 'json',
               'messages': [{'role':'system','content':reglas},{'role':'user','content':pregunta}],
               'options': {'num_predict': 200, 'temperature': 0}}
    request = Request('http://127.0.0.1:11434/api/chat', data=json.dumps(payload).encode(),
                      headers={'Content-Type':'application/json'})
    with urlopen(request, timeout=60) as response:
        raw = response.read(65537)
    if len(raw)>65536: raise ValueError('Respuesta demasiado grande')
    return json.loads(json.loads(raw)['message']['content'])

class Pruebas(unittest.TestCase):
    def test_suma(self): self.assertEqual(ejecutar({'herramienta':'sumar_minutos','argumentos':{'valores':[20,30]}}),{'total':50})
    def test_herramienta_prohibida(self):
        with self.assertRaises(ValueError): ejecutar({'herramienta':'eliminar_archivos','argumentos':{}})
    def test_limites(self):
        for v in [True,-1,1441,float('nan'),'20']:
            with self.subTest(v=v),self.assertRaises(ValueError): ejecutar({'herramienta':'sumar_minutos','argumentos':{'valores':[v]}})
    def test_sin_aprobacion(self): self.assertEqual(ejecutar({'herramienta':'preparar_borrador','argumentos':{'texto':'Hola'}}),{'estado':'no_aprobado'})
    def test_aprobado_no_envia(self): self.assertFalse(ejecutar({'herramienta':'preparar_borrador','argumentos':{'texto':'Hola'}},lambda _:True)['enviado'])
    def test_campos_extra(self):
        with self.assertRaises(ValueError): ejecutar({'herramienta':'sumar_minutos','argumentos':{'valores':[1],'comando':'algo'}})

if __name__=='__main__':
    parser=argparse.ArgumentParser();parser.add_argument('--demo',action='store_true');parser.add_argument('--test',action='store_true');parser.add_argument('--model');parser.add_argument('--question')
    opts=parser.parse_args()
    if opts.test: unittest.main(argv=['agente_abierto'],verbosity=2)
    elif opts.demo: print(ejecutar({'herramienta':'sumar_minutos','argumentos':{'valores':[20,30]}}))
    elif opts.model and opts.question:
        try:
            propuesta=decidir_local(opts.question,opts.model)
            print('Propuesta no confiable del modelo:',json.dumps(propuesta,ensure_ascii=True))
            def aprobar(texto):
                print('Borrador propuesto:',json.dumps(texto,ensure_ascii=True))
                return input('¿Permitir mostrar este borrador local? Escribe SI: ')=='SI'
            print(json.dumps(ejecutar(propuesta,aprobar),ensure_ascii=True))
        except Exception as error: print('No se ejecutó una acción válida:',type(error).__name__);raise SystemExit(1)
    else: parser.print_help()
