"""Multimodal content primitives for message composition.

This module defines the atomic building blocks for multimodal messages: text,
images, and audio. Each content part is a discriminated union member that can
be combined in message content arrays to support rich interactions.

Security Features:
    - MIME type allowlists prevent arbitrary file uploads
    - Mutual exclusivity (data XOR url) prevents ambiguous sources
    - Base64Data and SafeURL types enforce format validity
    - Empty content is rejected to prevent protocol pollution

Classes:
    TextPart: Plain text content fragment
    ImagePart: Visual data (base64 or URL)
    AudioPart: Audio data (base64 or URL)

Type Aliases:
    ContentPart: Discriminated union of all content types

Enums:
    ContentType: Discriminates between multimodal content types
"""

from typing import Literal, Optional, Union, Annotated
from enum import Enum

from pydantic import Field, field_validator, model_validator

from xulcan.core import (
    CanonicalRecord,
    MimeType,
    SemanticText,
    SafeURL,
    Base64Data,
)


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

class ContentType(str, Enum):
    """Discriminates between multimodal content types.
    
    Enables type-safe handling of text vs. binary media without runtime checks.
    """
    TEXT = "text"
    IMAGE = "image"
    AUDIO = "audio"


# ═══════════════════════════════════════════════════════════════════════════
# MULTIMODAL CONTENT PRIMITIVES
# ═══════════════════════════════════════════════════════════════════════════

class TextPart(CanonicalRecord):
    """A content fragment containing plain text.
    
    The simplest multimodal building block. Used in messages where text is
    combined with images or audio. The min_length=1 constraint prevents
    empty text parts from polluting the content array.
    
    Attributes:
        type: Discriminator field (always "text").
        text: The textual content (validated by SemanticText base type).
    
    Example:
        >>> part = TextPart(type="text", text="Describe this image:")
    """
    type: Literal[ContentType.TEXT] = ContentType.TEXT
    text: SemanticText = Field(
        min_length=1,
        description="The text content (protected against DoS by SemanticText constraints)"
    )

    @field_validator("text")
    @classmethod
    def _reject_whitespace_only(cls, v: str) -> str:
        """Reject inputs that are only whitespace while preserving formatting.

        We must allow newlines and leading/trailing whitespace to remain in
        stored text, but a value containing only whitespace provides no
        semantic content and should be rejected.
        """
        if isinstance(v, str) and v.strip() == "":
            raise ValueError("TextPart.text cannot be empty or whitespace only")
        return v


class ImagePart(CanonicalRecord):
    """A content fragment representing visual data.
    
    Images can be provided either as base64-encoded data (for small images or
    when URL hosting is unavailable) or as public URLs (for large images or
    when bandwidth is constrained). This class enforces mutual exclusivity
    to prevent ambiguity about which source to use.
    
    Security Boundaries:
        - MIME type allowlist prevents arbitrary file types (e.g., executables).
        - Exactly one source (data XOR url) prevents double-fetch attacks.
        - Base64Data and SafeURL types enforce format validity.
    
    Attributes:
        type: Discriminator field (always "image").
        media_type: MIME type from allowlist (default: "image/jpeg").
        data: Base64-encoded image bytes (without data URI prefix).
        url: Public URL to image resource.
    
    Example:
        >>> # From URL
        >>> img = ImagePart(
        ...     type="image",
        ...     url="https://example.com/photo.jpg"
        ... )
        >>> 
        >>> # From base64
        >>> img = ImagePart(
        ...     type="image",
        ...     media_type="image/png",
        ...     data="iVBORw0KGgoAAAANSUhEUgAAAAUA..."
        ... )
    """
    type: Literal[ContentType.IMAGE] = ContentType.IMAGE
    media_type: MimeType = Field(
        default="image/jpeg",
        description="MIME type constrained to safe image formats"
    )
    data: Optional[Base64Data] = Field(
        default=None,
        description="Base64-encoded image data (excludes data URI scheme prefix)"
    )
    url: Optional[SafeURL] = Field(
        default=None,
        description="Public URL to image resource"
    )
    @field_validator("url", mode="before")
    @classmethod
    def _coerce_url_dict(cls, v):
        """Accept a dict wrapper like {'url': 'https://...'} for fixtures.

        Tests sometimes pass url as a small mapping. Allow that shape and
        extract the inner string to satisfy the `SafeURL` annotated type.
        """
        if isinstance(v, dict) and "url" in v:
            return v["url"]
        return v

    @field_validator("data", mode="before")
    @classmethod
    def _coerce_data_dict_or_datauri(cls, v):
        """Accept dict wrappers and extract base64 from data URIs.

        Fixtures may provide a data URI (e.g. 'data:image/png;base64,...')
        or a dict container with key 'url'. Strip prefixes and return only
        the raw base64 payload expected by `Base64Data`.
        """
        if isinstance(v, dict) and "url" in v:
            v = v["url"]

        if isinstance(v, str) and v.startswith("data:"):
            # data:<mediatype>;base64,<data>
            parts = v.split(",", 1)
            if len(parts) == 2:
                return parts[1]
            # fallthrough: if malformed, let Base64Data validator catch it
        return v
    
    @field_validator("media_type")
    @classmethod
    def validate_mime_type(cls, v: str) -> str:
        """Enforces MIME type allowlist to prevent arbitrary file uploads.
        
        Why Allowlist:
            Preventing executable types (application/x-executable, text/html)
            protects downstream systems from code injection if they naively
            render user-provided content.
        
        Supported Types:
            - image/jpeg, image/png: Ubiquitous raster formats.
            - image/gif, image/webp: Modern compression.
            - image/svg+xml: Vector graphics (note: SVG can contain scripts,
              so downstream renderers must sanitize).
        """
        SAFE_MIMES = {
            "image/jpeg",
            "image/png",
            "image/gif",
            "image/webp",
            "image/svg+xml"
        }
        # Increasing this set affects attack surface. SVG is particularly
        # risky due to embedded JavaScript capabilities.
        
        # Compare case-insensitively per RFC but return a canonical lowercased value
        v_norm = v.strip().lower()
        if v_norm not in SAFE_MIMES:
            raise ValueError(
                f"Unsupported image MIME type: '{v}'. "
                f"Allowed types: {SAFE_MIMES}. "
                f"This restriction prevents upload of executable content."
            )
        return v_norm

    @model_validator(mode='after')
    def validate_source(self) -> 'ImagePart':
        """Ensures exactly one source is provided (data XOR url).
        
        Mutual Exclusivity Rationale:
            If both are provided, which takes precedence? Ambiguity leads to
            security vulnerabilities (e.g., URL used for billing, data used
            for rendering, enabling resource exhaustion attacks).
        
        Note: Base64Data and SafeURL already enforce non-empty constraints.
        """
        has_data = self.data is not None
        has_url = self.url is not None
        
        if not has_data and not has_url:
            raise ValueError(
                "ImagePart must specify either 'data' or 'url' source. "
                "Without a source, the image cannot be rendered."
            )
        
        if has_data and has_url:
            raise ValueError(
                "ImagePart cannot specify both 'data' and 'url'. "
                "Ambiguous sources prevent deterministic processing."
            )
        
        return self


