"""Repository protocol definitions for Mantimon TCG.

This module defines the abstract interfaces (Protocols) for data access.
Concrete implementations can be PostgreSQL, SQLite, or in-memory storage.

The protocols define WHAT operations are available, not HOW they're implemented.
This enables the offline fork to implement LocalCollectionRepository while
the backend uses PostgresCollectionRepository.

Example:
    class CollectionRepository(Protocol):
        async def get_all(self, user_id: UUID) -> list[CollectionEntry]: ...

    # PostgreSQL implementation
    class PostgresCollectionRepository:
        def __init__(self, db: AsyncSession): ...
        async def get_all(self, user_id: UUID) -> list[CollectionEntry]: ...

    # Local/offline implementation
    class LocalCollectionRepository:
        def __init__(self, storage_path: Path): ...
        async def get_all(self, user_id: UUID) -> list[CollectionEntry]: ...
"""

from dataclasses import dataclass
from datetime import datetime
from typing import Any, Protocol
from uuid import UUID

from app.db.models.collection import CardSource

# =============================================================================
# Sentinel Value for Optional Updates
# =============================================================================
# UNSET is used to distinguish between "not provided" (keep existing)
# and "explicitly set to None" (clear the field).
#
# Example usage:
#     async def update(description: str | None = UNSET):
#         if description is not UNSET:
#             # User explicitly provided a value (could be None to clear)
#             record.description = description


class _UnsetType:
    """Sentinel class for unset optional parameters."""

    __slots__ = ()

    def __repr__(self) -> str:
        return "UNSET"

    def __bool__(self) -> bool:
        return False


UNSET: Any = _UnsetType()

# =============================================================================
# Data Transfer Objects (DTOs)
# =============================================================================
# These are storage-agnostic representations used by protocols.
# They decouple the service layer from ORM models.


@dataclass
class CollectionEntry:
    """Storage-agnostic representation of a collection entry.

    This DTO decouples the service layer from the ORM model,
    allowing different storage backends to return the same structure.
    """

    id: UUID
    user_id: UUID
    card_definition_id: str
    quantity: int
    source: CardSource
    obtained_at: datetime
    created_at: datetime
    updated_at: datetime


@dataclass
class DeckEntry:
    """Storage-agnostic representation of a deck.

    This DTO decouples the service layer from the ORM model.
    """

    id: UUID
    user_id: UUID
    name: str
    cards: dict[str, int]
    energy_cards: dict[str, int]
    is_valid: bool
    validation_errors: list[str] | None
    is_starter: bool
    starter_type: str | None
    description: str | None
    created_at: datetime
    updated_at: datetime


@dataclass
class UserEntry:
    """Storage-agnostic representation of a user account.

    This DTO decouples the service layer from the ORM model,
    enabling different storage backends (PostgreSQL, SQLite, JSON)
    to be used interchangeably.
    """

    id: UUID
    email: str
    display_name: str
    avatar_url: str | None
    oauth_provider: str
    oauth_id: str
    is_premium: bool
    premium_until: datetime | None
    last_login: datetime | None
    created_at: datetime
    updated_at: datetime


@dataclass
class LinkedAccountEntry:
    """Storage-agnostic representation of a linked OAuth account.

    Users can link multiple OAuth providers (e.g., Google + Discord)
    to a single account for flexible login options.
    """

    id: UUID
    user_id: UUID
    provider: str
    oauth_id: str
    email: str | None
    display_name: str | None
    avatar_url: str | None
    linked_at: datetime


# =============================================================================
# Repository Protocols
# =============================================================================


class CollectionRepository(Protocol):
    """Protocol for card collection data access.

    Implementations handle storage-specific details (PostgreSQL, SQLite, JSON).
    Services use this protocol for business logic without knowing storage details.

    All methods are async to support both database and file-based storage.
    """

    async def get_all(self, user_id: UUID) -> list[CollectionEntry]:
        """Get all collection entries for a user.

        Args:
            user_id: The user's UUID.

        Returns:
            List of all collection entries, ordered by card_definition_id.
        """
        ...

    async def get_by_card(self, user_id: UUID, card_definition_id: str) -> CollectionEntry | None:
        """Get a specific collection entry.

        Args:
            user_id: The user's UUID.
            card_definition_id: The card ID to look up.

        Returns:
            CollectionEntry if exists, None otherwise.
        """
        ...

    async def get_quantity(self, user_id: UUID, card_definition_id: str) -> int:
        """Get quantity of a specific card owned by user.

        Args:
            user_id: The user's UUID.
            card_definition_id: Card ID to check.

        Returns:
            Number of copies owned (0 if not owned).
        """
        ...

    async def upsert(
        self,
        user_id: UUID,
        card_definition_id: str,
        quantity: int,
        source: CardSource,
    ) -> CollectionEntry:
        """Add or update a collection entry.

        If entry exists, increments quantity. Otherwise creates new entry.

        Args:
            user_id: The user's UUID.
            card_definition_id: Card ID to add.
            quantity: Number of copies to add.
            source: How the cards were obtained.

        Returns:
            The created or updated CollectionEntry.
        """
        ...

    async def decrement(
        self,
        user_id: UUID,
        card_definition_id: str,
        quantity: int,
    ) -> CollectionEntry | None:
        """Decrement quantity of a collection entry.

        If quantity reaches 0, deletes the entry.

        Args:
            user_id: The user's UUID.
            card_definition_id: Card ID to decrement.
            quantity: Number of copies to remove.

        Returns:
            Updated entry, or None if entry was deleted or didn't exist.
        """
        ...

    async def exists_with_source(self, user_id: UUID, source: CardSource) -> bool:
        """Check if user has any entries with the given source.

        Useful for checking if user has received a starter deck.

        Args:
            user_id: The user's UUID.
            source: The CardSource to check for.

        Returns:
            True if any entries exist with that source.
        """
        ...


