# Standard library imports
# Allow forward reference to PlaylistModel
from __future__ import annotations

from collections.abc import Callable
from dataclasses import dataclass
from typing import Optional
import datetime as dt

# PyQt imports
from PyQt6.QtCore import (
    QAbstractTableModel,
    QModelIndex,
    Qt,
    QVariant,
)
from PyQt6.QtGui import (
    QBrush,
    QColor,
    QFont,
)

# Third party imports
from sqlalchemy.orm.session import Session

# import snoop  # type: ignore

# App imports
from classes import (
    ApplicationError,
    Filter,
    QueryCol,
)
from config import Config
from helpers import (
    file_is_unreadable,
    get_relative_date,
    ms_to_mmss,
    show_warning,
)
from log import log
from models import db, Playdates, Tracks
from playlistrow import PlaylistRow


@dataclass
class QueryRow:
    artist: str
    bitrate: int
    duration: int
    lastplayed: Optional[dt.datetime]
    path: str
    title: str
    track_id: int


class QuerylistModel(QAbstractTableModel):
    """
    The Querylist Model

    Used to support query lists. The underlying database is never
    updated. We just present tracks that match a query and allow the user
    to copy those to a playlist.

    """

    def __init__(self, session: Session, filter: Filter) -> None:
        """
        Load query
        """

        log.debug(f"QuerylistModel.__init__({filter=})")

        super().__init__()
        self.session = session
        self.filter = filter

        self.querylist_rows: dict[int, QueryRow] = {}
        self._selected_rows: set[int] = set()

        self.load_data()

    def __repr__(self) -> str:
        return f"<QuerylistModel: filter={self.filter}, {self.rowCount()} rows>"

    def _background_role(self, row: int, column: int, qrow: QueryRow) -> QBrush:
        """Return background setting"""

        # Unreadable track file
        if file_is_unreadable(qrow.path):
            return QBrush(QColor(Config.COLOUR_UNREADABLE))

        # Selected row
        if row in self._selected_rows:
            return QBrush(QColor(Config.COLOUR_QUERYLIST_SELECTED))

        # Individual cell colouring
        if column == QueryCol.BITRATE.value:
            if not qrow.bitrate or qrow.bitrate < Config.BITRATE_LOW_THRESHOLD:
                return QBrush(QColor(Config.COLOUR_BITRATE_LOW))
            elif qrow.bitrate < Config.BITRATE_OK_THRESHOLD:
                return QBrush(QColor(Config.COLOUR_BITRATE_MEDIUM))
            else:
                return QBrush(QColor(Config.COLOUR_BITRATE_OK))

        return QBrush()

    def columnCount(self, parent: QModelIndex = QModelIndex()) -> int:
        """Standard function for view"""

        return len(QueryCol)

    def data(
        self, index: QModelIndex, role: int = Qt.ItemDataRole.DisplayRole
    ) -> QVariant:
        """Return data to view"""

        if (
            not index.isValid()
            or not (0 <= index.row() < len(self.querylist_rows))
            or role
            in [
                Qt.ItemDataRole.CheckStateRole,
                Qt.ItemDataRole.DecorationRole,
                Qt.ItemDataRole.EditRole,
                Qt.ItemDataRole.FontRole,
                Qt.ItemDataRole.ForegroundRole,
                Qt.ItemDataRole.InitialSortOrderRole,
                Qt.ItemDataRole.SizeHintRole,
                Qt.ItemDataRole.StatusTipRole,
                Qt.ItemDataRole.TextAlignmentRole,
                Qt.ItemDataRole.WhatsThisRole,
            ]
        ):
            return QVariant()

        row = index.row()
        column = index.column()
        # rat for playlist row data as it's used a lot
        qrow = self.querylist_rows[row]

        # Dispatch to role-specific functions
        dispatch_table: dict[int, Callable] = {
            int(Qt.ItemDataRole.BackgroundRole): self._background_role,
            int(Qt.ItemDataRole.DisplayRole): self._display_role,
            int(Qt.ItemDataRole.ToolTipRole): self._tooltip_role,
        }

        if role in dispatch_table:
            return QVariant(dispatch_table[role](row, column, qrow))

        # Fall through to no-op
        return QVariant()

    def _display_role(self, row: int, column: int, qrow: QueryRow) -> str:
        """
        Return text for display
        """

        dispatch_table = {
            QueryCol.ARTIST.value: qrow.artist,
            QueryCol.BITRATE.value: str(qrow.bitrate),
            QueryCol.DURATION.value: ms_to_mmss(qrow.duration),
            QueryCol.LAST_PLAYED.value: get_relative_date(qrow.lastplayed),
            QueryCol.TITLE.value: qrow.title,
        }
        if column in dispatch_table:
            return dispatch_table[column]

        return ""

    def flags(self, index: QModelIndex) -> Qt.ItemFlag:
        """
        Standard model flags
        """

        if not index.isValid():
            return Qt.ItemFlag.NoItemFlags

        return Qt.ItemFlag.ItemIsEnabled | Qt.ItemFlag.ItemIsSelectable

    def get_selected_track_ids(self) -> list[int]:
        """
        Return a list of track_ids from selected tracks
        """

        return [self.querylist_rows[row].track_id for row in self._selected_rows]

    def headerData(
        self,
        section: int,
        orientation: Qt.Orientation,
        role: int = Qt.ItemDataRole.DisplayRole,
    ) -> QVariant:
        """
        Return text for headers
        """

        display_dispatch_table = {
            QueryCol.TITLE.value: QVariant(Config.HEADER_TITLE),
            QueryCol.ARTIST.value: QVariant(Config.HEADER_ARTIST),
            QueryCol.DURATION.value: QVariant(Config.HEADER_DURATION),
            QueryCol.LAST_PLAYED.value: QVariant(Config.HEADER_LAST_PLAYED),
            QueryCol.BITRATE.value: QVariant(Config.HEADER_BITRATE),
        }

        if role == Qt.ItemDataRole.DisplayRole:
            if orientation == Qt.Orientation.Horizontal:
                return display_dispatch_table[section]
            else:
                if Config.ROWS_FROM_ZERO:
                    return QVariant(str(section))
                else:
                    return QVariant(str(section + 1))

        elif role == Qt.ItemDataRole.FontRole:
            boldfont = QFont()
            boldfont.setBold(True)
            return QVariant(boldfont)

        return QVariant()

    def load_data(self) -> None:
        """
        Populate self.querylist_rows
        """

        # Clear any exsiting rows
        self.querylist_rows = {}
        row = 0

        try:
            results = Tracks.get_filtered_tracks(self.session, self.filter)
            for result in results:
                lastplayed = None
                if hasattr(result, "playdates"):
                    pds = result.playdates
                    if pds:
                        lastplayed = max([a.lastplayed for a in pds])
                queryrow = QueryRow(
                    artist=result.artist,
                    bitrate=result.bitrate or 0,
                    duration=result.duration,
                    lastplayed=lastplayed,
                    path=result.path,
                    title=result.title,
                    track_id=result.id,
                )

                self.querylist_rows[row] = queryrow
                row += 1
        except ApplicationError as e:
            show_warning(None, "Query error", f"Error loading query data ({e})")

    def rowCount(self, index: QModelIndex = QModelIndex()) -> int:
        """Standard function for view"""

        return len(self.querylist_rows)

    def toggle_row_selection(self, row: int) -> None:
        if row in self._selected_rows:
            self._selected_rows.discard(row)
        else:
            self._selected_rows.add(row)

        # Emit dataChanged for the entire row
        top_left = self.index(row, 0)
        bottom_right = self.index(row, self.columnCount() - 1)
        self.dataChanged.emit(top_left, bottom_right, [Qt.ItemDataRole.BackgroundRole])

    def _tooltip_role(self, row: int, column: int, rat: PlaylistRow) -> str | QVariant:
        """
        Return tooltip. Currently only used for last_played column.
        """

        if column != QueryCol.LAST_PLAYED.value:
            return QVariant()
        with db.Session() as session:
            track_id = self.querylist_rows[row].track_id
            if not track_id:
                return QVariant()
            playdates = Playdates.last_playdates(session, track_id)
            return (
                "<br>".join(
                    [
                        a.lastplayed.strftime(Config.LAST_PLAYED_TOOLTIP_DATE_FORMAT)
                        for a in reversed(playdates)
                    ]
                )
            )
