"""Pydantic models describing Stegawave API payloads."""

from __future__ import annotations

from datetime import datetime
from typing import Any, ClassVar, Dict, List, Literal, Optional, Set, Union

from pydantic import AliasChoices, BaseModel, ConfigDict, Field, HttpUrl, field_validator, model_validator

InputType = Literal[
    "RTP",
    "RTP_FEC",
    "RIST",
    "ZIXI_PUSH",
    "SRT_LISTENER",
    "SRT_CALLER",
]

AdaptiveQuantization = Literal[
    "AUTO",
    "OFF",
    "LOW",
    "MEDIUM",
    "HIGH",
    "HIGHER",
    "MAX",
]

H264Profile = Literal[
    "BASELINE",
    "MAIN",
    "HIGH",
    "HIGH_10BIT",
    "HIGH_422",
    "HIGH_422_10BIT",
]

H265Profile = Literal[
    "MAIN",
    "MAIN_10BIT",
]

ContainerType = Literal["CMAF", "TS", "ISM"]


def _parse_bitrate(bitrate: Union[int, str, None]) -> Optional[int]:
    if bitrate is None:
        return None
    if isinstance(bitrate, int):
        return bitrate
    token = bitrate.strip().lower()
    if token.endswith("k"):
        return int(float(token[:-1]) * 1_000)
    if token.endswith("m"):
        return int(float(token[:-1]) * 1_000_000)
    return int(token)


class SrtCallerDecryption(BaseModel):
    model_config = ConfigDict(extra="forbid")

    Algorithm: Literal["AES128", "AES192", "AES256"]
    Passphrase: str = Field(min_length=16, max_length=64)


class SrtCallerSource(BaseModel):
    model_config = ConfigDict(extra="forbid")

    SrtListenerAddress: str
    SrtListenerPort: int = Field(ge=1, le=65535)
    StreamId: Optional[str] = Field(default=None, max_length=512)
    SrtCallerDecryption: Optional[SrtCallerDecryption] = None


class SrtListenerSettings(BaseModel):
    model_config = ConfigDict(extra="forbid")

    IngestPort: int = Field(ge=1024, le=65535)
    MinLatency: int = Field(default=2000, ge=0, le=60000)
    MaxLatency: Optional[int] = Field(default=None, ge=0, le=60000)
    PassphraseEnabled: Optional[bool] = None
    Passphrase: Optional[str] = Field(default=None, min_length=32, max_length=32)

    @field_validator("IngestPort")
    @classmethod
    def _validate_port(cls, value: int) -> int:
        if value in {2077, 2088}:
            raise ValueError("SRT ingest port cannot be 2077 or 2088")
        return value

    @model_validator(mode="after")
    def _validate_passphrase(self) -> "SrtListenerSettings":
        if self.Passphrase and not (self.PassphraseEnabled or str(self.PassphraseEnabled).upper() in {"TRUE", "1"}):
            raise ValueError("Passphrase provided but PassphraseEnabled is not set")
        return self

# Work around a Pydantic quirk where field/type name collisions lose the annotation.
SrtListenerSettingsType = SrtListenerSettings

class InputConfig(BaseModel):
    model_config = ConfigDict(extra="forbid")

    Type: InputType
    whitelist: Optional[Union[str, List[str]]] = None
    SrtListenerSettings: Optional[SrtListenerSettingsType] = None
    SrtCallerSources: Optional[List[SrtCallerSource]] = None

    @field_validator("Type")
    @classmethod
    def _upper_type(cls, value: str) -> str:
        return value.upper()

    @field_validator("whitelist")
    @classmethod
    def _normalize_whitelist(cls, value: Optional[Union[str, List[str]]]) -> Optional[str]:
        """Normalize whitelist to a single string.
        
        Accepts:
        - None -> None
        - Empty string -> None
        - String -> validated string
        - Empty list -> None
        - List with 1 element -> validated string
        - List with 2+ elements -> ValueError
        """
        if value is None:
            return None
            
        if isinstance(value, str):
            # Empty string -> None
            if not value.strip():
                return None
            return value
            
        if isinstance(value, list):
            if len(value) == 0:
                return None
            if len(value) > 1:
                raise ValueError(
                    f"Only one IP address/CIDR block is allowed in the whitelist. "
                    f"Got {len(value)} entries: {value}"
                )
            # Extract single element
            cidr = value[0]
            if not isinstance(cidr, str):
                raise ValueError(f"Whitelist CIDR must be a string, got {type(cidr)}")
            if not cidr.strip():
                return None
            return cidr
            
        raise ValueError(f"Whitelist must be a string or list, got {type(value)}")

    @model_validator(mode="after")
    def _validate_structure(self) -> "InputConfig":
        if self.Type == "SRT_LISTENER":
            if not self.SrtListenerSettings:
                raise ValueError("SRT_LISTENER inputs require SrtListenerSettings")
        if self.Type == "SRT_CALLER" and not self.SrtCallerSources:
            raise ValueError("SRT_CALLER inputs require SrtCallerSources")
        return self


