# app/xulcan/app.py

"""Xulcan OS - The Core Application Facade."""

import os
import logging
import inspect
from typing import Callable, Optional, Any, get_origin, get_args, Union

# 1. Las Interfaces y la Magia Universal
from xulcan.kernel.interfaces import LLMProvider, LedgerRepository, StateStore
from xulcan.system.registry import ProviderRegistry
from xulcan.system.loader import BlueprintLoader

# 2. El Motor y los Tipos Puros
from xulcan.kernel.runtime import ProtoKernel
from xulcan.blueprint.schema import AgentBlueprint, ModelProvider
from xulcan.protocol.tools import ToolDefinition, FunctionDef
from xulcan.kernel.environment import SystemEnvironment

# 3. Los Adaptadores (Las Clases, no las instancias)
from xulcan.ledger.adapters.in_memory import InMemoryLedger
from xulcan.memory.state.adapters.in_memory import MemoryStateStore

from xulcan.llm.adapters.gemini import GeminiAdapter
from xulcan.llm.adapters.ollama import OllamaAdapter
from xulcan.llm.adapters.groq import GroqAdapter

# 4. Los Ejecutores (Músculos)
from xulcan.tools.router import ToolRouterExecutor  # <-- ¡Renombrado!
from xulcan.tools.executors.local import LocalPythonExecutor
from xulcan.tools.executors.agent import SubAgentExecutor
from xulcan.tools.executors.sandbox.docker import DockerProvider
from xulcan.tools.executors.sandbox.executor import SandboxExecutor

logger = logging.getLogger(__name__)


