"""
OAuth Service for Email Integration

Handles OAuth flows for Google and Microsoft to obtain tokens for
IMAP/SMTP XOAUTH2 authentication.

Google OAuth:
- Uses google-auth-oauthlib for OAuth 2.0 flow
- Scopes: Gmail IMAP and SMTP access
- Supports on-demand token refresh

Microsoft OAuth:
- Uses MSAL (Microsoft Authentication Library)
- Scopes: IMAP.AccessAsUser.All, SMTP.Send
- Supports on-demand token refresh
"""

import logging
from datetime import timedelta
from typing import Optional
from urllib.parse import urlencode

from django.conf import settings
from django.utils import timezone

from google.oauth2.credentials import Credentials as GoogleCredentials
from google_auth_oauthlib.flow import Flow as GoogleFlow
from google.auth.transport.requests import Request as GoogleAuthRequest

import msal

from .models import OAuthCredential

logger = logging.getLogger(__name__)


# Google OAuth scopes for email access
GOOGLE_SCOPES = [
    'https://mail.google.com/',  # Full Gmail access (IMAP, SMTP)
    'openid',
    'https://www.googleapis.com/auth/userinfo.email',
]

# Microsoft OAuth scopes for email access
MICROSOFT_SCOPES = [
    'https://outlook.office.com/IMAP.AccessAsUser.All',
    'https://outlook.office.com/SMTP.Send',
    'offline_access',  # For refresh tokens
    'openid',
    'email',
]


class GoogleOAuthService:
    """
    Service for handling Google OAuth flow for email access.
    """

    def __init__(self):
        self.client_id = getattr(settings, 'GOOGLE_OAUTH_CLIENT_ID', '')
        self.client_secret = getattr(settings, 'GOOGLE_OAUTH_CLIENT_SECRET', '')

    def is_configured(self) -> bool:
        """Check if Google OAuth is configured."""
        return bool(self.client_id and self.client_secret)

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

        Args:
            redirect_uri: The callback URL after authorization
            state: CSRF state token

        Returns:
            Authorization URL to redirect user to
        """
        if not self.is_configured():
            raise ValueError("Google OAuth is not configured")

        flow = GoogleFlow.from_client_config(
            {
                'web': {
                    'client_id': self.client_id,
                    'client_secret': self.client_secret,
                    'auth_uri': 'https://accounts.google.com/o/oauth2/auth',
                    'token_uri': 'https://oauth2.googleapis.com/token',
                }
            },
            scopes=GOOGLE_SCOPES,
            redirect_uri=redirect_uri,
        )

        authorization_url, _ = flow.authorization_url(
            access_type='offline',  # Get refresh token
            include_granted_scopes='true',
            state=state,
            prompt='consent',  # Force consent to get refresh token
        )

        return authorization_url

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

        Args:
            code: Authorization code from callback
            redirect_uri: Same redirect URI used in authorization

        Returns:
            Dict with access_token, refresh_token, expires_in, email
        """
        if not self.is_configured():
            raise ValueError("Google OAuth is not configured")

        flow = GoogleFlow.from_client_config(
            {
                'web': {
                    'client_id': self.client_id,
                    'client_secret': self.client_secret,
                    'auth_uri': 'https://accounts.google.com/o/oauth2/auth',
                    'token_uri': 'https://oauth2.googleapis.com/token',
                }
            },
            scopes=GOOGLE_SCOPES,
            redirect_uri=redirect_uri,
        )

        flow.fetch_token(code=code)
        credentials = flow.credentials

        # Get user email from ID token
        from google.oauth2 import id_token
        from google.auth.transport import requests

        try:
            id_info = id_token.verify_oauth2_token(
                credentials.id_token,
                requests.Request(),
                self.client_id
            )
            email = id_info.get('email', '')
        except Exception as e:
            logger.warning(f"Could not extract email from ID token: {e}")
            email = ''

        return {
            'access_token': credentials.token,
            'refresh_token': credentials.refresh_token,
            'expires_in': (credentials.expiry - timezone.now()).total_seconds() if credentials.expiry else 3600,
            'email': email,
            'scopes': list(credentials.scopes) if credentials.scopes else GOOGLE_SCOPES,
        }

    def refresh_token(self, credential: OAuthCredential) -> bool:
        """
        Refresh an expired Google OAuth token.

        Args:
            credential: OAuthCredential instance to refresh

        Returns:
            True if refresh successful, False otherwise
        """
        if not credential.refresh_token:
            logger.error(f"No refresh token for credential {credential.id}")
            credential.mark_invalid("No refresh token available")
            return False

        try:
            creds = GoogleCredentials(
                token=credential.access_token,
                refresh_token=credential.refresh_token,
                token_uri='https://oauth2.googleapis.com/token',
                client_id=self.client_id,
                client_secret=self.client_secret,
            )

            creds.refresh(GoogleAuthRequest())

            # Calculate expires_in from expiry datetime
            expires_in = None
            if creds.expiry:
                expires_in = int((creds.expiry - timezone.now()).total_seconds())

            credential.update_tokens(
                access_token=creds.token,
                refresh_token=creds.refresh_token,  # May be rotated
                expires_in=expires_in or 3600,
            )

            logger.info(f"Successfully refreshed Google token for {credential.email}")
            return True

        except Exception as e:
            logger.error(f"Failed to refresh Google token for {credential.email}: {e}")
            credential.mark_invalid(str(e))
            return False