class InputSpecification(BaseModel):
    model_config = ConfigDict(extra="forbid")

    Codec: Literal["AVC", "HEVC"] = "AVC"
    MaximumBitrate: Literal["MAX_10_MBPS", "MAX_20_MBPS", "MAX_50_MBPS"] = "MAX_20_MBPS"
    Resolution: Literal["SD", "HD", "UHD"] = "HD"


class OutputConfig(BaseModel):
    model_config = ConfigDict(extra="forbid")

    OutputName: Optional[str] = None
    resolution: Optional[str] = None
    Width: Optional[int] = Field(default=None, gt=0, le=7680)
    Height: Optional[int] = Field(default=None, gt=0, le=4320)
    FramerateNumerator: int = Field(gt=0)
    FramerateDenominator: int = Field(gt=0)
    VideoBitrate: Optional[int] = Field(default=None, gt=0)
    Bitrate: Optional[int] = Field(default=None, gt=0)
    AudioBitrate: Optional[int] = Field(default=128_000, gt=0)
    SampleRate: Optional[int] = Field(default=48_000, gt=0)
    Profile: Optional[Union[H264Profile, H265Profile]] = None
    AdaptiveQuantization: Optional[AdaptiveQuantization] = None

    @model_validator(mode="after")
    def _validate_resolution(self) -> "OutputConfig":
        if not self.resolution and not (self.Width and self.Height):
            raise ValueError("Output must define either resolution string or Width/Height")
        if self.resolution and (self.Width or self.Height):
            raise ValueError("Provide resolution string or Width/Height, not both")
        if self.resolution:
            parts = self.resolution.lower().split("x")
            if len(parts) != 2:
                raise ValueError("resolution must be in WIDTHxHEIGHT format")
            width, height = parts
            if not width.isdigit() or not height.isdigit():
                raise ValueError("resolution must contain numeric width/height")
        return self

    @model_validator(mode="after")
    def _coerce_bitrates(self) -> "OutputConfig":
        parsed_video = _parse_bitrate(self.VideoBitrate or self.Bitrate)
        if parsed_video is None:
            parsed_video = 5_000_000
        object.__setattr__(self, "VideoBitrate", parsed_video)
        object.__setattr__(self, "Bitrate", parsed_video)
        object.__setattr__(self, "AudioBitrate", _parse_bitrate(self.AudioBitrate) or 128_000)
        return self


class OutputGroup(BaseModel):
    model_config = ConfigDict(extra="forbid")

    Name: str = "cmaf-main"
    Outputs: List[OutputConfig]

    @model_validator(mode="after")
    def _validate_framerate(self) -> "OutputGroup":
        numerators = {o.FramerateNumerator for o in self.Outputs}
        denominators = {o.FramerateDenominator for o in self.Outputs}
        if len(numerators) > 1 or len(denominators) > 1:
            raise ValueError("All outputs must share the same framerate")
        return self


class EncoderConfig(BaseModel):
    model_config = ConfigDict(extra="forbid")

    vodArchive: bool = False
    InputSpecification: Optional[InputSpecification] = None
    Outputs: List[OutputConfig]


class HlsManifest(BaseModel):
    model_config = ConfigDict(extra="forbid")

    ManifestName: str = "index"
    ManifestWindowSeconds: Optional[int] = Field(default=360, ge=30, le=3600)
    ProgramDateTimeIntervalSeconds: Optional[int] = Field(default=None, ge=0, le=3600)
    ChildManifestName: Optional[str] = None


class DashManifest(BaseModel):
    model_config = ConfigDict(extra="forbid")

    ManifestName: str = "index"
    ManifestWindowSeconds: Optional[int] = Field(default=360, ge=30, le=3600)
    MinUpdatePeriodSeconds: Optional[int] = Field(default=None, ge=1, le=120)
    MinBufferTimeSeconds: Optional[int] = Field(default=None, ge=1, le=900)
    SuggestedPresentationDelaySeconds: Optional[int] = Field(default=None, ge=1, le=900)


