"""Output contracts for model generation responses.

This module defines the response formats returned by the Kernel after model
generation completes. It supports both synchronous (complete) and streaming
(incremental) response modes, with strict validation to prevent protocol
violations and ensure economic accountability.

Response Types:
    - UnifiedResponse: Complete, finalized response from synchronous generation
    - DeltaContent: Incremental update in a streaming response
    - UnifiedChunk: Single event in a streaming response sequence

Security Features:
    - Substantive content validation rejects empty responses
    - Chunk size limits prevent DoS via memory exhaustion
    - Usage statistics enforce economic accountability

Enums:
    FinishReason: Enumerates why a model generation terminated
"""

from typing import Any, Dict, List, Literal
from enum import Enum

from pydantic import Field, field_validator, model_validator

from xulcan.core import (
    CanonicalRecord,
    UsageStats,
    SemanticText,
    SafePayload
)
from .tools import ToolCall
from .utils import MAX_CHUNK_SIZE 


# ═══════════════════════════════════════════════════════════════════════════
# DOMAIN ENUMERATIONS
# ═══════════════════════════════════════════════════════════════════════════

class FinishReason(str, Enum):
    """Enumerates why a model generation terminated.
    
    Used for telemetry, debugging, and enforcing correct handling of incomplete
    responses (e.g., length limits require continuation logic).
    """
    STOP = "stop"                    # Natural completion
    LENGTH = "length"                # Hit token/character limit
    TOOL_CALLS = "tool_calls"        # Model requested tool execution
    CONTENT_FILTER = "content_filter"  # Safety filter triggered
    UNKNOWN = "unknown"              # Fallback for provider-specific reasons


# ═══════════════════════════════════════════════════════════════════════════
# RESPONSE PRIMITIVES (Complete Generations)
# ═══════════════════════════════════════════════════════════════════════════

class UnifiedResponse(CanonicalRecord):
    """A complete, finalized response from a model generation request.

    This is the terminal output type returned after a full generation completes
    (non-streaming mode). It aggregates all response components—text, tool calls,
    reasoning, refusals—along with resource consumption metrics and termination
    metadata.
    
    Economic Accounting:
        The 'usage' field tracks the actual resource consumption (tokens) for
        billing, rate limiting, and capacity planning. Providers must report
        accurate counts to prevent resource leakage.
    
    Finish Reason Semantics:
        - "stop": Natural completion (model satisfied the request).
        - "length": Hit token/character limit (response may be truncated).
        - "tool_calls": Model delegated to external tools (requires continuation).
        - "content_filter": Safety system blocked the response.
        - "unknown": Fallback for provider-specific reasons.
    
    Attributes:
        content: Main response text visible to users.
        reasoning_content: Internal reasoning steps (extended thinking).
        refusal: Refusal explanation (if model declined).
        tool_calls: Requested tool invocations.
        usage: Token consumption statistics (input + output + total).
        finish_reason: Why the generation terminated.
        provider_metadata: Provider-specific metadata (model version, headers).
        logprobs: Log probabilities for generated tokens (debugging/analysis).
    
    Example:
        >>> response = UnifiedResponse(
        ...     content="The capital of France is Paris.",
        ...     usage=UsageStats(input_tokens=8, output_tokens=7, total_tokens=15),
        ...     finish_reason="stop",
        ...     provider_metadata={"model": "gpt-4", "version": "2024-01-10"}
        ... )
    """
    content: SemanticText | None = Field(
        default=None,
        description="Main response text visible to end-users"
    )
    
    reasoning_content: SemanticText | None = Field(
        default=None,
        description="Internal chain-of-thought steps (not shown to users)"
    )
    
    refusal: SemanticText | None = Field(
        default=None,
        description="Explanation of why the model declined to respond"
    )
    
    tool_calls: List[ToolCall] | None = Field(
        default=None,
        description="Tool invocations requested by the model"
    )
    
    usage: UsageStats = Field(
        description="Token consumption metrics for economic accounting and rate limiting"
    )
    
    finish_reason: FinishReason = Field(
        description="Reason the generation terminated (affects continuation logic)"
    )
    
    provider_metadata: SafePayload = Field(
        default_factory=dict,
        description="Provider-specific metadata (model version, request headers, etc.)"
    )

    logprobs: List[SafePayload] | None = Field(
        default=None,
        description="Log probabilities for generated tokens (debugging and analysis)"
    )


    @model_validator(mode='after')
    def validate_has_content(self) -> 'UnifiedResponse':
        """Ensures the response contains substantive data.
        
        A response without content represents a generation failure. This could
        indicate a provider bug, timeout, or safety filter activation. At least
        one of the content fields must be populated.
        
        Non-Substantive Values:
            - None
            - Empty strings ("")
            - Whitespace-only strings ("   ")
            - Empty lists ([])
        
        Raises:
            ValueError: If all content fields are non-substantive.
        """
        has_content = bool(self.content and self.content.strip())
        has_reasoning = bool(self.reasoning_content and self.reasoning_content.strip())
        has_refusal = bool(self.refusal and self.refusal.strip())
        has_tool_calls = bool(self.tool_calls)
        
        if not any([has_content, has_reasoning, has_refusal, has_tool_calls]):
            raise ValueError(
                "UnifiedResponse must contain substantive data in at least one field. "
                "Received: content={}, reasoning_content={}, refusal={}, tool_calls={}. "
                "This indicates a generation failure or corrupted provider response.".format(
                    repr(self.content),
                    repr(self.reasoning_content),
                    repr(self.refusal),
                    repr(self.tool_calls)
                )
            )
        
        return self


