"""Discord OAuth service for Mantimon TCG.

This module handles Discord OAuth 2.0 authentication flow:
1. Generate authorization URL for user redirect
2. Exchange authorization code for tokens
3. Fetch user information from Discord

Discord OAuth Endpoints:
    - Authorization: https://discord.com/api/oauth2/authorize
    - Token: https://discord.com/api/oauth2/token
    - User Info: https://discord.com/api/users/@me

Example:
    from app.services.oauth.discord import discord_oauth

    # Step 1: Redirect user to Discord
    auth_url = discord_oauth.get_authorization_url(
        redirect_uri="https://play.mantimon.com/api/auth/discord/callback",
        state="random-csrf-token"
    )

    # Step 2: Handle callback and get user info
    user_info = await discord_oauth.get_user_info(code, redirect_uri)
"""

from urllib.parse import urlencode

import httpx

from app.config import settings
from app.schemas.user import OAuthUserInfo


class DiscordOAuthError(Exception):
    """Exception raised for Discord OAuth errors."""

    pass


class DiscordOAuth:
    """Discord OAuth 2.0 service.

    Handles the OAuth flow for authenticating users with Discord accounts.
    """

    AUTHORIZATION_URL = "https://discord.com/api/oauth2/authorize"
    TOKEN_URL = "https://discord.com/api/oauth2/token"
    USER_INFO_URL = "https://discord.com/api/users/@me"
    CDN_URL = "https://cdn.discordapp.com"

    # Scopes we request from Discord
    SCOPES = ["identify", "email"]

    def get_authorization_url(self, redirect_uri: str, state: str) -> str:
        """Generate the Discord OAuth authorization URL.

        Args:
            redirect_uri: Where Discord should redirect after authorization.
            state: Random string for CSRF protection.

        Returns:
            Full authorization URL to redirect user to.

        Raises:
            DiscordOAuthError: If Discord OAuth is not configured.

        Example:
            url = discord_oauth.get_authorization_url(
                redirect_uri="https://play.mantimon.com/api/auth/discord/callback",
                state="abc123"
            )
            # Redirect user to url
        """
        if not settings.discord_client_id:
            raise DiscordOAuthError("Discord OAuth is not configured")

        params = {
            "client_id": settings.discord_client_id,
            "redirect_uri": redirect_uri,
            "response_type": "code",
            "scope": " ".join(self.SCOPES),
            "state": state,
            "prompt": "consent",  # Always show consent screen
        }

        return f"{self.AUTHORIZATION_URL}?{urlencode(params)}"

    async def exchange_code_for_tokens(
        self,
        code: str,
        redirect_uri: str,
    ) -> dict:
        """Exchange authorization code for access tokens.

        Args:
            code: Authorization code from Discord callback.
            redirect_uri: Same redirect_uri used in authorization request.

        Returns:
            Token response containing access_token, refresh_token, etc.

        Raises:
            DiscordOAuthError: If token exchange fails.
        """
        if not settings.discord_client_id or not settings.discord_client_secret:
            raise DiscordOAuthError("Discord OAuth is not configured")

        data = {
            "client_id": settings.discord_client_id,
            "client_secret": settings.discord_client_secret.get_secret_value(),
            "code": code,
            "grant_type": "authorization_code",
            "redirect_uri": redirect_uri,
        }

        async with httpx.AsyncClient() as client:
            response = await client.post(
                self.TOKEN_URL,
                data=data,
                headers={"Content-Type": "application/x-www-form-urlencoded"},
            )

            if response.status_code != 200:
                error_data = response.json() if response.content else {}
                error_msg = error_data.get("error_description", response.text)
                raise DiscordOAuthError(f"Token exchange failed: {error_msg}")

            return response.json()

    async def fetch_user_info(self, access_token: str) -> dict:
        """Fetch user information from Discord.

        Args:
            access_token: Valid Discord access token.

        Returns:
            User info dict with id, username, email, avatar, etc.

        Raises:
            DiscordOAuthError: If fetching user info fails.
        """
        async with httpx.AsyncClient() as client:
            response = await client.get(
                self.USER_INFO_URL,
                headers={"Authorization": f"Bearer {access_token}"},
            )

            if response.status_code != 200:
                raise DiscordOAuthError(f"Failed to fetch user info: {response.text}")

            return response.json()

    def _build_avatar_url(self, user_id: str, avatar_hash: str | None) -> str | None:
        """Build Discord avatar URL from user ID and avatar hash.

        Args:
            user_id: Discord user ID.
            avatar_hash: Avatar hash from Discord API (can be None).

        Returns:
            Full CDN URL for avatar, or None if no avatar.

        Note:
            Discord avatar format: https://cdn.discordapp.com/avatars/{user_id}/{avatar_hash}.png
            If avatar_hash starts with 'a_', it's animated (gif).
        """
        if not avatar_hash:
            return None

        # Animated avatars start with 'a_'
        extension = "gif" if avatar_hash.startswith("a_") else "png"
        return f"{self.CDN_URL}/avatars/{user_id}/{avatar_hash}.{extension}"

    async def get_user_info(
        self,
        code: str,
        redirect_uri: str,
    ) -> OAuthUserInfo:
        """Complete OAuth flow: exchange code and fetch user info.

        This is the main method to call after receiving the OAuth callback.
        It exchanges the authorization code for tokens, then fetches user info.

        Args:
            code: Authorization code from Discord callback.
            redirect_uri: Same redirect_uri used in authorization request.

        Returns:
            Normalized OAuthUserInfo ready for user creation/lookup.

        Raises:
            DiscordOAuthError: If any step of the OAuth flow fails.

        Example:
            # In your callback handler:
            user_info = await discord_oauth.get_user_info(code, redirect_uri)
            user, created = await user_service.get_or_create_from_oauth(db, user_info)
        """
        # Exchange code for tokens
        tokens = await self.exchange_code_for_tokens(code, redirect_uri)
        access_token = tokens.get("access_token")

        if not access_token:
            raise DiscordOAuthError("No access token in response")

        # Fetch user info
        user_data = await self.fetch_user_info(access_token)

        # Discord requires email scope, but email can still be None if not verified
        email = user_data.get("email")
        if not email:
            raise DiscordOAuthError("Discord account does not have a verified email")

        # Build display name: prefer global_name, then username
        display_name = user_data.get("global_name") or user_data["username"]

        # Build avatar URL
        avatar_url = self._build_avatar_url(
            user_data["id"],
            user_data.get("avatar"),
        )

        # Normalize to our schema
        return OAuthUserInfo(
            provider="discord",
            oauth_id=user_data["id"],
            email=email,
            name=display_name,
            avatar_url=avatar_url,
        )

    def is_configured(self) -> bool:
        """Check if Discord OAuth is properly configured.

        Returns:
            True if client ID and secret are set.
        """
        return bool(settings.discord_client_id and settings.discord_client_secret)


# Global instance
discord_oauth = DiscordOAuth()
