#!/usr/bin/env python3
"""
sipi_ia_servidor.py - Servidor HTTP para SiPi IA.

sipi_ia_agente.py ya se puede usar como herramienta por linea de
comandos (--json). Este servidor expone la MISMA capacidad como API
HTTP, para el caso en que quien va a usar SiPi IA como herramienta
principal no sea un script que puede lanzar procesos, sino un agente
de IA que trabaja mejor (o unicamente) con llamadas HTTP -- por
ejemplo un agente conectado por MCP/tool-calling remoto, o un panel
web. Usa exclusivamente la biblioteca estandar (http.server), sin
Flask ni ninguna dependencia externa, para que levantar el servidor no
requiera instalar nada mas que Python.

Uso:
    python3 sipi_ia_servidor.py               # escucha en 127.0.0.1:8420
    python3 sipi_ia_servidor.py --puerto 9000
    python3 sipi_ia_servidor.py --host 0.0.0.0 --token MI_TOKEN_SECRETO

Endpoints:
    GET  /salud
        -> {"ok": true, "servicio": "sipi-ia", "version": "..."}

    POST /comando
        Body: el mismo JSON que acepta sipi_ia_agente.ejecutar_comando_json
        (accion: generar_codigo, construir_proyecto, sesion_pedir, etc.)
        -> el mismo dict de resultado, serializado como JSON.

    GET  /acciones
        -> lista de acciones soportadas con una linea de descripcion
        cada una, para que un agente que recien se conecta pueda
        'descubrir' que puede pedir sin tener que leer el codigo fuente.

Seguridad: pensado para uso local/confiable por defecto (host
127.0.0.1). Si se expone en una red mas amplia (--host 0.0.0.0), se
recomienda pasar --token y que el cliente lo mande en el header
'X-SiPi-Token' -- sin eso, CUALQUIERA en esa red podria pedirle a este
servidor que ejecute codigo (a traves del motor de SiPi) y gaste la
clave de API configurada.
"""
import os
import sys
import json
import argparse
import threading
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer

sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
import sipi_ia_agente  # noqa: E402 - necesita el sys.path.insert de arriba
import sipi_debug  # noqa: E402

VERSION_SERVIDOR = "1.0"

ACCIONES_DESCRIPCION = {
    "generar_codigo": "Genera un programa SiPi nuevo a partir de una descripcion.",
    "corregir_error": "Corrige un programa dado su codigo y el error real de ejecucion.",
    "auto_corregir": "Genera (o corrige) codigo en un loop escribir->ejecutar->corregir hasta que funcione.",
    "construir_proyecto": "Planifica y construye un proyecto SiPi multi-archivo completo.",
    "optimizar_codigo": "Prolija codigo que ya funciona sin cambiarle el comportamiento.",
    "generar_pruebas": "Genera un segundo programa que prueba el comportamiento del primero.",
    "diagnosticar_error": "Explica la causa de un error sin reescribir el codigo.",
    "crear_plan": "Arma el plan de archivos para un objetivo, sin generar codigo todavia.",
    "indexar_proyecto": "Analiza y cachea la estructura de todos los .sipi de una carpeta.",
    "buscar_en_proyecto": "Busca donde esta definido algo (funcion, variable, clase) en un proyecto.",
    "renombrar_en_proyecto": "Refactor: renombra algo de forma consistente en todos los archivos.",
    "verificar_proyecto": "Corre todos los .sipi de una carpeta contra el motor real.",
    "sesion_pedir": "Continua (o inicia) una conversacion de refinamiento iterativo con memoria.",
    "buscar_patrones": "Busca en la biblioteca soluciones ya verificadas parecidas a una descripcion.",
    "estadisticas_biblioteca": "Devuelve estadisticas de uso de la biblioteca de patrones.",
}


