from datetime import date
from typing import TYPE_CHECKING, Any, Optional

from sqlalchemy import Index, ForeignKey, UniqueConstraint
from sqlalchemy.orm import Mapped, mapped_column, relationship, validates
from sqlalchemy.ext.orderinglist import ordering_list
from sqlalchemy.ext.associationproxy import association_proxy
from sqlalchemy.ext.associationproxy import AssociationProxy

from advanced_alchemy.base import BigIntAuditBase, BigIntBase

from .author import Author, BookAuthorLink
from .publisher import Publisher
from .tag import Tag, BookTagLink
from .library import Library


if TYPE_CHECKING:
    from chitai.database.models import (
        BookProgress,
        BookList,
        BookSeries,
        BookListLink,
    )


class Book(BigIntAuditBase):
    __tablename__ = "books"

    __table_args__ = (
        # Create trigram indexes
        Index(
            "ix_books_title_trigram",
            "title",
            postgresql_using="gin",
            postgresql_ops={"title": "gin_trgm_ops"},
        ),
    )

    library_id: Mapped[int] = mapped_column(ForeignKey("libraries.id"), nullable=False)
    library: Mapped["Library"] = relationship(back_populates="books")

    title: Mapped[str]

    # Kept current by `_normalize_title` below — never assign it directly.
    #
    # Deliberately not unique: two spellings collapsing onto one value is the whole
    # point of the column, and a second edition is allowed to exist.
    normalized_title: Mapped[str] = mapped_column(default="", index=True)

    subtitle: Mapped[Optional[str]]
    description: Mapped[Optional[str]]
    published_date: Mapped[Optional[date]]
    language: Mapped[Optional[str]]
    pages: Mapped[Optional[int]]
    cover_image: Mapped[Optional[str]]
    edition: Mapped[Optional[int]]
    path: Mapped[Optional[str]]

    library: Mapped[Library] = relationship(back_populates="books")

    author_links: Mapped[list["BookAuthorLink"]] = relationship(
        back_populates="book",
        cascade="all, delete-orphan",
        passive_deletes=True,
        order_by="BookAuthorLink.position",
        collection_class=ordering_list("position"),
    )

    authors: AssociationProxy[list["Author"]] = association_proxy(
        "author_links", "author", creator=lambda author: BookAuthorLink(author=author)
    )

    publisher_id: Mapped[Optional[int]] = mapped_column(ForeignKey("publishers.id"))
    publisher: Mapped[Optional[Publisher]] = relationship()

    tag_links: Mapped[list["BookTagLink"]] = relationship(
        back_populates="book",
        cascade="all, delete-orphan",
        passive_deletes=True,
        order_by="BookTagLink.position",
        collection_class=ordering_list("position"),
    )

    tags: AssociationProxy[list["Tag"]] = association_proxy(
        "tag_links", "tag", creator=lambda tag: BookTagLink(tag=tag)
    )

    identifiers: Mapped[list["Identifier"]] = relationship(
        cascade="all, delete-orphan",
        passive_deletes=True,
    )

    files: Mapped[list["FileMetadata"]] = relationship(
        cascade="all, delete-orphan", passive_deletes=True
    )

    list_links: Mapped[list["BookListLink"]] = relationship(
        back_populates="book", cascade="all, delete-orphan", passive_deletes=True
    )

    lists: AssociationProxy[list["BookList"]] = association_proxy(
        "list_links", "book_list"
    )

    series_id: Mapped[Optional[int]] = mapped_column(ForeignKey("book_series.id"))
    series: Mapped[Optional["BookSeries"]] = relationship()
    series_position: Mapped[Optional[str]]

    progress_records: Mapped[list["BookProgress"]] = relationship(
        cascade="all, delete-orphan", passive_deletes=True
    )

    @property
    def progress(self) -> Optional["BookProgress"]:
        return self.progress_records[0] if self.progress_records else None

    @validates("title")
    def _normalize_title(self, _key: str, title: str) -> str:
        """
        Derive `normalized_title` from whatever writes the title.

        A validator rather than a service call because `BookService` sets titles from
        at least three places — `to_model_on_create`, `to_model_on_update` and the
        `setattr` loop in `_populate_with_unique_relationships` — and a fourth would
        otherwise leave the key silently stale.
        """
        # Imported here rather than at module scope: `chitai.services.matching` cannot
        # be reached without initialising the `chitai.services` package, which imports
        # the services, which import this module.
        from chitai.services.matching import normalize_title

        self.normalized_title = normalize_title(title)
        return title

    def __repr__(self) -> str:
        return f"Book({self.title=!r})"

    def to_dict(self, exclude: set[str] | None = None) -> dict[str, Any]:
        data = super().to_dict(exclude)
        data["authors"] = [author.name for author in self.authors]
        data["tags"] = [tag.name for tag in self.tags]
        data["publisher"] = self.publisher.name if self.publisher else None
        data["series"] = self.series.title if self.series else None
        data["files"] = [file.to_dict() for file in self.files]
        return data


class Identifier(BigIntBase):
    __tablename__ = "identifiers"
    __table_args__ = (UniqueConstraint("name", "book_id"),)

    name: Mapped[str]
    book_id: Mapped[int] = mapped_column(ForeignKey("books.id", ondelete="cascade"))
    value: Mapped[str]

    # Kept current by `_normalize` below — never assign it directly. Null for an
    # identifier that cannot carry a match: a per-build UUID, or an ISBN that fails
    # its own checksum.
    normalized_value: Mapped[Optional[str]] = mapped_column(index=True)

    @validates("name", "value")
    def _normalize(self, key: str, value: str) -> str:
        """Recompute `normalized_value` whenever either half of the pair changes."""
        from chitai.services.matching import normalize_identifier

        name = value if key == "name" else self.name
        raw = value if key == "value" else self.value

        self.normalized_value = (
            normalize_identifier(name, raw) if name and raw else None
        )
        return value

    def __repr__(self):
        return f"Identifier({self.name!r} : {self.value!r})"


class FileMetadata(BigIntBase):
    __tablename__ = "file_metadata"

    __table_args__ = (
        # Deliberately not unique. The hash is KOReader's partial MD5, which samples
        # 12 KiB of the file, so two genuinely different files can collide — and an
        # existing database may already hold duplicates, which a unique index would
        # refuse to build over. Duplicate detection pairs it with `size` and treats a
        # match as advisory, so this only has to make the lookup cheap.
        Index("ix_file_metadata_hash", "hash"),
    )

    book_id: Mapped[int] = mapped_column(ForeignKey("books.id", ondelete="cascade"))
    book: Mapped[Book] = relationship(back_populates="files")
    hash: Mapped[str]
    path: Mapped[str]
    size: Mapped[int]
    content_type: Mapped[Optional[str]]

    def __repr__(self) -> str:
        return f"FileMetadata({self.path!r})"
