from enum import StrEnum
from typing import Any, Optional
from dataclasses import dataclass, field

from sqlalchemy.orm import aliased
from sqlalchemy import Select, and_, func, or_, text
from advanced_alchemy.filters import (
    StatementTypeT,
    StatementFilter,
    ModelT,
)

from chitai.database import models as m


@dataclass
class TagFilter(StatementFilter):
    """Filters for books containing at least one of the tags."""

    tags: Optional[list[int]]

    def append_to_statement(
        self, statement: StatementTypeT, model: type[ModelT], *args, **kwargs
    ) -> StatementTypeT:
        if self.tags:
            statement = statement.where(m.Book.tags.any(m.Tag.id.in_(self.tags)))

        return super().append_to_statement(statement, model, *args, **kwargs)


@dataclass
class AuthorFilter(StatementFilter):
    """Filters for books by any of the given authors."""

    authors: Optional[list[int]]

    def append_to_statement(
        self, statement: StatementTypeT, model: type[ModelT], *args, **kwargs
    ) -> StatementTypeT:
        if self.authors:
            statement = statement.where(
                m.Book.authors.any(m.Author.id.in_(self.authors))
            )

        return super().append_to_statement(statement, model, *args, **kwargs)


@dataclass
class BookshelfFilter(StatementFilter):
    """Filters for books in the given bookshelves."""

    lists: Optional[list[int]]
    user_id: Optional[int] = None

    def append_to_statement(
        self, statement: StatementTypeT, model: type[ModelT], *args, **kwargs
    ) -> StatementTypeT:
        if self.lists:
            if self.user_id:
                statement = statement.where(
                    m.Book.lists.any(
                        and_(
                            m.BookList.user_id == self.user_id,
                            m.BookList.id.in_(self.lists),
                        )
                    )
                )

            else:
                statement = statement.where(
                    m.Book.lists.any(m.BookList.id.in_(self.lists))
                )

        return super().append_to_statement(statement, model, *args, **kwargs)


class ProgressStatus(StrEnum):
    """Enum for different progress statuses."""

    IN_PROGRESS = "in_progress"
    READ = "read"
    UNREAD = "unread"


@dataclass
class ProgressFilter(StatementFilter):
    """
    Filters books based on their progress status.

    Supports filtering completed, in progress, and unread books.

    """

    user_id: int
    statuses: set[ProgressStatus] = field(default_factory=set)

    def append_to_statement(
        self, statement: StatementTypeT, model: type[ModelT], *args: Any, **kwargs: Any
    ) -> StatementTypeT:
        if not isinstance(statement, Select) or not self.statuses:
            return super().append_to_statement(statement, model, *args, **kwargs)

        # Check if UNREAD is the only status or part of the filter
        has_unread = ProgressStatus.UNREAD in self.statuses
        has_other_statuses = bool(self.statuses - {ProgressStatus.UNREAD})

        # If only filtering by UNREAD, use a simpler query
        if has_unread and not has_other_statuses:
            statement = statement.outerjoin(
                m.BookProgress,
                and_(
                    m.BookProgress.book_id == model.id,
                    m.BookProgress.user_id == self.user_id,
                ),
            ).where(m.BookProgress.id.is_(None))
            return super().append_to_statement(statement, model, *args, **kwargs)

        # For other statuses, build the where clause
        statement = statement.outerjoin(
            m.BookProgress,
            and_(
                m.BookProgress.book_id == model.id,
                m.BookProgress.user_id == self.user_id,
            ),
        ).where(self._build_where_clause())

        return super().append_to_statement(statement, model, *args, **kwargs)

    def _build_where_clause(self) -> Any:
        """Build the appropriate where clause based on the status."""
        status_conditions = []

        if ProgressStatus.IN_PROGRESS in self.statuses:
            status_conditions.append(
                and_(
                    or_(
                        m.BookProgress.completed.is_(False),
                        m.BookProgress.completed.is_(None),
                    ),
                    m.BookProgress.percentage > 0,
                )
            )

        if ProgressStatus.READ in self.statuses:
            status_conditions.append(m.BookProgress.completed.is_(True))

        if ProgressStatus.UNREAD in self.statuses:
            status_conditions.append(m.BookProgress.id.is_(None))

        return or_(*status_conditions)


@dataclass
class FileFilter(StatementFilter):
    """Filter books that are related to the given files."""

    file_ids: list[int]

    def append_to_statement(
        self, statement: StatementTypeT, model: type[ModelT], *args: Any, **kwargs: Any
    ) -> StatementTypeT:
        statement = statement.where(
            m.Book.files.any(m.FileMetadata.id.in_(self.file_ids))
        )

        return super().append_to_statement(statement, model, *args, **kwargs)


@dataclass
class FileHashFilter(StatementFilter):
    file_hashes: list[str]

    def append_to_statement(
        self, statement: StatementTypeT, model: type[ModelT], *args, **kwargs
    ) -> StatementTypeT:
        statement = statement.where(
            m.Book.files.any(m.FileMetadata.hash.in_(self.file_hashes))
        )

        return super().append_to_statement(statement, model, *args, **kwargs)


@dataclass
class CustomOrderBy(StatementFilter):
    """Order by filter with support for 'random' and 'last accessed' orderings."""

    field_name: str
    sort_order: str = "desc"
    user: m.User | None = None

    def append_to_statement(
        self, statement: Any, model: type[Any], *args: Any, **kwargs: Any
    ) -> Any:
        if not isinstance(statement, Select):
            return super().append_to_statement(statement, model, *args, **kwargs)

        # Sort randomly
        if self.sort_order == "random":
            statement = statement.order_by(func.random())

        # Sort by BookProgress.updated_at for the current user
        elif self.field_name == "last_accessed":
            if self.user:
                BP = aliased(m.BookProgress)
                statement = statement.outerjoin(
                    BP, (BP.book_id == model.id) & (BP.user_id == self.user.id)
                )
                order_column = BP.updated_at
                if self.sort_order == "asc":
                    statement = statement.order_by(order_column.asc().nullslast())
                else:
                    statement = statement.order_by(order_column.desc().nullslast())
            else:
                # Skip sorting if no user provided
                return statement

        # Sort by a regular field
        else:
            field = getattr(model, self.field_name, None)
            if field is not None:
                if self.sort_order == "asc":
                    statement = statement.order_by(field.asc())
                else:
                    statement = statement.order_by(field.desc())

        return super().append_to_statement(statement, model, *args, **kwargs)


@dataclass
class TrigramSearchFilter(StatementFilter):
    """Custom search filter using PostgreSQL trigram similarity for fuzzy matching."""

    field_name: str
    value: str
    ignore_case: bool = False

    def append_to_statement(
        self, statement: StatementTypeT, model: type[ModelT], *args: Any, **kwargs: Any
    ) -> StatementTypeT:
        if not isinstance(statement, Select):
            return super().append_to_statement(statement, model, *args, **kwargs)

        statement = statement.where(
            or_(
                text("books.title % :term").bindparams(term=self.value),
                m.Book.title.ilike(f"%{self.value}%"),
            )
        ).order_by(
            text("greatest(similarity(books.title, :term)) DESC").bindparams(
                term=self.value
            )
        )

        return super().append_to_statement(statement, model, *args, **kwargs)
