"""Relay-style pagination data types.

Defines the Pydantic models used by ``paginate`` / ``paginate_sync``:

* ``PaginationArgs`` — validated Relay cursor arguments (``first``, ``last``,
  ``after``, ``before``) together with the active ``PaginationConfig``.
  All argument constraints are expressed as Pydantic validators so that
  invalid combinations raise ``InvalidArgumentsError`` at construction time.
* ``RequestedFields`` — carries which parts of a Connection the caller
  actually needs, enabling query-level optimisations.
* ``PageInfo`` — mirrors the GraphQL ``PageInfo`` type.
* ``Edge[T]`` — a single node with its opaque cursor string.
* ``Connection[T]`` — the top-level paginated result container.
"""

from typing import Annotated, Generic, TypeVar

from pydantic import BaseModel, BeforeValidator, ConfigDict, Field, model_validator

from sqlalchemy_relay_pagination.config import PaginationConfig, get_config
from sqlalchemy_relay_pagination.exceptions import InvalidArgumentsError


def _resolve_config(v: PaginationConfig | None) -> PaginationConfig:
    """Resolve ``None`` to the current global config."""
    return v if v is not None else get_config()


class PaginationArgs(BaseModel):
    """Validated Relay pagination arguments.

    Accepts the four standard Relay cursor-connection arguments and the
    active ``PaginationConfig``.  All constraints are enforced in Pydantic
    validators so that any invalid combination raises ``InvalidArgumentsError``
    immediately on construction — before any database query is issued.

    Computed properties expose the derived values consumed by the paginator:

    * ``is_forward`` — ``True`` when paginating with ``first``/``after``.
    * ``effective_limit`` — resolved page size (falls back to
      ``config.default_page_size`` when neither ``first`` nor ``last`` is
      given).
    * ``cursor`` — the relevant cursor string for the current direction
      (``after`` when forward, ``before`` when backward).

    The ``config`` field defaults to the global ``PaginationConfig`` and is
    excluded from serialisation.  Passing ``None`` falls back to the global
    config identical to omitting the argument entirely.

    Example::

        args = PaginationArgs(first=10, after=cursor)
        args = PaginationArgs(last=5, before=cursor, config=custom_config)
    """

    model_config = ConfigDict(arbitrary_types_allowed=True)

    first: int | None = None
    last: int | None = None
    after: str | None = None
    before: str | None = None
    config: Annotated[PaginationConfig, BeforeValidator(_resolve_config)] = Field(
        default_factory=get_config, exclude=True
    )

    @model_validator(mode="after")
    def _validate_args(self) -> "PaginationArgs":
        if self.first is not None and self.last is not None:
            raise InvalidArgumentsError("Cannot specify both 'first' and 'last'")
        if self.first is not None and self.first < 0:
            raise InvalidArgumentsError("'first' must be non-negative")
        if self.last is not None and self.last < 0:
            raise InvalidArgumentsError("'last' must be non-negative")
        limit = self.first if self.first is not None else self.last
        if limit is not None and limit > self.config.max_page_size:
            raise InvalidArgumentsError(f"Page size {limit} exceeds maximum allowed {self.config.max_page_size}")
        return self

    @property
    def is_forward(self) -> bool:
        """``True`` for ``first``/``after`` pagination, ``False`` for ``last``/``before``."""
        return self.last is None

    @property
    def effective_limit(self) -> int:
        """Page size to use, falling back to ``config.default_page_size``."""
        if self.first is None and self.last is None:
            return self.config.default_page_size
        return self.first if self.first is not None else self.last  # type: ignore[return-value]

    @property
    def cursor(self) -> str | None:
        """The active cursor: ``after`` when forward, ``before`` when backward."""
        return self.after if self.is_forward else self.before


class RequestedFields(BaseModel):
    """Declares which connection fields the caller intends to consume.

    Defaults to requesting everything except ``totalCount`` (opt-in because
    it requires a separate ``COUNT(*)`` query).
    """

    edges: bool = True
    has_next_page: bool = True
    has_previous_page: bool = True
    start_cursor: bool = True
    end_cursor: bool = True
    total_count: bool = False

    @property
    def needs_page_query(self) -> bool:
        """``True`` when at least one field requires fetching actual rows."""
        return self.edges or self.has_next_page or self.has_previous_page or self.start_cursor or self.end_cursor


class PageInfo(BaseModel):
    """Relay ``PageInfo`` object."""

    hasNextPage: bool = Field(alias="has_next_page")
    hasPreviousPage: bool = Field(alias="has_previous_page")
    startCursor: str | None = Field(alias="start_cursor")
    endCursor: str | None = Field(alias="end_cursor")
    totalCount: int | None = Field(alias="total_count")

    model_config = ConfigDict(validate_by_name=True, validate_by_alias=True)


T = TypeVar("T")


class Edge(BaseModel, Generic[T]):
    """A single edge in a connection, pairing a node with its cursor."""

    model_config = ConfigDict(arbitrary_types_allowed=True)

    node: T
    cursor: str


class Connection(BaseModel, Generic[T]):
    """Top-level Relay connection result."""

    edges: list[Edge[T]]
    pageInfo: PageInfo = Field(alias="page_info")

    model_config = ConfigDict(validate_by_name=True, validate_by_alias=True)