class AudioPart(CanonicalRecord):
    """A content fragment representing audio data.
    
    Mirrors ImagePart design for audio media. Enforces MIME type safety and
    source exclusivity to prevent ambiguous interpretation.
    
    Attributes:
        type: Discriminator field (always "audio").
        media_type: MIME type from allowlist (default: "audio/wav").
        data: Base64-encoded audio bytes.
        url: Public URL to audio resource.
    
    Example:
        >>> audio = AudioPart(
        ...     type="audio",
        ...     media_type="audio/mp3",
        ...     url="https://example.com/speech.mp3"
        ... )
    """
    type: Literal[ContentType.AUDIO] = ContentType.AUDIO
    media_type: MimeType = Field(
        default="audio/wav",
        description="MIME type constrained to safe audio formats"
    )
    data: Optional[Base64Data] = Field(
        default=None,
        description="Base64-encoded audio data"
    )
    url: Optional[SafeURL] = Field(
        default=None,
        description="Public URL to audio file"
    )
    @field_validator("url", mode="before")
    @classmethod
    def _coerce_url_dict_audio(cls, v):
        if isinstance(v, dict) and "url" in v:
            return v["url"]
        return v

    @field_validator("data", mode="before")
    @classmethod
    def _coerce_data_dict_or_datauri_audio(cls, v):
        if isinstance(v, dict) and "url" in v:
            v = v["url"]
        if isinstance(v, str) and v.startswith("data:"):
            parts = v.split(",", 1)
            if len(parts) == 2:
                return parts[1]
        return v
    
    @field_validator("media_type")
    @classmethod
    def validate_mime_type(cls, v: str) -> str:
        """Enforces MIME type allowlist for audio formats.
        
        Supported Formats:
            - audio/wav: Uncompressed PCM (large but high quality).
            - audio/mp3, audio/mpeg: Lossy compression (ubiquitous).
            - audio/ogg, audio/webm: Modern open-source codecs.
        
        Excluded: Executable formats (application/octet-stream) and proprietary
        containers that could carry malicious payloads.
        """
        SAFE_MIMES = {
            "audio/wav",
            "audio/mp3",
            "audio/ogg",
            "audio/mpeg",
            "audio/webm"
        }
        # Compare case-insensitively per RFC but return a canonical lowercased value
        v_norm = v.strip().lower()
        if v_norm not in SAFE_MIMES:
            raise ValueError(
                f"Unsupported audio MIME type: '{v}'. "
                f"Allowed types: {SAFE_MIMES}. "
                f"This restriction prevents upload of executable content."
            )
        return v_norm

    @model_validator(mode='after')
    def validate_source(self) -> 'AudioPart':
        """Ensures exactly one source is provided (data XOR url)."""
        has_data = self.data is not None
        has_url = self.url is not None
        
        if not has_data and not has_url:
            raise ValueError(
                "AudioPart must specify either 'data' or 'url' source."
            )
        
        if has_data and has_url:
            raise ValueError(
                "AudioPart cannot specify both 'data' and 'url'. "
                "Ambiguous sources prevent deterministic processing."
            )
        
        return self


# ═══════════════════════════════════════════════════════════════════════════
# TYPE ALIASES
# ═══════════════════════════════════════════════════════════════════════════

ContentPart = Annotated[
    Union[TextPart, ImagePart, AudioPart],
    Field(discriminator='type')
]
# Discriminated union enabling type-safe polymorphism.
# Pydantic automatically routes to the correct class based on 'type' field.