"""User service for Mantimon TCG.

This module provides business logic for user account operations,
including OAuth-based user creation, account linking, and premium
status management.

The service layer contains business logic while repositories handle
pure data access. This separation enables testing and different
storage backends.

Example:
    from app.services.user_service import UserService
    from app.repositories.postgres import PostgresUserRepository, PostgresLinkedAccountRepository

    # Create service with injected repositories
    user_repo = PostgresUserRepository(db)
    linked_repo = PostgresLinkedAccountRepository(db)
    service = UserService(user_repo, linked_repo)

    # Use service
    user = await service.get_by_id(user_id)
    user, created = await service.get_or_create_from_oauth(oauth_info)
"""

from datetime import datetime
from uuid import UUID

from app.repositories.protocols import (
    LinkedAccountEntry,
    LinkedAccountRepository,
    UserEntry,
    UserRepository,
)
from app.schemas.user import OAuthUserInfo, UserUpdate


class AccountLinkingError(Exception):
    """Error during account linking operation."""

    pass


class UserService:
    """Service for user account operations.

    Provides business logic for user CRUD, OAuth-based creation,
    account linking, and premium subscription management.

    Attributes:
        _user_repo: Repository for user data access.
        _linked_repo: Repository for linked account data access.
    """

    def __init__(
        self,
        user_repository: UserRepository,
        linked_account_repository: LinkedAccountRepository,
    ) -> None:
        """Initialize with repository dependencies.

        Args:
            user_repository: Repository for user data access.
            linked_account_repository: Repository for linked account data access.
        """
        self._user_repo = user_repository
        self._linked_repo = linked_account_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.

        Example:
            user = await service.get_by_id(user_id)
            if user:
                print(f"Found user: {user.display_name}")
        """
        return await self._user_repo.get_by_id(user_id)

    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.

        Example:
            user = await service.get_by_email("player@example.com")
        """
        return await self._user_repo.get_by_email(email)

    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.

        Example:
            user = await service.get_by_oauth("google", "123456789")
        """
        return await self._user_repo.get_by_oauth(provider, oauth_id)

    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.

        Example:
            user = await service.create(
                email="player@example.com",
                display_name="Player1",
                oauth_provider="google",
                oauth_id="123456789"
            )
        """
        return await self._user_repo.create(
            email=email,
            display_name=display_name,
            oauth_provider=oauth_provider,
            oauth_id=oauth_id,
            avatar_url=avatar_url,
        )

    async def create_from_oauth(self, oauth_info: OAuthUserInfo) -> UserEntry:
        """Create a new user from OAuth provider info.

        Convenience method that extracts fields from OAuthUserInfo.

        Args:
            oauth_info: Normalized OAuth user information.

        Returns:
            The created UserEntry.

        Example:
            oauth_info = OAuthUserInfo(
                provider="google",
                oauth_id="123456789",
                email="player@example.com",
                name="Player One",
                avatar_url="https://..."
            )
            user = await service.create_from_oauth(oauth_info)
        """
        return await self.create(
            email=oauth_info.email,
            display_name=oauth_info.name,
            oauth_provider=oauth_info.provider,
            oauth_id=oauth_info.oauth_id,
            avatar_url=oauth_info.avatar_url,
        )

    async def get_or_create_from_oauth(
        self,
        oauth_info: OAuthUserInfo,
    ) -> tuple[UserEntry, bool]:
        """Get existing user or create new one from OAuth info.

        First checks for existing user by OAuth provider+ID, then by email
        (for account linking), and finally creates a new user if not found.

        Args:
            oauth_info: Normalized OAuth user information.

        Returns:
            Tuple of (UserEntry, created) where created is True if new user.

        Example:
            user, created = await service.get_or_create_from_oauth(oauth_info)
            if created:
                print("Welcome, new user!")
            else:
                print("Welcome back!")
        """
        # First, check by OAuth provider + ID (exact match)
        user = await self._user_repo.get_by_oauth(oauth_info.provider, oauth_info.oauth_id)
        if user:
            return user, False

        # Check by email for potential account linking
        # If user exists with same email but different OAuth, update their OAuth
        user = await self._user_repo.get_by_email(oauth_info.email)
        if user:
            # Update OAuth credentials for existing user
            # This links the new OAuth provider to the existing account
            updated_user = await self._user_repo.update(
                user_id=user.id,
                oauth_provider=oauth_info.provider,
                oauth_id=oauth_info.oauth_id,
                avatar_url=oauth_info.avatar_url if not user.avatar_url else None,
            )
            return updated_user or user, False

        # Create new user
        user = await self.create_from_oauth(oauth_info)
        return user, True

    async def update(
        self,
        user_id: UUID,
        update_data: UserUpdate,
    ) -> UserEntry | None:
        """Update user profile fields.

        Only updates fields that are explicitly provided. Uses UNSET pattern
        for avatar_url to distinguish "not provided" from "set to None".

        Args:
            user_id: The user's UUID.
            update_data: Fields to update.

        Returns:
            The updated UserEntry, or None if not found.

        Example:
            update_data = UserUpdate(display_name="New Name")
            user = await service.update(user_id, update_data)
        """
        from app.repositories.protocols import UNSET

        # Use UNSET for avatar_url unless explicitly provided
        avatar_url = (
            update_data.avatar_url if "avatar_url" in update_data.model_fields_set else UNSET
        )

        return await self._user_repo.update(
            user_id=user_id,
            display_name=update_data.display_name,
            avatar_url=avatar_url,
        )

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

        Args:
            user_id: The user's UUID.

        Returns:
            The updated UserEntry, or None if not found.

        Example:
            user = await service.update_last_login(user_id)
        """
        return await self._user_repo.update_last_login(user_id)

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

        Args:
            user_id: The user's UUID.
            premium_until: When premium expires, or None to remove premium.

        Returns:
            The updated UserEntry, or None if not found.

        Example:
            # Grant 30 days of premium
            expires = datetime.now(UTC) + timedelta(days=30)
            user = await service.update_premium(user_id, expires)

            # Remove premium
            user = await service.update_premium(user_id, None)
        """
        is_premium = premium_until is not None
        return await self._user_repo.update_premium(
            user_id=user_id,
            is_premium=is_premium,
            premium_until=premium_until,
        )

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

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

        Args:
            user_id: The user's UUID.

        Returns:
            True if deleted, False if not found.

        Example:
            success = await service.delete(user_id)
        """
        return await self._user_repo.delete(user_id)

    # =========================================================================
    # Linked Account Operations
    # =========================================================================

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

        Args:
            user_id: The user's UUID.

        Returns:
            List of LinkedAccountEntry.
        """
        return await self._linked_repo.get_by_user(user_id)

    async def get_linked_account(
        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.
        """
        return await self._linked_repo.get_by_provider(provider, oauth_id)

    async def link_oauth_account(
        self,
        user_id: UUID,
        user_oauth_provider: str,
        oauth_info: OAuthUserInfo,
    ) -> LinkedAccountEntry:
        """Link an additional OAuth provider to a user account.

        Args:
            user_id: The user's UUID.
            user_oauth_provider: The user's primary OAuth provider.
            oauth_info: OAuth information from the provider.

        Returns:
            The created LinkedAccountEntry.

        Raises:
            AccountLinkingError: If provider is already linked to this or another user.

        Example:
            linked = await service.link_oauth_account(user.id, user.oauth_provider, discord_info)
        """
        # Check if this provider+oauth_id is already linked to any user
        existing = await self._linked_repo.get_by_provider(oauth_info.provider, oauth_info.oauth_id)
        if existing:
            if existing.user_id == user_id:
                raise AccountLinkingError(
                    f"{oauth_info.provider.title()} account is already linked to your account"
                )
            raise AccountLinkingError(
                f"This {oauth_info.provider.title()} account is already linked to another user"
            )

        # Check if this is the user's primary OAuth provider
        if user_oauth_provider == oauth_info.provider:
            raise AccountLinkingError(
                f"{oauth_info.provider.title()} is your primary login provider"
            )

        # Check if user already has this provider linked
        linked_accounts = await self._linked_repo.get_by_user(user_id)
        for linked in linked_accounts:
            if linked.provider == oauth_info.provider:
                raise AccountLinkingError(
                    f"You already have a {oauth_info.provider.title()} account linked"
                )

        # Create the linked account
        return await self._linked_repo.create(
            user_id=user_id,
            provider=oauth_info.provider,
            oauth_id=oauth_info.oauth_id,
            email=oauth_info.email,
            display_name=oauth_info.name,
            avatar_url=oauth_info.avatar_url,
        )

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

        Cannot unlink the primary OAuth provider.

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

        Returns:
            True if unlinked, False if provider wasn't linked.

        Raises:
            AccountLinkingError: If trying to unlink the primary provider.

        Example:
            success = await service.unlink_oauth_account(user.id, user.oauth_provider, "discord")
        """
        # Cannot unlink primary provider
        if user_oauth_provider == provider:
            raise AccountLinkingError(
                f"Cannot unlink {provider.title()} - it is your primary login provider"
            )

        return await self._linked_repo.delete(user_id, provider)
