from datetime import date
import json
from pathlib import Path
from typing import Annotated, Optional, Union
from pydantic import (
    BaseModel,
    BeforeValidator,
    ConfigDict,
    Field,
    SkipValidation,
    field_validator,
    computed_field,
)

from litestar.datastructures import UploadFile

from chitai.schemas.shelf import ShelfRead
from chitai.schemas.author import AuthorRead
from chitai.schemas.tag import TagRead
from chitai.schemas.publisher import PublisherRead


class BookSeriesRead(BaseModel):
    id: int
    title: str


class FileMetadataRead(BaseModel):
    id: int
    path: str
    hash: str
    size: int

    # Nullable, though every ingest path now writes one through
    # `guess_content_type`. Rows predating it can hold null, and a required field here
    # turns one of those into a 500 on a book the reader can otherwise open.
    content_type: str | None = None

    @computed_field
    @property
    def filename(self) -> str:
        return Path(self.path).name


class BookProgressRead(BaseModel):
    percentage: float
    epub_cfi: str | None = None
    epub_xpointer: str | None = None
    pdf_page: int | None = None
    completed: bool | None = False
    device_type: str | None = None
    device_id: str | None = None


class BookRead(BaseModel):
    id: int
    library_id: int
    title: str
    subtitle: str | None
    description: str | None
    cover_image: Path | None
    authors: list["AuthorRead"]
    edition: int | None
    publisher: Optional["PublisherRead"]
    published_date: date | None
    identifiers: dict[str, str]
    language: str | None
    pages: int | None
    tags: list["TagRead"]
    lists: list[ShelfRead]
    series: Optional["BookSeriesRead"]
    series_position: str | None
    files: list["FileMetadataRead"]
    progress: Union["BookProgressRead", None] = None

    @field_validator("identifiers", mode="before")
    @classmethod
    def convert_dict(cls, value) -> dict[str, str]:
        if isinstance(value, list):
            return {item.name: item.value for item in value}
        return value


def split_string_and_remove_duplicates(value: list | str) -> list:
    if isinstance(
        value, str
    ):  # If a string was received, split it into individual authors
        value = value.split(",")

    return list(dict.fromkeys(value))  # Remove duplicates, keep insertion order


class BookCreate(BaseModel):
    library_id: int
    title: str | None = None
    subtitle: str | None = None
    description: str | None = None
    authors: Annotated[
        list[str],
        BeforeValidator(split_string_and_remove_duplicates),
    ] = Field(default_factory=list)
    tags: Annotated[
        list[str],
        BeforeValidator(split_string_and_remove_duplicates),
    ] = Field(default_factory=list)
    edition: int | None = None
    publisher: str | None = None
    published_date: date | None = None
    identifiers: dict[str, str] | None = Field(default_factory=dict)
    language: str | None = None
    pages: int | None = None

    series: str | None = None
    series_position: str | None = None
    cover_image: Annotated[UploadFile | None, SkipValidation] = None
    files: Annotated[list[UploadFile], SkipValidation] = Field(default_factory=list)

    @field_validator("identifiers", mode="before")
    @classmethod
    def to_dict(cls, value) -> dict[str, str]:
        if isinstance(value, str):
            return json.loads(value)
        return value or {}

    model_config = ConfigDict(arbitrary_types_allowed=True)


class BooksCreateFromFiles(BaseModel):
    """Upload schema for multiple books with only files."""

    files: Annotated[list[UploadFile], SkipValidation, Field(default_factory=list)]

    model_config = ConfigDict(arbitrary_types_allowed=True)


class FileFingerprint(BaseModel):
    """What a client can say about a file it has not uploaded yet."""

    hash: str
    size: int
    filename: str = ""


class DuplicateFileRead(BaseModel):
    """A file that was not stored because the library already holds its bytes."""

    model_config = ConfigDict(from_attributes=True)

    filename: str
    hash: str
    size: int
    library_id: int

    # Null when the match was another file in the same upload, which has no row yet.
    book_id: int | None = None
    book_title: str | None = None


class DuplicateBookRead(BaseModel):
    """
    A stored book that may be the same book as another one.

    Unlike `DuplicateFileRead` this is a guess: the evidence is metadata two editions
    of one work legitimately share. Nothing was refused on the strength of it.
    """

    model_config = ConfigDict(from_attributes=True)

    book_id: int
    title: str
    authors: list[str]
    library_id: int
    cover_image: Path | None = None

    # Why it matched: "identifier" and/or "title-author".
    matched_on: list[str]


class PossibleDuplicateRead(BaseModel):
    """A book that was imported, together with what it might be a second copy of."""

    model_config = ConfigDict(from_attributes=True)

    book_id: int
    title: str
    candidates: list[DuplicateBookRead]


class DuplicateBookGroupRead(BaseModel):
    """Books the library holds that all look like copies of one book."""

    books: list[DuplicateBookRead]


class DuplicateDismissal(BaseModel):
    """Two books a reader is saying are not the same book."""

    book_a_id: int
    book_b_id: int


class BooksUploadResult(BaseModel):
    """The outcome of a multi-file upload: what was created, and what was skipped."""

    created: list["BookRead"]
    skipped: list[DuplicateFileRead]

    # Created, not skipped — these are books that went in and look like something the
    # library already had. The reader decides what to do about it.
    possible_duplicates: list[PossibleDuplicateRead] = Field(default_factory=list)


class BookMetadataUpdate(BaseModel):
    title: str | None = None
    subtitle: str | None = None
    description: str | None = None
    authors: Annotated[
        list[str],
        BeforeValidator(split_string_and_remove_duplicates),
        Field(default_factory=list),
    ]
    tags: Annotated[
        list[str],
        BeforeValidator(split_string_and_remove_duplicates),
        Field(default_factory=list),
    ]
    edition: int | None = None
    publisher: str | None = None
    published_date: date | None = None
    identifiers: dict[str, str] | None = dict()
    language: str | None = None
    pages: int | None = None

    series: str | None = None
    series_position: str | None = None

    @field_validator("identifiers", mode="before")
    @classmethod
    def to_dict(cls, value) -> dict[str, str]:
        if isinstance(value, str):
            return json.loads(value)
        return value or {}

    model_config = ConfigDict(arbitrary_types_allowed=True)

    @field_validator("title", mode="before")
    @classmethod
    def title_not_none(cls, v):
        if v is None:
            raise ValueError("title cannot be explicitly set to None")
        return v


class BookMerge(BaseModel):
    """
    Fold several books into one.

    `metadata` is the reader's resolution of the fields the records disagreed on.
    Anything it does not name keeps the survivor's value — merging metadata is a
    judgement, so nothing is guessed on the caller's behalf.
    """

    survivor_id: int
    merged_ids: list[int]
    metadata: Optional["BookMetadataUpdate"] = None


class BookProgressCreate(BaseModel):
    percentage: float
    epub_cfi: str | None = None
    epub_xpointer: str | None = None
    pdf_page: int | None = None
    completed: bool | None = None
    device_type: str | None = None
    device_id: str | None = None