class Xulcan:
    """
    El motor principal de Xulcan Agent OS. 
    Actúa como Facade y Contenedor IoC para el ecosistema agéntico.
    """

    def __init__(self, gemini_api_key: Optional[str] = None, groq_api_key: Optional[str] = None, ollama_host: str = "http://localhost:11434"):
        
        # ==========================================
        # 1. PREPARAR LOS REGISTROS UNIVERSALES
        # ==========================================
        self.ledger_registry = ProviderRegistry[LedgerRepository]("Ledger")
        self.store_registry = ProviderRegistry[StateStore]("StateStore")
        self.llm_registry = ProviderRegistry[LLMProvider]("LLM")
        
        # Registramos las CLASES (Patrón Fábrica Abstracta)
        self.ledger_registry.register("memory", InMemoryLedger)
        self.store_registry.register("memory", MemoryStateStore)
        
        self.llm_registry.register(ModelProvider.GOOGLE, GeminiAdapter)
        self.llm_registry.register(ModelProvider.GROQ, GroqAdapter)
        self.llm_registry.register(ModelProvider.OLLAMA, OllamaAdapter)

        # Guardamos las llaves maestras en un diccionario para la Inyección Perezosa (Lazy Loading)
        self._llm_credentials = {
            ModelProvider.GOOGLE: gemini_api_key or os.getenv("GEMINI_API_KEY"),
            ModelProvider.GROQ: groq_api_key or os.getenv("GROQ_API_KEY"),
            ModelProvider.OLLAMA: ollama_host
        }

        # ==========================================
        # 2. INICIALIZAR LA INFRAESTRUCTURA ACTIVA
        # ==========================================
        LedgerClass = self.ledger_registry.get_class("memory")
        self.active_ledger = LedgerClass()
        
        StoreClass = self.store_registry.get_class("memory")
        self.environment = SystemEnvironment(
            state_store=StoreClass(),
            workspace_id=None
        )

        # ==========================================
        # 3. ENSAMBLAR LOS MÚSCULOS (TOOLS)
        # ==========================================
        self.local_tools = LocalPythonExecutor(environment=self.environment)
        self.sub_agent_tools = SubAgentExecutor(environment=self.environment)
        self.tool_router = ToolRouterExecutor(environment=self.environment)

        self.sandbox_executor = None
        try:
            self.sandbox_executor = SandboxExecutor(
                provider=DockerProvider(), 
                environment=self.environment
            )
        except Exception:
            logger.warning("⚠️ [Xulcan OS] Docker no disponible. Las herramientas de Sandbox estarán deshabilitadas.")
        
        # ==========================================
        # 4. INICIAR EL CEREBRO
        # ==========================================
        # Nota: Ahora el Kernel recibe el Registry de Clases y las Credenciales.
        # Él mismo instanciará a Gemini u Ollama SOLO si el Blueprint se lo pide.
        self.kernel = ProtoKernel(
            repository=self.active_ledger,
            llm_registry=self.llm_registry,
            tool_executor=self.tool_router,
            environment=self.environment,
            llm_credentials=self._llm_credentials  # <-- Se lo pasamos al Kernel
        )

        self.sub_agent_tools.bind_kernel(self.kernel)

    # =====================================================================
    # MÉTODOS PÚBLICOS DE EXPERIENCIA DE DESARROLLADOR (DX)
    # =====================================================================

    def load_agent(self, source: Any) -> AgentBlueprint:
        """Carga y bautiza a un agente desde un dict, string YAML o ruta de archivo."""
        if isinstance(source, dict):
            return BlueprintLoader.from_dict(source)
        elif isinstance(source, str):
            if source.endswith((".yml", ".yaml")):
                return BlueprintLoader.from_file(source)
            return BlueprintLoader.from_yaml_string(source)
        else:
            raise TypeError("El source debe ser un diccionario, un string YAML, o una ruta a un archivo .yml")

    def tool(self, _func: Optional[Callable] = None, *, name: Optional[str] = None, description: Optional[str] = None):
        """
        Decorador Mágico: Convierte funciones Python en Herramientas Agénticas.
        Soporta uso con y sin paréntesis: @app.tool o @app.tool(description="...")
        """
        def decorator(func: Callable):
            tool_name = name or func.__name__
            tool_desc = description or inspect.getdoc(func) or "Sin descripción."

            sig = inspect.signature(func)
            properties = {}
            required = []

            for param_name, param in sig.parameters.items():
                if param_name in ["context", "self"]: continue

                param_type = param.annotation

                # Desenvuelve Optional[X] → X antes de inferir el tipo JSON
                origin = get_origin(param_type)
                if origin is Union:
                    inner = [a for a in get_args(param_type) if a is not type(None)]
                    param_type = inner[0] if inner else str

                json_type = "string"
                if param_type is int: json_type = "integer"
                elif param_type is float: json_type = "number"
                elif param_type is bool: json_type = "boolean"
                elif param_type is list or get_origin(param_type) is list: json_type = "array"
                elif param_type is dict or get_origin(param_type) is dict: json_type = "object"

                properties[param_name] = {"type": json_type, "description": f"Parámetro: {param_name}"}

                # Solo es required si no tiene valor por defecto
                if param.default == inspect.Parameter.empty:
                    required.append(param_name)

            definition = ToolDefinition(
                type="function",
                function=FunctionDef(
                    name=tool_name, description=tool_desc.strip(),
                    parameters={"type": "object", "properties": properties, "required": required}
                )
            )
            self._add_tool(definition, func)
            return func

        if _func is None:
            return decorator
        return decorator(_func)

    def add_agent(self, blueprint: AgentBlueprint, description: str) -> str:
        """Registra un Sub-Agente como herramienta y devuelve el nombre de la herramienta."""
        tool_name = blueprint.id.replace("-", "_")
        definition = ToolDefinition(type="function", function=FunctionDef(
            name=tool_name, description=description,
            parameters={
                "type": "object",
                "properties": {
                    "prompt": {"type": "string", "description": "Instrucciones detalladas."},
                    "output_memory_key": {"type": "string", "description": "Llave RAM opcional."}
                },
                "required": ["prompt"]
            }
        ))
        self._add_tool(definition, blueprint)
        return tool_name

    def enable_sandbox(self) -> list[str]:
        """Activa toda la suite de herramientas Docker y devuelve la lista de comandos expuestos."""
        if not self.sandbox_executor:
            raise RuntimeError("El entorno Docker Sandbox no está disponible en esta máquina.")
            
        tools = [
            ToolDefinition(type="function", function=FunctionDef(
                name="sandbox_run_python", description="Ejecuta código Python efímero de forma segura.",
                parameters={"type": "object", "properties": {"code": {"type": "string"}}, "required": ["code"]})),
            ToolDefinition(type="function", function=FunctionDef(
                name="sandbox_run_bash", description="Ejecuta comandos de terminal (Bash).",
                parameters={"type": "object", "properties": {"command": {"type": "string"}}, "required": ["command"]})),
            ToolDefinition(type="function", function=FunctionDef(
                name="sandbox_write_file", description="Escribe código en el disco duro persistente.",
                parameters={"type": "object", "properties": {"file_path": {"type": "string"}, "content": {"type": "string"}}, "required": ["file_path", "content"]})),
            ToolDefinition(type="function", function=FunctionDef(
                name="sandbox_read_file", description="Lee un archivo del disco duro del contenedor.",
                parameters={"type": "object", "properties": {"file_path": {"type": "string"}}, "required": ["file_path"]}))
        ]
        for t in tools:
            self._add_tool(t, "sandbox")
        return [t.function.name for t in tools]

    def enable_stdlib(self, modules: list[str] | str = "all") -> None:
        """Activa los paquetes de herramientas nativas de Xulcan OS."""
        if modules == "all": modules = ["fs", "network", "system"]
        elif isinstance(modules, str): modules = [modules]

        if "fs" in modules:
            from xulcan.tools.stdlib.fs import list_directory, read_file_content, read_file_ast
            self.tool(description="Lista un directorio local")(list_directory)
            self.tool(description="Lee el contenido de un archivo local")(read_file_content)
            self.tool(description="Lee el AST de un archivo Python")(read_file_ast)

        if "network" in modules:
            from xulcan.tools.stdlib.network import network_fetch_webpage, network_api_get
            self.tool(description="Descarga y limpia una página web")(network_fetch_webpage)
            self.tool(description="Hace una petición GET a una API REST")(network_api_get)

        logger.info("📦 Xulcan Stdlib cargada: %s", ', '.join(modules))

    # =====================================================================
    # MÉTODOS DE EJECUCIÓN
    # =====================================================================

    async def run(
        self,
        prompt: str,
        blueprint: AgentBlueprint,
        session_key: Optional[str] = None,
    ) -> str:
        """
        Inicia o continúa el ciclo cognitivo FSM de un agente.

        - Sin session_key → Agente reactivo (sin memoria entre llamadas).
        - Con session_key → Agente conversacional (el Ledger recuerda el hilo).
        """
        # El Ledger resuelve la continuidad — app.py no sabe nada de run_ids
        parent_id = None
        if session_key:
            parent_id = await self.active_ledger.get_last_run_id(session_key)

        run_id, response = await self.kernel.execute_run(blueprint, prompt, parent_id=parent_id)

        if session_key:
            await self.active_ledger.tag_run(run_id, session_key)

        if not response or not response.content:
            raise RuntimeError(f"El agente no produjo respuesta en run {run_id}")

        return str(response.content)

    async def get_audit(self, run_id: str) -> dict:
        """Recupera la caja negra (Ledger) de una ejecución específica."""
        summary = await self.active_ledger.get_summary(run_id)
        events = await self.active_ledger.get_events(run_id)
        return {
            "summary": summary.model_dump(mode='json') if summary else None,
            "history": [event.model_dump(mode='json') for event in events]
        }

    # =====================================================================
    # MÉTODOS PRIVADOS
    # =====================================================================

    def _add_tool(self, definition: ToolDefinition, target: Any) -> None:
        """Enrutador de bajo nivel. Usa @tool, add_agent o enable_sandbox en su lugar."""
        if isinstance(target, AgentBlueprint):
            self.sub_agent_tools.register_agent(definition, target)
            self.tool_router.route_tool(definition.function.name, self.sub_agent_tools)
        elif callable(target):
            self.local_tools.register_function(definition, target)
            self.tool_router.route_tool(definition.function.name, self.local_tools)
        elif target == "sandbox":
            self.sandbox_executor.register_tool(definition)
            self.tool_router.route_tool(definition.function.name, self.sandbox_executor)
        else:
            raise ValueError(f"Target inválido para '{definition.function.name}'.")