"""SQLAlchemy ORM models for AI MusiMuse.

This module defines the database tables for :class:`Project`,
:class:`Track`, :class:`AnalyzerRun`, and :class:`TrackFeature`.
Audio metadata fields (duration, channels, sample_rate, bit_depth) are
nullable and remain ``NULL`` until the decoder task.

All models inherit from :class:`~database.base.Base`.
"""

from __future__ import annotations

import enum
import uuid
from datetime import UTC, datetime

from sqlalchemy import (
    Boolean,
    DateTime,
    Enum,
    Float,
    ForeignKey,
    Index,
    Integer,
    String,
    Text,
    UniqueConstraint,
)
from sqlalchemy.orm import Mapped, mapped_column, relationship

from database.base import Base


class TrackStatus(enum.Enum):
    """Persistent state of a Track in the database.

    Attributes:
        ACTIVE: The file exists on disk.
        MISSING: The file was deleted from disk.
    """

    ACTIVE = "active"
    MISSING = "missing"


class Project(Base):
    """Represents one musical workspace.

    Attributes:
        id: UUID primary key (string representation).
        name: Human-readable project name.
        created_at: UTC timestamp of project creation.
    """

    __tablename__ = "projects"

    id: Mapped[str] = mapped_column(String(36), primary_key=True)
    name: Mapped[str] = mapped_column(String(255), nullable=False)
    created_at: Mapped[datetime] = mapped_column(
        DateTime, nullable=False, default=lambda: datetime.now(UTC)
    )

    tracks: Mapped[list[Track]] = relationship(
        "Track", back_populates="project", cascade="all, delete-orphan"
    )


class Track(Base):
    """Represents one musical composition (immutable record).

    A Track never changes.  If the audio changes, a TrackVersion is
    created in a future task.

    Attributes:
        id: UUID primary key (string representation).
        project_id: Foreign key to :class:`Project`.
        relative_path: Path relative to the music directory root.
        original_filename: The file's name without directory components.
        sha256: SHA-256 hash of the file contents (hex digest).
        import_date: UTC timestamp of when the track was imported.
        file_size: File size in bytes.
        status: Persistent track state (ACTIVE or MISSING).
        duration: Audio duration in seconds (nullable, future task).
        channels: Number of audio channels (nullable, future task).
        sample_rate: Sample rate in Hz (nullable, future task).
        bit_depth: Bit depth (nullable, future task).
    """

    __tablename__ = "tracks"
    __table_args__ = (
        Index("ix_tracks_project_id", "project_id"),
        Index("ix_tracks_sha256", "sha256"),
        Index("ix_tracks_relative_path", "relative_path"),
        UniqueConstraint("project_id", "relative_path", name="uq_tracks_project_path"),
    )

    id: Mapped[str] = mapped_column(String(36), primary_key=True)
    project_id: Mapped[str] = mapped_column(
        String(36), ForeignKey("projects.id"), nullable=False
    )
    relative_path: Mapped[str] = mapped_column(String(512), nullable=False)
    original_filename: Mapped[str] = mapped_column(String(255), nullable=False)
    sha256: Mapped[str] = mapped_column(String(64), nullable=False)
    import_date: Mapped[datetime] = mapped_column(
        DateTime, nullable=False, default=lambda: datetime.now(UTC)
    )
    file_size: Mapped[int] = mapped_column(Integer, nullable=False)
    status: Mapped[TrackStatus] = mapped_column(
        Enum(TrackStatus), nullable=False, default=TrackStatus.ACTIVE
    )

    duration: Mapped[float | None] = mapped_column(Float, nullable=True)
    channels: Mapped[int | None] = mapped_column(Integer, nullable=True)
    sample_rate: Mapped[int | None] = mapped_column(Integer, nullable=True)
    bit_depth: Mapped[int | None] = mapped_column(Integer, nullable=True)

    project: Mapped[Project] = relationship("Project", back_populates="tracks")
    analyzer_runs: Mapped[list[AnalyzerRun]] = relationship(
        "AnalyzerRun", back_populates="track", cascade="all, delete-orphan"
    )


