from typing import TYPE_CHECKING, Optional
from collections.abc import Hashable

from sqlalchemy import ColumnElement, ForeignKey, UniqueConstraint
from sqlalchemy.orm import Mapped
from sqlalchemy.orm import mapped_column
from sqlalchemy.orm import relationship
from sqlalchemy.orm import validates

from advanced_alchemy.base import BigIntAuditBase, BigIntBase
from advanced_alchemy.mixins import UniqueMixin

if TYPE_CHECKING:
    from .book import Book


class Author(BigIntAuditBase, UniqueMixin):
    __tablename__ = "authors"

    # Always the canonical form — see `_canonicalize`. Extractors hand over whatever
    # the file happened to say: "Newman, Sam;" from a `DC:creator` list, or
    # "Sam Newman.epub" from a filename. Storing those verbatim is how one person ends
    # up as several rows in the sidebar.
    name: Mapped[str] = mapped_column(unique=True, index=True)

    # Kept current by `_canonicalize` too — never assign it directly. Not unique: two
    # spellings that survive canonicalization, "Steve Mcconnell" and "Steve McConnell",
    # are still one person to a reader, which is what this column exists to express.
    normalized_name: Mapped[str] = mapped_column(default="", index=True)

    description: Mapped[Optional[str]]

    @validates("name")
    def _canonicalize(self, _key: str, name: str) -> str:
        """
        Store the tidied name, and derive the matching key from it.

        A validator so no write can get around it, and `unique_hash` / `unique_filter`
        below tidy the same way so `as_unique_async` looks the row up under the name it
        would actually be stored as. All three have to agree: if the lookup used the
        raw name and the insert used the tidy one, every variant spelling would miss
        the existing row and then collide with it on the unique index.
        """
        # 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 format_author_name, normalize_author

        name = format_author_name(name)
        self.normalized_name = normalize_author(name)
        return name

    @classmethod
    def _tidy(cls, name: str) -> str:
        """The name as `_canonicalize` would store it."""
        from chitai.services.matching import format_author_name

        return format_author_name(name)

    @classmethod
    def unique_hash(cls, name: str) -> Hashable:
        """Generate a unique hash for deduplication."""
        return cls._tidy(name)

    @classmethod
    def unique_filter(cls, name: str) -> ColumnElement[bool]:
        """SQL filter for finding existing records."""
        return cls.name == cls._tidy(name)

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


class BookAuthorLink(BigIntBase):
    __tablename__ = "book_author_links"
    __table_args__ = (UniqueConstraint("book_id", "author_id"),)

    book_id: Mapped[int] = mapped_column(ForeignKey("books.id", ondelete="cascade"))
    author_id: Mapped[int] = mapped_column(ForeignKey("authors.id"))

    position: Mapped[int]

    book: Mapped["Book"] = relationship(back_populates="author_links")
    author: Mapped[Author] = relationship()