class MssManifest(BaseModel):
    model_config = ConfigDict(extra="forbid")

    ManifestName: str = "index"
    ManifestWindowSeconds: Optional[int] = Field(default=360, ge=30, le=3600)


class OriginEndpoint(BaseModel):
    model_config = ConfigDict(extra="forbid")

    name: str
    ContainerType: ContainerType = "CMAF"
    description: Optional[str] = None
    HlsManifests: Optional[List[HlsManifest]] = None
    DashManifests: Optional[List[DashManifest]] = None
    MssManifests: Optional[List[MssManifest]] = None
    StartoverWindowSeconds: Optional[int] = Field(default=None, ge=60, le=1_209_600)
    TsUseAudioRenditionGroup: Optional[bool] = None
    drm: Optional[DrmConfig] = Field(default=None, validation_alias=AliasChoices("drm", "DRM"))

    @model_validator(mode="after")
    def _validate_manifests(self) -> "OriginEndpoint":
        if not any([self.HlsManifests, self.DashManifests, self.MssManifests]):
            raise ValueError("At least one manifest type must be provided")
        return self


class PackagerConfig(BaseModel):
    model_config = ConfigDict(extra="forbid")

    originEndpoints: List[OriginEndpoint]


class CreatePipelineRequest(BaseModel):
    model_config = ConfigDict(extra="forbid")

    name: str
    description: Optional[str] = None
    segmentDuration: int = Field(default=4, ge=1, le=30)
    autoStart: bool = False
    input: InputConfig
    encoder: EncoderConfig
    packager: PackagerConfig


class CreatePipelineResponse(BaseModel):
    model_config = ConfigDict(extra="forbid", populate_by_name=True)

    message: str
    eventID: str = Field(validation_alias=AliasChoices("eventID", "eventId"))
    status: str
    note: Optional[str] = None

    def is_success(self) -> bool:
        status = (self.status or "").lower()
        return status not in {"failed", "error", "invalid"}

    def valid(self) -> bool:
        return self.is_success()


class PipelineInputStatus(BaseModel):
    model_config = ConfigDict(extra="allow", validate_assignment=False, arbitrary_types_allowed=True)

    protocol: Optional[str] = None
    endpoints: Optional[List[str]] = None
    endpoint: Optional[str] = None  # Legacy field for backward compatibility
    allowedIPs: Optional[List[str]] = None
    latency: Optional[int] = None
    passphraseEnabled: Optional[bool] = None
    passphrase: Optional[str] = None


class PipelineEncoderProfile(BaseModel):
    model_config = ConfigDict(extra="allow", validate_assignment=False, arbitrary_types_allowed=True)

    name: Optional[str] = None
    width: Optional[int] = None
    height: Optional[int] = None
    videoBitrate: Optional[int] = None
    audioBitrate: Optional[int] = None
    videoCodec: Optional[str] = None
    audioCodec: Optional[str] = None


class PipelineEncoderStatus(BaseModel):
    model_config = ConfigDict(extra="allow", validate_assignment=False, arbitrary_types_allowed=True)

    profiles: Optional[List[PipelineEncoderProfile]] = None
    vodArchive: Optional[bool] = None
    archive: Optional[Dict[str, Any]] = None  # Deprecated: kept for backward compatibility
    
    @model_validator(mode="after")
    def _normalize_archive_fields(self) -> "PipelineEncoderStatus":
        """Handle both old (archive) and new (vodArchive) formats gracefully."""
        # If we got old format (archive dict) but no vodArchive, extract it
        if self.archive is not None and self.vodArchive is None:
            self.vodArchive = self.archive.get("enabled", False)
        
        # If we got new format (vodArchive bool) but no archive dict, create it for old code
        elif self.vodArchive is not None and self.archive is None:
            self.archive = {"enabled": self.vodArchive}
        
        # Ensure vodArchive defaults to False if neither provided
        if self.vodArchive is None:
            self.vodArchive = False
            
        return self


class ManifestInfo(BaseModel):
    """Manifest information with type and name."""
    model_config = ConfigDict(extra="allow", validate_assignment=False, arbitrary_types_allowed=True)

    type: Optional[str] = None
    name: Optional[str] = None


class PipelinePackagerEndpoint(BaseModel):
    model_config = ConfigDict(extra="allow", validate_assignment=False, arbitrary_types_allowed=True)

    containerFormat: Optional[str] = None
    segmentLength: Optional[int] = None
    manifests: Optional[List[Union[ManifestInfo, str, Dict[str, Any]]]] = None  # Support both new format and legacy
    startoverWindow: Optional[int] = None