class AnalyzerRun(Base):
    """Represents one execution of one analyzer.

    Attributes:
        id: UUID primary key (string representation).
        track_id: Foreign key to :class:`Track`.
        analyzer_name: Name of the analyzer (e.g. ``"basic_signal"``).
        analyzer_version: Version of the analyzer (e.g. ``"1.0.0"``).
        started_at: UTC timestamp when execution started.
        finished_at: UTC timestamp when execution finished (nullable).
        execution_time_ms: Wall-clock execution time in milliseconds.
        success: Whether the analyzer completed without errors.
        warnings: JSON-serialized tuple of warning strings (nullable).
    """

    __tablename__ = "analyzer_runs"
    __table_args__ = (
        Index("ix_analyzer_runs_track_id", "track_id"),
        Index("ix_analyzer_runs_analyzer_name", "analyzer_name"),
        UniqueConstraint(
            "track_id",
            "analyzer_name",
            "analyzer_version",
            name="uq_analyzer_runs_track_name_version",
        ),
    )

    id: Mapped[str] = mapped_column(String(36), primary_key=True)
    track_id: Mapped[str] = mapped_column(
        String(36), ForeignKey("tracks.id"), nullable=False
    )
    analyzer_name: Mapped[str] = mapped_column(String(255), nullable=False)
    analyzer_version: Mapped[str] = mapped_column(String(50), nullable=False)
    started_at: Mapped[datetime] = mapped_column(
        DateTime, nullable=False, default=lambda: datetime.now(UTC)
    )
    finished_at: Mapped[datetime | None] = mapped_column(DateTime, nullable=True)
    execution_time_ms: Mapped[float] = mapped_column(Float, nullable=False, default=0.0)
    success: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False)
    warnings: Mapped[str | None] = mapped_column(Text, nullable=True)

    track: Mapped[Track] = relationship("Track", back_populates="analyzer_runs")
    features: Mapped[list[TrackFeature]] = relationship(
        "TrackFeature", back_populates="analyzer_run", cascade="all, delete-orphan"
    )


class TrackFeature(Base):
    """Represents one calculated feature.

    One row = one feature.  Never overwrite.  Never update.
    Never delete.

    Attributes:
        id: UUID primary key (string representation).
        track_id: Foreign key to :class:`Track`.
        analyzer_run_id: Foreign key to :class:`AnalyzerRun`.
        name: Feature name (e.g. ``"peak_left"``).
        value: Serialized feature value.
        unit: Optional unit of measurement (e.g. ``"dBFS"``).
        created_at: UTC timestamp of creation.
    """

    __tablename__ = "track_features"
    __table_args__ = (
        Index("ix_track_features_track_id", "track_id"),
        Index("ix_track_features_analyzer_run_id", "analyzer_run_id"),
        Index("ix_track_features_name", "name"),
    )

    id: Mapped[str] = mapped_column(String(36), primary_key=True)
    track_id: Mapped[str] = mapped_column(
        String(36), ForeignKey("tracks.id"), nullable=False
    )
    analyzer_run_id: Mapped[str] = mapped_column(
        String(36), ForeignKey("analyzer_runs.id"), nullable=False
    )
    name: Mapped[str] = mapped_column(String(255), nullable=False)
    value: Mapped[str | None] = mapped_column(String, nullable=True)
    unit: Mapped[str | None] = mapped_column(String(50), nullable=True)
    created_at: Mapped[datetime] = mapped_column(
        DateTime, nullable=False, default=lambda: datetime.now(UTC)
    )

    analyzer_run: Mapped[AnalyzerRun] = relationship(
        "AnalyzerRun", back_populates="features"
    )


def generate_uuid() -> str:
    """Generate a new UUID4 as a string.

    Returns:
        A string representation of a new UUID.
    """
    return str(uuid.uuid4())
