import json
import secrets
from abc import ABC, abstractmethod
from urllib.parse import urlencode

import httpx
from loguru import logger

from .exceptions import ConfigurationException, ProviderAuthException
from .models import AuthConfig, GithubAuthConfig, User


class AuthProvider(ABC):
    """Base authentication provider interface."""

    @abstractmethod
    async def get_login_url(self) -> str:
        """Return the URL for initiating login."""
        pass

    @abstractmethod
    async def process_callback(self, code: str, state: str | None = None) -> User:
        """Process the OAuth callback code and return user data."""
        pass

    @abstractmethod
    async def validate_token(self, token: str) -> bool:
        """Validate a provider token and return boolean indicating validity."""
        pass


class NoAuthProvider(AuthProvider):
    """Default provider that always authenticates (for development)."""

    def __init__(self):
        self.default_user = User(
            id="guestuser@gmail.com", name="Default User", email="guestuser@gmail.com", provider="none"
        )

    async def get_login_url(self) -> str:
        """Return the URL for initiating login."""
        return "/api/auth/callback?automatic=true"

    async def process_callback(self, code: str | None = None, state: str | None = None) -> User:
        """Process the OAuth callback code and return user data."""
        return self.default_user

    async def validate_token(self, token: str) -> bool:
        """Validate a provider token and return boolean indicating validity."""
        return True


class GithubAuthProvider(AuthProvider):
    """GitHub OAuth authentication provider."""

    def __init__(self, config: AuthConfig):
        if not config.github:
            raise ConfigurationException("GitHub auth configuration is missing")

        self.config = config.github
        self.client_id = self.config.client_id
        self.client_secret = self.config.client_secret
        self.callback_url = self.config.callback_url
        self.scopes = self.config.scopes

    async def get_login_url(self) -> str:
        """Return the GitHub OAuth login URL."""
        state = secrets.token_urlsafe(32)  # Generate a secure random state
        params = {
            "client_id": self.client_id,
            "redirect_uri": self.callback_url,
            "scope": " ".join(self.scopes),
            "state": state,
            "allow_signup": "true",
        }
        return f"https://github.com/login/oauth/authorize?{urlencode(params)}"

    async def process_callback(self, code: str, state: str | None = None) -> User:
        """Exchange code for access token and get user info."""
        if not code:
            raise ProviderAuthException("github", "Authorization code is missing")

        # Exchange code for access token
        token_url = "https://github.com/login/oauth/access_token"
        token_data = {
            "client_id": self.client_id,
            "client_secret": self.client_secret,
            "code": code,
            "redirect_uri": self.callback_url,
        }

        async with httpx.AsyncClient() as client:
            token_response = await client.post(token_url, data=token_data, headers={"Accept": "application/json"})

            if token_response.status_code != 200:
                logger.error(f"GitHub token exchange failed: {token_response.text}")
                raise ProviderAuthException("github", "Failed to exchange code for access token")

            token_json = token_response.json()
            access_token = token_json.get("access_token")

            if not access_token:
                logger.error(f"No access token in GitHub response: {token_json}")
                raise ProviderAuthException("github", "No access token received")

            # Get user info with the access token
            user_response = await client.get(
                "https://api.github.com/user",
                headers={"Authorization": f"token {access_token}", "Accept": "application/json"},
            )

            if user_response.status_code != 200:
                logger.error(f"GitHub user info fetch failed: {user_response.text}")
                raise ProviderAuthException("github", "Failed to fetch user information")

            user_data = user_response.json()

            # Get user emails if scope includes email
            email = None
            if "user:email" in self.scopes:
                email_response = await client.get(
                    "https://api.github.com/user/emails",
                    headers={"Authorization": f"token {access_token}", "Accept": "application/json"},
                )

                if email_response.status_code == 200:
                    emails = email_response.json()
                    primary_emails = [e for e in emails if e.get("primary") is True]
                    if primary_emails:
                        email = primary_emails[0].get("email")

            # Create User object
            return User(
                id=str(user_data.get("id")),
                name=user_data.get("name") or user_data.get("login"),
                email=email,
                avatar_url=user_data.get("avatar_url"),
                provider="github",
                metadata={
                    "login": user_data.get("login"),
                    "github_id": user_data.get("id"),
                    "access_token": access_token,
                },
            )

    async def validate_token(self, token: str) -> bool:
        """Validate a GitHub access token."""
        async with httpx.AsyncClient() as client:
            response = await client.get(
                "https://api.github.com/user", headers={"Authorization": f"token {token}", "Accept": "application/json"}
            )
            return response.status_code == 200


class MSALAuthProvider(AuthProvider):
    """Microsoft Authentication Library (MSAL) provider."""

    def __init__(self, config: AuthConfig):
        if not config.msal:
            raise ConfigurationException("MSAL auth configuration is missing")

        self.config = config.msal
        # MSAL provider implementation would go here
        # This is a placeholder - full implementation would use msal library

    async def get_login_url(self) -> str:
        """Return the MSAL OAuth login URL."""
        # Placeholder - would use MSAL library to generate auth URL
        return "https://login.microsoftonline.com/placeholder"

    async def process_callback(self, code: str, state: str | None = None) -> User:
        """Process the MSAL callback."""
        # Placeholder - would use MSAL library to process code and get token/user info
        return User(id="msal_user_id", name="MSAL User", provider="msal")

    async def validate_token(self, token: str) -> bool:
        """Validate an MSAL token."""
        # Placeholder - would validate token with MSAL library
        return False


class FirebaseAuthProvider(AuthProvider):
    """Firebase authentication provider."""

    def __init__(self, config: AuthConfig):
        if not config.firebase:
            raise ConfigurationException("Firebase auth configuration is missing")

        self.config = config.firebase
        # Firebase provider implementation would go here
        # This is a placeholder - full implementation would use Firebase Admin SDK

    async def get_login_url(self) -> str:
        """Return information for Firebase auth (used differently than OAuth)."""
        # Firebase auth is typically handled on the client side
        return json.dumps(
            {"apiKey": self.config.api_key, "authDomain": self.config.auth_domain, "projectId": self.config.project_id}
        )

    async def process_callback(self, code: str, state: str | None = None) -> User:
        """Process a Firebase ID token."""
        # Placeholder - would verify Firebase ID token and get user info
        return User(id="firebase_user_id", name="Firebase User", provider="firebase")

    async def validate_token(self, token: str) -> bool:
        """Validate a Firebase ID token."""
        # Placeholder - would validate token with Firebase Admin SDK
        return False
