"""Data objects for acoupi System."""

import datetime
from dataclasses import dataclass
from enum import IntEnum
from pathlib import Path
from typing import Any, List, Optional, Tuple, Union
from uuid import UUID, uuid4

from pydantic import BaseModel, ConfigDict, Field, field_validator

__all__ = [
    "TimeInterval",
    "Deployment",
    "Recording",
    "PredictedTag",
    "Detection",
    "ModelOutput",
    "ModelOutputInfo",
    "Message",
    "ResponseStatus",
    "Response",
    "BoundingBox",
]


class TimeInterval(BaseModel):
    """An interval of time between two times of day."""

    start: datetime.time
    """Start time of the interval."""

    end: datetime.time
    """End time of the interval."""


class DeviceInfo(BaseModel):
    """Runtime information about the current device.

    This object is intentionally small and currently only exposes the device
    identifier used in filename templates and heartbeat-style messages.
    """

    id: str
    """Unique identifier for the current device, if available."""


class Deployment(BaseModel):
    """A Deployment captures information about the device deployment.

    This includes the latitude, longitude, and deployment start.
    """

    id: UUID = Field(default_factory=uuid4)
    """The unique ID of the deployment."""

    name: str
    """User provided name of the deployment."""

    latitude: Optional[float] = None
    """The latitude of the site where the device is deployed."""

    longitude: Optional[float] = None
    """The longitude of the site where the device is deployed."""

    started_on: datetime.datetime = Field(
        default_factory=datetime.datetime.now
    )
    """The datetime when the device was deployed."""

    ended_on: Optional[datetime.datetime] = None
    """The datetime when the deployment ended."""

    @field_validator("latitude")
    def validate_latitude(cls, value):
        """Validate that the latitude are within range."""
        if value is not None and (value < -90 or value > 90):
            raise ValueError("latitude must be between -90 and 90")
        return value

    @field_validator("longitude")
    def validate_longitude(cls, value):
        """Validate that the longitude are within range."""
        if value is not None and (value < -180 or value > 180):
            raise ValueError("longitude must be between -180 and 180")
        return value


class Recording(BaseModel):
    """A Recording is a single audio file recorded from the microphone."""

    created_on: datetime.datetime = Field(
        default_factory=datetime.datetime.now,
        repr=False,
    )
    """The datetime when the recording was made"""

    duration: float = Field(
        repr=False,
    )
    """The duration of the recording in seconds"""

    samplerate: int = Field(repr=False)
    """The samplerate of the recording in Hz"""

    deployment: Deployment = Field(repr=False)
    """The deployment that the recording belongs to"""

    path: Optional[Path] = Field(None, repr=True)
    """The path to the audio file in the local filesystem"""

    audio_channels: Optional[int] = Field(default=1, repr=False)
    """The number of audio_channels in the recording."""

    chunksize: Optional[int] = Field(default=4096, repr=False)
    """The chunksize of the audio file in bytes. Defaults to 4096."""

    time_expansion: float = Field(default=1, repr=False, gt=0)
    """Factor by which the recording's time scale is multiplied.

    Values > 1.0 indicate time expansion (slowing down playback), while values
    between 0.0 and 1.0 indicate time compression (speeding up playback).
    """

    id: UUID = Field(default_factory=uuid4, repr=True)
    """The unique ID of the recording"""

    @field_validator("duration")
    def validate_duration(cls, value):
        """Validate that the duration is greater than 0."""
        if value <= 0:
            raise ValueError("duration must be greater than 0")
        return value

    @field_validator("samplerate")
    def validate_samplerate(cls, value):
        """Validate that the samplerate is greater than 0."""
        if value <= 0:
            raise ValueError("samplerate must be greater than 0")
        return value

    @field_validator("audio_channels")
    def validate_audio_channels(cls, value):
        """Validate that the number of audio_channels is greater than 1."""
        if value <= 0:
            raise ValueError("audio_channels must be 1 or greater")
        return value


@dataclass(slots=True, frozen=True)
class Tag:
    """A Tag is a label for a recording."""

    key: str
    """The key of the tag."""

    value: str
    """The value of the tag."""


@dataclass(slots=True, frozen=True)
class PredictedTag:
    """A PredictedTag is a label predicted by a model.

    It consists of a key, a value and a score.
    """

    tag: Tag
    """The tag predicted by the model."""

    confidence_score: float = 1
    """The confidence score of the predicted tag."""