class ManejadorSiPiIA(BaseHTTPRequestHandler):
    token_esperado = None  # se configura desde main() antes de levantar el servidor

    def log_message(self, formato, *args):
        # En produccion (SIPI_DEBUG apagado) no imprimimos ni la IP de
        # quien pega, ni la ruta pedida -- eso es info interna. Con
        # SIPI_DEBUG=1 se ve el log completo, util para desarrollar.
        sipi_debug.log_debug(f"{self.address_string()} - {formato % args}")

    def _responder_json(self, cuerpo, codigo=200):
        datos = json.dumps(cuerpo, ensure_ascii=False).encode("utf-8")
        self.send_response(codigo)
        self.send_header("Content-Type", "application/json; charset=utf-8")
        self.send_header("Content-Length", str(len(datos)))
        self.end_headers()
        self.wfile.write(datos)

    def _autorizado(self):
        if not self.token_esperado:
            return True
        return self.headers.get("X-SiPi-Token") == self.token_esperado

    def do_GET(self):
        if self.path == "/salud":
            self._responder_json({"ok": True, "servicio": "sipi-ia", "version": VERSION_SERVIDOR})
        elif self.path == "/acciones":
            self._responder_json({"ok": True, "acciones": ACCIONES_DESCRIPCION})
        else:
            self._responder_json({"ok": False, "error": "Ruta no encontrada. Ver GET /acciones."}, codigo=404)

    def do_POST(self):
        if self.path != "/comando":
            self._responder_json({"ok": False, "error": "Ruta no encontrada. Usa POST /comando."}, codigo=404)
            return
        if not self._autorizado():
            self._responder_json({"ok": False, "error": "Token invalido o faltante (header X-SiPi-Token)."}, codigo=401)
            return

        try:
            largo = int(self.headers.get("Content-Length", 0))
            cuerpo_crudo = self.rfile.read(largo) if largo else b"{}"
            comando = json.loads(cuerpo_crudo.decode("utf-8"))
        except (ValueError, json.JSONDecodeError) as e:
            sipi_debug.log_debug(f"JSON invalido recibido: {e}")
            self._responder_json({"ok": False, "error": "El cuerpo del pedido no es JSON valido."}, codigo=400)
            return

        try:
            resultado = sipi_ia_agente.ejecutar_comando_json(comando)
        except Exception as e:
            sipi_debug.log_debug(f"Error no controlado en ejecutar_comando_json: {e!r}")
            self._responder_json(
                {"ok": False, "error": sipi_debug.mensaje_error_usuario(e, "no se pudo ejecutar el comando")},
                codigo=500,
            )
            return
        self._responder_json(resultado, codigo=200 if resultado.get("ok") else 422)


def iniciar_servidor(host="127.0.0.1", puerto=8420, token=None, en_hilo_aparte=False):
    """Arranca el servidor. Con en_hilo_aparte=True lo corre en un
    thread daemon y devuelve el objeto servidor (para poder integrarlo
    dentro del editor grafico sin bloquear la interfaz, por ejemplo
    desde un boton 'Activar servidor SiPi IA')."""
    ManejadorSiPiIA.token_esperado = token
    servidor = ThreadingHTTPServer((host, puerto), ManejadorSiPiIA)

    if en_hilo_aparte:
        hilo = threading.Thread(target=servidor.serve_forever, daemon=True)
        hilo.start()
        return servidor

    print(f"SiPi IA servidor escuchando en http://{host}:{puerto}  (Ctrl+C para detener)")
    print("Endpoints: GET /salud , GET /acciones , POST /comando")
    if token:
        print("Autenticacion activada: los pedidos necesitan el header X-SiPi-Token.")
    try:
        servidor.serve_forever()
    except KeyboardInterrupt:
        print("\nDeteniendo servidor...")
        servidor.shutdown()
    return servidor


def main():
    parser = argparse.ArgumentParser(description="Servidor HTTP de SiPi IA (herramienta para otras IAs, sobre la red).")
    parser.add_argument("--host", default="127.0.0.1", help="Direccion donde escuchar (default: 127.0.0.1, solo local).")
    parser.add_argument("--puerto", type=int, default=8420)
    parser.add_argument("--token", default=None,
                         help="Si se define, los pedidos necesitan el header X-SiPi-Token con este valor.")
    args = parser.parse_args()

    if args.host not in ("127.0.0.1", "localhost") and not args.token:
        print("[SiPi IA servidor] AVISO: estas exponiendo el servidor mas alla de localhost sin --token. "
              "Cualquiera en esa red podria usar tu clave de API. Se recomienda encarecidamente pasar --token.",
              file=sys.stderr)

    iniciar_servidor(host=args.host, puerto=args.puerto, token=args.token)


if __name__ == "__main__":
    main()
