"""Core interface contracts for the Xulcan Kernel.

This module defines the abstract protocols (contracts) that all external
adapters must implement. The Kernel depends on these interfaces, NOT on
concrete implementations, enabling true dependency inversion.

Architecture:
    - LLMProvider: Contract for model inference adapters
    - ToolExecutor: Contract for tool execution adapters  
    - LedgerRepository: Contract for event persistence adapters

All interfaces use Protocol (PEP 544) for structural subtyping, allowing
implementations without explicit inheritance.
"""

import asyncio
import logging
from typing import Protocol, List, Optional, runtime_checkable, Any
from abc import abstractmethod

from ..protocol import (
    UnifiedMessage,
    UnifiedResponse,
    UnifiedChunk,
    ToolDefinition,
    ToolCall,
    ToolMessage,
)
from ..history.events import RunEvent, RunSummary
from ..core.primitives import MachineID, ExternalID, SafePayload


# ═══════════════════════════════════════════════════════════════════════════
# LLM PROVIDER INTERFACE
# ═══════════════════════════════════════════════════════════════════════════

class LLMProvider(Protocol):
    """Contract that all LLM adapters must implement.
    
    This protocol defines the interface for language model inference. Adapters
    for different providers (OpenAI, Anthropic, Google, etc.) must implement
    these methods to be compatible with the Xulcan Kernel.
    
    Design Philosophy:
        - Provider-agnostic: Works with any LLM backend
        - Streaming support: Optional streaming via AsyncIterator
        - Tool integration: Native support for function calling
        - Error handling: Implementations must raise specific exceptions
    
    Example Implementation:
        >>> class GeminiProvider:
        ...     async def generate(
        ...         self,
        ...         messages: List[UnifiedMessage],
        ...         tools: List[ToolDefinition] | None = None,
        ...         temperature: float = 0.0,
        ...         max_tokens: int | None = None,
        ...     ) -> UnifiedResponse:
        ...         # Translate Protocol → Gemini API
        ...         # Call Gemini
        ...         # Translate response → Protocol
        ...         return UnifiedResponse(...)
    """
    
    @abstractmethod
    async def generate(
        self,
        model: ExternalID,
        messages: List[UnifiedMessage],
        tools: List[ToolDefinition] | None = None,
        temperature: float = 0.0,
        max_tokens: int | None = None,
        **kwargs,
    ) -> UnifiedResponse:
        """Generate a completion from the language model.
        
        This is the core inference method. It takes a conversation history
        (messages), optional tool definitions, and generation parameters,
        then returns a complete response including usage statistics.
        
        Args:
            messages: Conversation history (System, User, Assistant, Tool messages).
            tools: Optional tool definitions for function calling.
            temperature: Sampling temperature (0.0 = deterministic, 2.0 = creative).
            max_tokens: Maximum tokens to generate (None = provider default).
            **kwargs: Provider-specific parameters.
        
        Returns:
            UnifiedResponse with content, usage stats, and metadata.
        
        Raises:
            ValueError: If messages are malformed or invalid.
            RuntimeError: If the provider API fails.
            BudgetExceededError: If generation would exceed token limits.
        
        Notes:
            - Implementations MUST populate usage statistics accurately
            - Tool calls (if any) should be included in the response
            - Streaming is NOT supported by this method (see generate_stream)
        """
        ...
    
    async def generate_stream(
        self,
        messages: List[UnifiedMessage],
        tools: List[ToolDefinition] | None = None,
        temperature: float = 0.0,
        max_tokens: int | None = None,
        **kwargs,
    ) -> 'AsyncIterator[UnifiedChunk]':
        """Generate a streaming completion from the language model.
        
        Optional method for streaming responses. If not implemented,
        the Kernel will fall back to non-streaming generation.
        
        Args:
            Same as generate().
        
        Yields:
            UnifiedChunk objects containing incremental deltas.
        
        Notes:
            - Final chunk MUST include finish_reason and usage statistics
            - Implementations can raise NotImplementedError if streaming unsupported
        """
        raise NotImplementedError("Streaming not supported by this provider")


# ═══════════════════════════════════════════════════════════════════════════
# TOOL EXECUTOR INTERFACE
# ═══════════════════════════════════════════════════════════════════════════