class BoundingBox(BaseModel):
    """BoundingBox to locate a sound event in time and frequency.

    All time values are in seconds and all frequency values are in Hz.
    """

    model_config = ConfigDict(frozen=True)

    type: str = "BoundingBox"

    coordinates: Tuple[float, float, float, float]

    @classmethod
    def from_coordinates(
        cls,
        start_time: float,
        low_freq: float,
        end_time: float,
        high_freq: float,
    ):
        """Create a BoundingBox from coordinates."""
        return cls(
            coordinates=(start_time, low_freq, end_time, high_freq),
        )

    @field_validator("coordinates")
    def validate_coordinates(cls, value):
        """Validate that the coordinates are within range."""
        start_time, low_freq, end_time, high_freq = value

        if end_time <= start_time:
            raise ValueError("end time must be greater than start time")

        if low_freq >= high_freq:
            raise ValueError(
                "high frequency must be greater than low frequency"
            )

        return value


class Detection(BaseModel):
    """A Detection is a single prediction from a model."""

    model_config = ConfigDict(frozen=True)

    id: UUID = Field(default_factory=uuid4)
    """The unique ID of the detection"""

    location: Optional[BoundingBox] = None
    """The location of the detection in the recording."""

    detection_score: float = 1
    """The score of the detection."""

    tags: List[PredictedTag] = Field(default_factory=list)
    """The tags predicted by the model for the detection."""

    @field_validator("detection_score")
    def validate_score(cls, value):
        """Validate that the score is between 0 and 1."""
        if value < 0 or value > 1:
            raise ValueError("score must be between 0 and 1")
        return value

    def __hash__(self) -> int:
        return hash(
            (
                self.id,
                self.location,
                self.detection_score,
                tuple(sorted(hash(t) for t in self.tags)),
            )
        )

    def __eq__(self, other: Any) -> bool:
        if not isinstance(other, Detection):
            return NotImplemented

        # Check standard fields first by creating a dict copy without the list fields
        self_dict = self.model_dump(exclude={"tags"})
        other_dict = other.model_dump(exclude={"tags"})

        if self_dict != other_dict:
            return False

        self_tags = set(self.tags)
        other_tags = set(other.tags)
        return self_tags == other_tags


class ModelOutput(BaseModel):
    """The output of a model."""

    model_config = ConfigDict(frozen=True)

    id: UUID = Field(default_factory=uuid4)
    """The unique ID of the model output."""

    name_model: str
    """The name of the model that produced the output."""

    recording: Recording
    """The recording that was used as input to the model."""

    tags: List[PredictedTag] = Field(default_factory=list)
    """The tags predicted by the model at the recording level."""

    detections: List[Detection] = Field(default_factory=list)
    """List of predicted sound events in the recording."""

    created_on: datetime.datetime = Field(
        default_factory=datetime.datetime.now
    )
    """The datetime when the model output was created."""

    def __eq__(self, other: Any) -> bool:
        if not isinstance(other, ModelOutput):
            return NotImplemented

        # Check standard fields first by creating a dict copy without the list fields
        self_dict = self.model_dump(exclude={"tags", "detections"})
        other_dict = other.model_dump(exclude={"tags", "detections"})

        if self_dict != other_dict:
            return False

        self_tags = set(self.tags)
        other_tags = set(other.tags)

        if self_tags != other_tags:
            return False

        self_detections = set(d for d in self.detections)
        other_detections = set(d for d in other.detections)
        return self_detections == other_detections


class ModelOutputInfo(BaseModel):
    """Lightweight model-output information for management-style queries."""

    id: UUID = Field(default_factory=uuid4)
    """The unique ID of the model output."""

    name_model: str
    """The name of the model that produced the output."""

    created_on: datetime.datetime = Field(
        default_factory=datetime.datetime.now
    )
    """The datetime when the model output was created."""


class Message(BaseModel):
    """The message to be sent to the remote server."""

    id: UUID = Field(default_factory=uuid4)
    """The unique ID of the message."""

    content: Union[str, bytes]
    """The message to be sent. Usually JSON text, but may be raw bytes."""

    created_on: datetime.datetime = Field(
        default_factory=datetime.datetime.now
    )
    """The datetime when the message was created."""


class ResponseStatus(IntEnum):
    """The status of a message."""

    SUCCESS = 0
    """The message was received successfully."""

    FAILED = 1
    """The message failed to send."""

    ERROR = 2
    """The message was sent, but there was an error."""

    TIMEOUT = 3
    """The message timed out."""


class Response(BaseModel):
    """The response from sending a message."""

    status: ResponseStatus
    """The status of the message."""

    message: Message
    """The message that was sent."""

    content: Optional[str] = None
    """The content of the response."""

    received_on: datetime.datetime = Field(
        default_factory=datetime.datetime.now
    )
    """The datetime the message was received."""