class DeckRepository(Protocol):
    """Protocol for deck data access.

    Implementations handle storage-specific details (PostgreSQL, SQLite, JSON).
    Services use this protocol for business logic without knowing storage details.
    """

    async def get_by_id(self, deck_id: UUID) -> DeckEntry | None:
        """Get a deck by its ID.

        Args:
            deck_id: The deck's UUID.

        Returns:
            DeckEntry if found, None otherwise.
        """
        ...

    async def get_by_user(self, user_id: UUID) -> list[DeckEntry]:
        """Get all decks for a user.

        Args:
            user_id: The user's UUID.

        Returns:
            List of all user's decks, ordered by name.
        """
        ...

    async def get_user_deck(self, user_id: UUID, deck_id: UUID) -> DeckEntry | None:
        """Get a specific deck owned by a user.

        Combines ownership check with retrieval.

        Args:
            user_id: The user's UUID.
            deck_id: The deck's UUID.

        Returns:
            DeckEntry if found and owned by user, None otherwise.
        """
        ...

    async def count_by_user(self, user_id: UUID) -> int:
        """Count how many decks a user has.

        Args:
            user_id: The user's UUID.

        Returns:
            Number of decks owned by user.
        """
        ...

    async def create(
        self,
        user_id: UUID,
        name: str,
        cards: dict[str, int],
        energy_cards: dict[str, int],
        is_valid: bool,
        validation_errors: list[str] | None,
        is_starter: bool = False,
        starter_type: str | None = None,
        description: str | None = None,
    ) -> DeckEntry:
        """Create a new deck.

        Args:
            user_id: The user's UUID.
            name: Display name for the deck.
            cards: Card ID to quantity mapping.
            energy_cards: Energy type to quantity mapping.
            is_valid: Whether deck passes validation.
            validation_errors: List of validation error messages.
            is_starter: Whether this is a starter deck.
            starter_type: Type of starter deck if applicable.
            description: Optional deck description.

        Returns:
            The created DeckEntry.
        """
        ...

    async def update(
        self,
        deck_id: UUID,
        name: str | None = None,
        cards: dict[str, int] | None = None,
        energy_cards: dict[str, int] | None = None,
        is_valid: bool | None = None,
        validation_errors: list[str] | None = UNSET,  # type: ignore[assignment]
        description: str | None = UNSET,  # type: ignore[assignment]
    ) -> DeckEntry | None:
        """Update an existing deck.

        Only provided (non-UNSET) fields are updated.
        Use UNSET (default) to keep existing value, or None to clear.

        Args:
            deck_id: The deck's UUID.
            name: New name (optional, None keeps existing).
            cards: New card composition (optional, None keeps existing).
            energy_cards: New energy composition (optional, None keeps existing).
            is_valid: New validation status (optional, None keeps existing).
            validation_errors: New errors (UNSET=keep, None=clear, list=set).
            description: New description (UNSET=keep, None=clear, str=set).

        Returns:
            Updated DeckEntry, or None if deck not found.
        """
        ...

    async def delete(self, deck_id: UUID) -> bool:
        """Delete a deck.

        Args:
            deck_id: The deck's UUID.

        Returns:
            True if deleted, False if not found.
        """
        ...

    async def has_starter(self, user_id: UUID) -> tuple[bool, str | None]:
        """Check if user has a starter deck.

        Args:
            user_id: The user's UUID.

        Returns:
            Tuple of (has_starter, starter_type).
        """
        ...