class PipelinePackagerStatus(BaseModel):
    model_config = ConfigDict(extra="allow", validate_assignment=False, arbitrary_types_allowed=True)

    endpoints: Optional[List[PipelinePackagerEndpoint]] = None


class CdnEndpoint(BaseModel):
    """CDN endpoint with protocol and URL."""
    model_config = ConfigDict(extra="allow", validate_assignment=False, arbitrary_types_allowed=True)

    protocol: Optional[str] = None
    url: Optional[str] = None


class PipelineCdnStatus(BaseModel):
    model_config = ConfigDict(extra="allow", validate_assignment=False, arbitrary_types_allowed=True)

    endpoints: Optional[List[Union[CdnEndpoint, str, Dict[str, Any]]]] = None  # Support both new format and legacy


class DetectedUser(BaseModel):
    """Information about a detected user from watermark analysis."""
    model_config = ConfigDict(extra="allow", validate_assignment=False, arbitrary_types_allowed=True)

    user: Optional[str] = None
    user_key: Optional[str] = None
    similarity: Optional[Union[float, str]] = None  # Can be float or string like "0.992"
    detected_at: Optional[str] = None


class PipelineStatusResponse(BaseModel):
    model_config = ConfigDict(extra="allow", validate_assignment=False, arbitrary_types_allowed=True)

    eventID: Optional[str] = None
    name: Optional[str] = None
    description: Optional[str] = None
    status: Optional[str] = None
    deleted: Optional[bool] = None
    createdAt: Optional[datetime] = None
    lastUpdated: Optional[datetime] = None
    input: Optional[PipelineInputStatus] = None
    encoder: Optional[PipelineEncoderStatus] = None
    packager: Optional[PipelinePackagerStatus] = None
    cdn: Optional[PipelineCdnStatus] = None
    detected_users: Optional[List[Union[DetectedUser, str, Dict[str, Any]]]] = None
    schedule: Optional[List[Dict[str, Any]]] = None  

    PROVISIONING_STATUSES: ClassVar[Set[str]] = {"provisioning", "creating", "pending", "initializing"}
    TERMINAL_FAILURE_STATUSES: ClassVar[Set[str]] = {"failed", "error"}

    def is_ready(self) -> bool:
        status = (self.status or "").lower()
        if not status:
            return False
        if status in self.TERMINAL_FAILURE_STATUSES:
            return False
        return status not in self.PROVISIONING_STATUSES

    def is_terminal_failure(self) -> bool:
        status = (self.status or "").lower()
        return status in self.TERMINAL_FAILURE_STATUSES


class PipelineListEntry(BaseModel):
    model_config = ConfigDict(extra="allow", validate_assignment=False, arbitrary_types_allowed=True)

    eventID: Optional[str] = None
    name: Optional[str] = None
    status: Optional[str] = None
    description: Optional[str] = None
    deleted: Optional[bool] = None
    createdAt: Optional[datetime] = None


class PipelineListResponse(BaseModel):
    model_config = ConfigDict(extra="allow", validate_assignment=False, arbitrary_types_allowed=True)

    pipelines: Optional[List[PipelineListEntry]] = None


class StateRequest(BaseModel):
    model_config = ConfigDict(extra="forbid")

    eventID: str
    action: Literal["status", "start", "stop"]


class StateResponse(BaseModel):
    model_config = ConfigDict(extra="allow")

    message: str
    eventID: str
    action: Literal["status", "start", "stop"]
    state: Optional[str] = None


class DeleteResponse(BaseModel):
    model_config = ConfigDict(extra="forbid")

    message: str
    eventID: str
    status: str
    note: Optional[str] = None


class ResetHistoryResponse(BaseModel):
    model_config = ConfigDict(extra="forbid")

    message: str
    eventID: str
    note: Optional[str] = None


class TokenResponse(BaseModel):
    model_config = ConfigDict(extra="forbid")

    tokens: Dict[str, str]

    @model_validator(mode="before")
    @classmethod
    def _wrap(cls, value):  # type: ignore[override]
        if isinstance(value, dict) and "tokens" not in value:
            return {"tokens": value}
        return value


class DecodeJobRequest(BaseModel):
    model_config = ConfigDict(extra="forbid")

    eventID: str
    input_stream: HttpUrl


class DecodeJobResponse(BaseModel):
    model_config = ConfigDict(extra="forbid")

    message: str
    eventID: str
    clientID: Optional[str] = None