# ═══════════════════════════════════════════════════════════════════════════
# STREAMING PRIMITIVES (Incremental Updates)
# ═══════════════════════════════════════════════════════════════════════════

# 1. El fragmento de la función (nombre parcial + argumentos parciales)
class FunctionCallDelta(CanonicalRecord):
    name: str | None = None          # Puede venir en un chunk y no en otro
    arguments: str | None = None     # String parcial (JSON roto), se concatena

# 2. El fragmento de la llamada (índice + id + función)
class ToolCallDelta(CanonicalRecord):
    index: int                       # Vital para saber qué tool call estamos construyendo
    id: str | None = None            # Solo viene en el primer chunk
    type: Literal["function"] | None = None
    function: FunctionCallDelta | None = None

class DeltaContent(CanonicalRecord):
    """Represents an incremental update in a streaming response.

    Streaming responses are delivered as a sequence of delta chunks, each
    containing a partial string or object. The client must accumulate deltas
    to reconstruct the complete response.
    
    Accumulation Strategy:
        - Strings (content, reasoning_content, refusal): Concatenate.
        - Tool calls: Merge partial objects by index, handling incomplete JSON.
    
    Why Streaming:
        Reduces perceived latency for long responses. Users see partial results
        immediately rather than waiting for full completion.
    
    Attributes:
        content: Partial main response text.
        reasoning_content: Partial reasoning steps.
        tool_calls: Partial tool call objects (may be incomplete/unparseable).
        refusal: Partial refusal explanation.
    
    Example:
        >>> # First chunk
        >>> delta1 = DeltaContent(content="The capital")
        >>> # Second chunk
        >>> delta2 = DeltaContent(content=" of France")
        >>> # Third chunk
        >>> delta3 = DeltaContent(content=" is Paris.")
        >>> # Accumulated result: "The capital of France is Paris."
    """
    content: str | None = Field(
        default=None,
        description="Partial response text (accumulate via concatenation)"
    )
    
    reasoning_content: str | None = Field(
        default=None,
        description="Partial reasoning steps (accumulate via concatenation)"
    )
    
    tool_calls: List[ToolCallDelta] | None = Field(
        default=None,
        description="Partial tool call objects (may be incomplete until final chunk)"
    )

    
    refusal: str | None = Field(
        default=None,
        description="Partial refusal explanation (accumulate via concatenation)"
    )

    @model_validator(mode='after')
    def validate_chunk_size(self) -> 'DeltaContent':
        """Prevents DoS via memory exhaustion from massive chunks.
        
        Streaming chunks should be small increments. A chunk exceeding the limit
        is indistinguishable from an attack or a provider malfunction.
        """
        # Sum the approximate size of text fields
        total_size = 0
        if self.content: 
            total_size += len(self.content)
        if self.reasoning_content: 
            total_size += len(self.reasoning_content)
        if self.refusal: 
            total_size += len(self.refusal)
        
        # Quick validation
        if total_size > MAX_CHUNK_SIZE:
            raise ValueError(
                f"Chunk size ({total_size} bytes) exceeds safety limit of {MAX_CHUNK_SIZE}. "
                "This serves as a DoS protection against malicious streams."
            )
            
        return self


class UnifiedChunk(CanonicalRecord):
    """Represents a single event in a streaming response sequence.

    Analogous to a Server-Sent Event (SSE). Streaming responses consist of
    multiple chunks, with the final chunk typically containing the finish_reason
    and usage statistics.
    
    Chunk Types:
        - Regular chunk: Contains delta with partial content.
        - Final chunk: Contains finish_reason and usage (may have empty delta).
    
    Stream Termination:
        A chunk with finish_reason != None signals the end of the stream.
        Clients should stop listening and finalize accumulation.
    
    Attributes:
        delta: The incremental content update.
        finish_reason: Present only in the final chunk.
        usage: Token usage statistics (present only in the final chunk).
        id: Provider-assigned chunk identifier (for debugging).
        provider_metadata: Provider-specific streaming metadata.
        logprobs: Log probabilities for tokens in this chunk.
    
    Example:
        >>> # Regular chunk (partial content)
        >>> chunk = UnifiedChunk(
        ...     delta=DeltaContent(content="Hello"),
        ...     id="chunk_1"
        ... )
        >>> 
        >>> # Another regular chunk
        >>> chunk = UnifiedChunk(
        ...     delta=DeltaContent(content=" world!"),
        ...     id="chunk_2"
        ... )
        >>> 
        >>> # Final chunk (stream termination)
        >>> final = UnifiedChunk(
        ...     delta=DeltaContent(),
        ...     finish_reason="stop",
        ...     usage=UsageStats(input_tokens=5, output_tokens=3, total_tokens=8),
        ...     id="chunk_final"
        ... )
    """
    delta: DeltaContent = Field(
        description="Incremental content update (accumulate with previous deltas)"
    )
    
    finish_reason: FinishReason | None = Field(
        default=None,
        description="Present only in final chunk; signals stream termination"
    )
    
    usage: UsageStats | None = Field(
        default=None,
        description="Token usage statistics (present only in final chunk)"
    )
    
    id: str | None = Field(
        default=None,
        description="Provider-assigned chunk identifier for debugging and correlation"
    )
    
    provider_metadata: SafePayload = Field(
        default_factory=dict,
        description="Provider-specific streaming metadata (chunk index, timestamps)"
    )

    logprobs: List[SafePayload] | None = Field(
        default=None,
        description="Log probabilities for tokens generated in this chunk"
    )