class ToolExecutor(Protocol):
    """Contract for executing tool calls requested by the model.
    
    This protocol defines how the Kernel dispatches tool execution. 
    Implementations can support local execution, remote APIs, sandboxed
    containers, or any other execution environment.
    
    Design Philosophy:
        - Isolation: Tools run in controlled environments
        - Timeout enforcement: Implementations must respect execution limits
        - Error recovery: Graceful handling of tool failures
        - Observability: Detailed logs and traces for debugging
    
    Example Implementation:
        >>> class LocalToolExecutor:
        ...     async def execute(self, call: ToolCall) -> ToolMessage:
        ...         tool_fn = self.registry.get(call.name)
        ...         result = await tool_fn(**call.arguments)
        ...         return ToolMessage(
        ...             tool_call_id=call.id,
        ...             content=json.dumps(result)
        ...         )
    """
    
    @abstractmethod
    async def execute(self, call: ToolCall) -> ToolMessage:
        """Execute a single tool call and return the result.
        
        This method receives a ToolCall from the model's response and
        executes the corresponding function, returning the result as
        a ToolMessage that will be fed back into the conversation.
        
        Args:
            call: The tool call to execute (includes name, id, arguments).
        
        Returns:
            ToolMessage with execution result and correlation ID.
        
        Raises:
            ValueError: If the tool name is unknown or arguments invalid.
            TimeoutError: If execution exceeds configured timeout.
            RuntimeError: If tool execution fails.
        
        Notes:
            - Execution MUST be idempotent when possible
            - Results should be JSON-serializable strings
            - Errors should be returned as ToolMessage with error content,
              not raised (to allow model to see and handle the error)
        """
        ...
    
    async def execute_batch(self, calls: List[ToolCall]) -> List[ToolMessage]:
        # """Execute multiple tool calls in parallel safely.
        
        # Default implementation uses asyncio.gather to run tools concurrently.
        # It catches exceptions to prevent one failing tool from crashing the batch,
        # returning the error as a ToolMessage to the LLM.
        # """
        # async def safe_execute(call: ToolCall) -> ToolMessage:
        #     try:
        #         return await self.execute(call)
        #     except Exception as e:
        #         logger.error(f"Tool {call.name} execution failed: {e}", exc_info=True)
        #         # Convertir la excepción en un mensaje para que el LLM pueda intentar arreglarlo
        #         return ToolMessage(
        #             tool_call_id=call.id,
        #             name=call.name,
        #             content=f"Error executing tool: {str(e)}"
        #         )

        # # Ejecutar todas concurrentemente
        # results = await asyncio.gather(*(safe_execute(call) for call in calls))
        # return list(results)
        ...


# ═══════════════════════════════════════════════════════════════════════════
# LEDGER REPOSITORY INTERFACE
# ═══════════════════════════════════════════════════════════════════════════

@runtime_checkable
class LedgerRepository(Protocol):
    """The async contract for the append-only event log.
    
    This interface separates the Kernel (Domain) from the Storage (Infrastructure).
    It enforces the CQRS pattern:
    - Write Side: append()
    - Read Side (History): get_events()
    - Read Side (State): get_summary()
    """

    async def append(self, event: RunEvent) -> None:
        """Atomically persist a new event.
        
        Args:
            event: The immutable event record. Must contain a valid run_id.
            
        Raises:
            ValueError: If the run_id is missing or inconsistent.
            ConcurrencyError: If using a DB and optimistic locking fails.
        """
        ...

    async def get_events(
        self, 
        run_id: MachineID, 
        from_index: int = 0
    ) -> List[RunEvent]:
        """Retrieve the raw timeline for a run (Replay).
        
        Args:
            run_id: The target run identifier.
            from_index: Optimization for polling (get events > N).
            
        Returns:
            Ordered list of events. Empty list if no events found.
        """
        ...

    async def get_summary(self, run_id: MachineID) -> RunSummary:
        """Retrieve the current state projection (The 'Fold').
        
        This method answers "Where is the agent now?" without requiring
        the caller to replay 10,000 events manually.
        
        Raises:
            KeyError: If the run_id does not exist.
        """
        ...


# ═══════════════════════════════════════════════════════════════════════════
# STATE STORE (BLACKBOARD) INTERFACE
# ═══════════════════════════════════════════════════════════════════════════


@runtime_checkable
class StateStore(Protocol):
    """Contract for the Agent's volatile Working Memory (Blackboard).
    
    This interface defines the Shared Memory / IPC (Inter-Process Communication) 
    mechanism. It allows the agent, sub-agents, and tools (like the Sandbox) 
    to pass large payloads by reference instead of passing them by value through 
    the LLM prompt, heavily optimizing token usage.
    
    Nota de Arquitectura: Las implementaciones (ej. RedisStateStore) 
    deben encargarse de la serialización/deserialización (JSON/Pickle) 
    ya que el Kernel enviará tipos nativos de Python (`Any`).
    """

    @abstractmethod
    async def set(self, run_id: "MachineID", key: str, value: Any) -> None:
        """Store a value in the blackboard for a specific run."""
        ...

    @abstractmethod
    async def get(self, run_id: "MachineID", key: str) -> Any:
        """Retrieve a value from the blackboard. Returns None if not found."""
        ...

    @abstractmethod
    async def exists(self, run_id: "MachineID", key: str) -> bool:
        """Check if a key exists without loading its potentially large payload."""
        ...

    @abstractmethod
    async def keys(self, run_id: "MachineID") -> List[str]:
        """List all variable names currently available in this run's memory.
        Crucial for LLM introspection (allowing the agent to know what it holds).
        """
        ...

    @abstractmethod
    async def delete(self, run_id: "MachineID", key: str) -> None:
        """Remove a specific key from the blackboard."""
        ...

    @abstractmethod
    async def clear(self, run_id: "MachineID") -> None:
        """Clear all memory for a specific run (Garbage Collection).
        Called automatically by the Kernel when the run terminates.
        """
        ...