class IptvQueryRequest(BaseModel):
    model_config = ConfigDict(extra="forbid")

    server: HttpUrl
    username: str
    password: str
    channelName: str
    categoryId: Optional[int] = None
    format: Optional[str] = None
    preferHD: Optional[bool] = None
    preferUK: Optional[bool] = None
    avoidVIP: Optional[bool] = None


class IptvStream(BaseModel):
    model_config = ConfigDict(extra="forbid")

    name: str
    stream_id: int
    stream_urls: List[HttpUrl]


class IptvQueryResponse(BaseModel):
    model_config = ConfigDict(extra="forbid")

    results: List[IptvStream]

    @model_validator(mode="before")
    @classmethod
    def _wrap(cls, value):  # type: ignore[override]
        if isinstance(value, list):
            return {"results": value}
        return value


# DRM Configuration Models
DrmSystem = Literal["WIDEVINE", "PLAYREADY", "FAIRPLAY", "IRDETO", "CLEAR_KEY_AES_128"]
EncryptionMethod = Literal["CENC", "CBCS", "SAMPLE_AES", "AES_128"]


class DrmKeyServerConfig(BaseModel):
    """SPEKE key server configuration."""
    model_config = ConfigDict(extra="forbid")

    url: HttpUrl
    headers: Optional[Dict[str, str]] = None
    query: Optional[Dict[str, str]] = Field(default=None, validation_alias=AliasChoices("query", "queryParams"))


class DrmConfig(BaseModel):
    """DRM encryption configuration for origin endpoints."""
    model_config = ConfigDict(extra="forbid")

    encryptionMethod: EncryptionMethod
    drmSystems: List[DrmSystem] = Field(min_length=1)
    resourceId: str
    keyServer: DrmKeyServerConfig
    keyRotationIntervalSeconds: Optional[int] = Field(default=None, ge=1)
    constantInitializationVector: Optional[str] = Field(default=None, pattern=r'^[0-9A-Fa-f]{32}$')
    cmafExcludeSegmentDrmMetadata: Optional[bool] = None


# Scheduling Models
ScheduledActionType = Literal["start", "stop", "delete"]


class ScheduleActionRequest(BaseModel):
    """Request to schedule a pipeline action."""
    model_config = ConfigDict(extra="forbid")

    action: ScheduledActionType
    eventID: str
    scheduledTime: str  # ISO 8601 format


class ScheduleActionResponse(BaseModel):
    """Response from schedule-actions endpoint."""
    model_config = ConfigDict(extra="allow")

    message: str
    actionID: str
    ruleName: str
    scheduledTime: str
    action: str
    eventID: str
    clientID: str


class CancelScheduledActionRequest(BaseModel):
    """Request to cancel a scheduled action."""
    model_config = ConfigDict(extra="forbid")

    actionID: str
    eventID: str


class CancelScheduledActionResponse(BaseModel):
    """Response from cancel-scheduled-action endpoint."""
    model_config = ConfigDict(extra="allow")

    message: str
    actionID: str
    eventID: str
    action: str
    scheduledTime: str


class VodArchiveFile(BaseModel):
    """Individual VOD archive file with presigned download URL."""
    model_config = ConfigDict(extra="allow")

    index: int
    filename: str
    url: str
    size: str  # Human-readable format (e.g., "50 MB")
    sizeBytes: int
    lastModified: str  # ISO format timestamp


class VodArchiveInfo(BaseModel):
    """Archive metadata and summary information."""
    model_config = ConfigDict(extra="allow")

    enabled: bool
    fileCount: int
    totalSize: str  # Human-readable format
    totalSizeBytes: int
    expiresIn: int  # Seconds
    expiresAt: str  # ISO format timestamp


class VodArchiveResponse(BaseModel):
    """Response from get-vod-archive endpoint."""
    model_config = ConfigDict(extra="allow")

    eventID: str
    clientID: str
    eventName: str
    eventDeleted: Optional[bool] = None
    archive: VodArchiveInfo
    files: List[VodArchiveFile]


# Fix for Pydantic 2 with __future__ annotations
# Rebuild models with proper namespace to resolve forward references
import sys
_current_module = sys.modules[__name__]
for _model_name in ['InputConfig', 'CreatePipelineRequest', 'EncoderConfig', 'PackagerConfig']:
    if hasattr(_current_module, _model_name):
        _model = getattr(_current_module, _model_name)
        _model.model_rebuild(_types_namespace=vars(_current_module))
del _current_module, _model_name, _model