class UserRepository(Protocol):
    """Protocol for user account data access.

    Implementations handle storage-specific details (PostgreSQL, SQLite, JSON).
    Services use this protocol for business logic without knowing storage details.

    Note: Business logic like get_or_create_from_oauth belongs in the service layer,
    not in the repository.
    """

    async def get_by_id(self, user_id: UUID) -> UserEntry | None:
        """Get a user by their ID.

        Args:
            user_id: The user's UUID.

        Returns:
            UserEntry if found, None otherwise.
        """
        ...

    async def get_by_email(self, email: str) -> UserEntry | None:
        """Get a user by their email address.

        Args:
            email: The user's email address.

        Returns:
            UserEntry if found, None otherwise.
        """
        ...

    async def get_by_oauth(self, provider: str, oauth_id: str) -> UserEntry | None:
        """Get a user by their OAuth provider and ID.

        Args:
            provider: OAuth provider name (google, discord).
            oauth_id: Unique ID from the OAuth provider.

        Returns:
            UserEntry if found, None otherwise.
        """
        ...

    async def create(
        self,
        email: str,
        display_name: str,
        oauth_provider: str,
        oauth_id: str,
        avatar_url: str | None = None,
    ) -> UserEntry:
        """Create a new user.

        Args:
            email: User's email address.
            display_name: Public display name.
            oauth_provider: OAuth provider name.
            oauth_id: Unique ID from the OAuth provider.
            avatar_url: Optional avatar URL.

        Returns:
            The created UserEntry.
        """
        ...

    async def update(
        self,
        user_id: UUID,
        display_name: str | None = None,
        avatar_url: str | None = UNSET,  # type: ignore[assignment]
        oauth_provider: str | None = None,
        oauth_id: str | None = None,
    ) -> UserEntry | None:
        """Update user profile fields.

        Only provided (non-None/non-UNSET) fields are updated.
        Use UNSET (default) to keep existing value for nullable fields,
        or None to explicitly clear them.

        Args:
            user_id: The user's UUID.
            display_name: New display name (None keeps existing).
            avatar_url: New avatar URL (UNSET=keep, None=clear, str=set).
            oauth_provider: New OAuth provider (None keeps existing).
            oauth_id: New OAuth ID (None keeps existing).

        Returns:
            Updated UserEntry, or None if user not found.
        """
        ...

    async def update_last_login(self, user_id: UUID) -> UserEntry | None:
        """Update the user's last login timestamp to now.

        Args:
            user_id: The user's UUID.

        Returns:
            Updated UserEntry, or None if user not found.
        """
        ...

    async def update_premium(
        self,
        user_id: UUID,
        is_premium: bool,
        premium_until: datetime | None,
    ) -> UserEntry | None:
        """Update user's premium subscription status.

        Args:
            user_id: The user's UUID.
            is_premium: Whether user has premium.
            premium_until: When premium expires, or None if not premium.

        Returns:
            Updated UserEntry, or None if user not found.
        """
        ...

    async def delete(self, user_id: UUID) -> bool:
        """Delete a user account.

        This will cascade delete all related data (decks, collection, etc.)
        based on database constraints.

        Args:
            user_id: The user's UUID.

        Returns:
            True if deleted, False if not found.
        """
        ...


class LinkedAccountRepository(Protocol):
    """Protocol for OAuth linked accounts data access.

    Users can link multiple OAuth providers to a single account.
    The primary OAuth provider is stored on the User model itself;
    additional linked providers are stored as LinkedAccount records.
    """

    async def get_by_provider(
        self,
        provider: str,
        oauth_id: str,
    ) -> LinkedAccountEntry | None:
        """Get a linked account by provider and OAuth ID.

        Args:
            provider: OAuth provider name (google, discord).
            oauth_id: Unique ID from the OAuth provider.

        Returns:
            LinkedAccountEntry if found, None otherwise.
        """
        ...

    async def get_by_user(self, user_id: UUID) -> list[LinkedAccountEntry]:
        """Get all linked accounts for a user.

        Args:
            user_id: The user's UUID.

        Returns:
            List of LinkedAccountEntry, ordered by provider.
        """
        ...

    async def create(
        self,
        user_id: UUID,
        provider: str,
        oauth_id: str,
        email: str | None = None,
        display_name: str | None = None,
        avatar_url: str | None = None,
    ) -> LinkedAccountEntry:
        """Link an OAuth provider to a user account.

        Args:
            user_id: The user's UUID.
            provider: OAuth provider name.
            oauth_id: Unique ID from the OAuth provider.
            email: Email from the OAuth provider.
            display_name: Display name from the OAuth provider.
            avatar_url: Avatar URL from the OAuth provider.

        Returns:
            The created LinkedAccountEntry.
        """
        ...

    async def delete(self, user_id: UUID, provider: str) -> bool:
        """Unlink an OAuth provider from a user account.

        Args:
            user_id: The user's UUID.
            provider: OAuth provider name to unlink.

        Returns:
            True if deleted, False if not found.
        """
        ...