class MicrosoftOAuthService:
    """
    Service for handling Microsoft OAuth flow for email access.
    Uses MSAL (Microsoft Authentication Library).
    """

    def __init__(self):
        self.client_id = getattr(settings, 'MICROSOFT_OAUTH_CLIENT_ID', '')
        self.client_secret = getattr(settings, 'MICROSOFT_OAUTH_CLIENT_SECRET', '')
        self.tenant_id = getattr(settings, 'MICROSOFT_OAUTH_TENANT_ID', 'common')

    def is_configured(self) -> bool:
        """Check if Microsoft OAuth is configured."""
        return bool(self.client_id and self.client_secret)

    @property
    def authority(self) -> str:
        """Get the Microsoft authority URL."""
        return f'https://login.microsoftonline.com/{self.tenant_id}'

    def _get_msal_app(self) -> msal.ConfidentialClientApplication:
        """Create MSAL application instance."""
        return msal.ConfidentialClientApplication(
            self.client_id,
            authority=self.authority,
            client_credential=self.client_secret,
        )

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

        Args:
            redirect_uri: The callback URL after authorization
            state: CSRF state token

        Returns:
            Authorization URL to redirect user to
        """
        if not self.is_configured():
            raise ValueError("Microsoft OAuth is not configured")

        app = self._get_msal_app()

        auth_url = app.get_authorization_request_url(
            scopes=MICROSOFT_SCOPES,
            redirect_uri=redirect_uri,
            state=state,
            prompt='consent',  # Force consent to get refresh token
        )

        return auth_url

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

        Args:
            code: Authorization code from callback
            redirect_uri: Same redirect URI used in authorization

        Returns:
            Dict with access_token, refresh_token, expires_in, email
        """
        if not self.is_configured():
            raise ValueError("Microsoft OAuth is not configured")

        app = self._get_msal_app()

        result = app.acquire_token_by_authorization_code(
            code=code,
            scopes=MICROSOFT_SCOPES,
            redirect_uri=redirect_uri,
        )

        if 'error' in result:
            raise ValueError(f"Token exchange failed: {result.get('error_description', result.get('error'))}")

        # Extract email from ID token claims
        email = ''
        if 'id_token_claims' in result:
            email = result['id_token_claims'].get('preferred_username', '')
            if not email:
                email = result['id_token_claims'].get('email', '')

        return {
            'access_token': result['access_token'],
            'refresh_token': result.get('refresh_token', ''),
            'expires_in': result.get('expires_in', 3600),
            'email': email,
            'scopes': result.get('scope', '').split() if result.get('scope') else MICROSOFT_SCOPES,
        }

    def refresh_token(self, credential: OAuthCredential) -> bool:
        """
        Refresh an expired Microsoft OAuth token.

        Args:
            credential: OAuthCredential instance to refresh

        Returns:
            True if refresh successful, False otherwise
        """
        if not credential.refresh_token:
            logger.error(f"No refresh token for credential {credential.id}")
            credential.mark_invalid("No refresh token available")
            return False

        try:
            app = self._get_msal_app()

            # MSAL handles refresh internally when using acquire_token_by_refresh_token
            result = app.acquire_token_by_refresh_token(
                refresh_token=credential.refresh_token,
                scopes=MICROSOFT_SCOPES,
            )

            if 'error' in result:
                raise ValueError(result.get('error_description', result.get('error')))

            credential.update_tokens(
                access_token=result['access_token'],
                refresh_token=result.get('refresh_token', credential.refresh_token),
                expires_in=result.get('expires_in', 3600),
            )

            logger.info(f"Successfully refreshed Microsoft token for {credential.email}")
            return True

        except Exception as e:
            logger.error(f"Failed to refresh Microsoft token for {credential.email}: {e}")
            credential.mark_invalid(str(e))
            return False


def get_oauth_service(provider: str):
    """
    Factory function to get the appropriate OAuth service.

    Args:
        provider: 'google' or 'microsoft'

    Returns:
        OAuth service instance
    """
    if provider == 'google':
        return GoogleOAuthService()
    elif provider == 'microsoft':
        return MicrosoftOAuthService()
    else:
        raise ValueError(f"Unknown OAuth provider: {provider}")


def ensure_valid_token(credential: OAuthCredential) -> Optional[str]:
    """
    Ensure the OAuth credential has a valid (non-expired) token.
    Refreshes if needed.

    Args:
        credential: OAuthCredential instance

    Returns:
        Valid access token, or None if refresh failed
    """
    if not credential.is_valid:
        logger.warning(f"Credential {credential.id} is marked invalid")
        return None

    if not credential.needs_refresh():
        credential.mark_used()
        return credential.access_token

    # Need to refresh
    logger.info(f"Refreshing token for {credential.email}")
    service = get_oauth_service(credential.provider)

    if service.refresh_token(credential):
        credential.mark_used()
        return credential.access_token

    return None


def generate_xoauth2_string(email: str, access_token: str) -> str:
    """
    Generate XOAUTH2 authentication string for IMAP/SMTP.

    The format is: base64("user=" + email + "^Aauth=Bearer " + access_token + "^A^A")
    where ^A is ASCII character 0x01

    Args:
        email: User's email address
        access_token: OAuth access token

    Returns:
        Base64-encoded XOAUTH2 string
    """
    import base64

    auth_string = f'user={email}\x01auth=Bearer {access_token}\x01\x01'
    return base64.b64encode(auth_string.encode()).decode()
