# Standard library imports
from typing import Optional
from dataclasses import asdict
import datetime as dt
import json

# PyQt imports

# Third party imports
from alchemical import Model  # type: ignore
from sqlalchemy import (
    Boolean,
    DateTime,
    ForeignKey,
    String,
)
from sqlalchemy.ext.associationproxy import association_proxy
from sqlalchemy.ext.hybrid import hybrid_property
from sqlalchemy.engine.interfaces import Dialect
from sqlalchemy.orm import (
    Mapped,
    mapped_column,
    relationship,
)
from sqlalchemy.types import TypeDecorator, TEXT

# App imports
from classes import Filter


class JSONEncodedDict(TypeDecorator):
    """
    Custom JSON Type for MariaDB (since native JSON type is just LONGTEXT)
    """

    impl = TEXT

    def process_bind_param(self, value: dict | None, dialect: Dialect) -> str | None:
        """Convert Python dictionary to JSON string before saving."""
        if value is None:
            return None
        return json.dumps(value, default=lambda o: o.__dict__)

    def process_result_value(self, value: str | None, dialect: Dialect) -> dict | None:
        """Convert JSON string back to Python dictionary after retrieval."""
        if value is None:
            return None
        return json.loads(value)


# Database classes
class NoteColoursTable(Model):
    __tablename__ = "notecolours"

    id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
    substring: Mapped[str] = mapped_column(String(256), index=True, unique=True)
    colour: Mapped[str] = mapped_column(String(21), index=False)
    enabled: Mapped[bool] = mapped_column(default=True, index=True)
    foreground: Mapped[Optional[str]] = mapped_column(String(21), index=False)
    is_regex: Mapped[bool] = mapped_column(default=False, index=False)
    is_casesensitive: Mapped[bool] = mapped_column(default=False, index=False)
    order: Mapped[Optional[int]] = mapped_column(index=True)
    strip_substring: Mapped[bool] = mapped_column(default=True, index=False)

    def __repr__(self) -> str:
        return (
            f"<NoteColours(id={self.id}, substring={self.substring}, "
            f"colour={self.colour}>"
        )


class PlaydatesTable(Model):
    __tablename__ = "playdates"

    id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
    lastplayed: Mapped[dt.datetime] = mapped_column(index=True)
    track_id: Mapped[int] = mapped_column(ForeignKey("tracks.id", ondelete="CASCADE"))
    track: Mapped["TracksTable"] = relationship(
        "TracksTable",
        back_populates="playdates",
    )

    def __repr__(self) -> str:
        return (
            f"<Playdates(id={self.id}, track_id={self.track_id} "
            f"lastplayed={self.lastplayed}>"
        )


class PlaylistsTable(Model):
    """
    Manage playlists
    """

    __tablename__ = "playlists"

    id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
    name: Mapped[str] = mapped_column(String(32), unique=True)
    last_used: Mapped[Optional[dt.datetime]] = mapped_column(DateTime, default=None)
    tab: Mapped[Optional[int]] = mapped_column(default=None)
    open: Mapped[bool] = mapped_column(default=False)
    is_template: Mapped[bool] = mapped_column(default=False)
    rows: Mapped[list["PlaylistRowsTable"]] = relationship(
        "PlaylistRowsTable",
        back_populates="playlist",
        cascade="all, delete-orphan",
        order_by="PlaylistRowsTable.row_number",
    )
    favourite: Mapped[bool] = mapped_column(
        Boolean, nullable=False, index=False, default=False
    )

    def __repr__(self) -> str:
        return (
            f"<Playlists(id={self.id}, name={self.name}, "
            f"is_templatee={self.is_template}, open={self.open}>"
        )


class PlaylistRowsTable(Model):
    __tablename__ = "playlist_rows"

    id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
    row_number: Mapped[int] = mapped_column(index=True)
    note: Mapped[str] = mapped_column(
        String(2048), index=False, default="", nullable=False
    )
    playlist_id: Mapped[int] = mapped_column(
        ForeignKey("playlists.id", ondelete="CASCADE"), index=True
    )

    playlist: Mapped[PlaylistsTable] = relationship(back_populates="rows")
    track_id: Mapped[Optional[int]] = mapped_column(
        ForeignKey("tracks.id", ondelete="CASCADE")
    )
    track: Mapped["TracksTable"] = relationship(
        "TracksTable",
        back_populates="playlistrows",
    )
    played: Mapped[bool] = mapped_column(
        Boolean, nullable=False, index=False, default=False
    )

    def __repr__(self) -> str:
        return (
            f"<PlaylistRows(id={self.id}, playlist_id={self.playlist_id}, "
            f"track_id={self.track_id}, "
            f"note={self.note}, row_number={self.row_number}>"
        )


class QueriesTable(Model):
    __tablename__ = "queries"

    id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
    name: Mapped[str] = mapped_column(String(128), nullable=False)
    _filter_data: Mapped[dict | None] = mapped_column("filter_data", JSONEncodedDict, nullable=False)
    favourite: Mapped[bool] = mapped_column(Boolean, nullable=False, index=False, default=False)

    def _get_filter(self) -> Filter:
        """Convert stored JSON dictionary to a Filter object."""
        if isinstance(self._filter_data, dict):
            return Filter(**self._filter_data)
        return Filter()  # Default object if None or invalid data

    def _set_filter(self, value: Filter | None) -> None:
        """Convert a Filter object to JSON before storing."""
        self._filter_data = asdict(value) if isinstance(value, Filter) else None

    # Single definition of `filter`
    filter = property(_get_filter, _set_filter)

    def __repr__(self) -> str:
        return f"<QueriesTable(id={self.id}, name={self.name}, filter={self.filter})>"


class SettingsTable(Model):
    """Manage settings"""

    __tablename__ = "settings"

    id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
    name: Mapped[str] = mapped_column(String(64), unique=True)
    f_datetime: Mapped[Optional[dt.datetime]] = mapped_column(default=None)
    f_int: Mapped[Optional[int]] = mapped_column(default=None)
    f_string: Mapped[Optional[str]] = mapped_column(String(128), default=None)

    def __repr__(self) -> str:
        return (
            f"<Settings(id={self.id}, name={self.name}, "
            f"f_datetime={self.f_datetime}, f_int={self.f_int}, f_string={self.f_string}>"
        )


class TracksTable(Model):
    __tablename__ = "tracks"

    id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
    artist: Mapped[str] = mapped_column(String(256), index=True)
    bitrate: Mapped[int] = mapped_column(default=None)
    duration: Mapped[int] = mapped_column(index=True)
    fade_at: Mapped[int] = mapped_column(index=False)
    intro: Mapped[Optional[int]] = mapped_column(default=None)
    path: Mapped[str] = mapped_column(String(2048), index=False, unique=True)
    silence_at: Mapped[int] = mapped_column(index=False)
    start_gap: Mapped[int] = mapped_column(index=False)
    title: Mapped[str] = mapped_column(String(256), index=True)

    playlistrows: Mapped[list[PlaylistRowsTable]] = relationship(
        "PlaylistRowsTable",
        back_populates="track",
        cascade="all, delete-orphan",
    )
    playlists = association_proxy("playlistrows", "playlist")
    playdates: Mapped[list[PlaydatesTable]] = relationship(
        "PlaydatesTable",
        back_populates="track",
        cascade="all, delete-orphan",
        lazy="joined",
    )

    def __repr__(self) -> str:
        return (
            f"<Track(id={self.id}, title={self.title}, "
            f"artist={self.artist}, path={self.path}>"
        )
