"""
YOUR COMPANY — FAB Club OAuth 2.0 Social Auth Backend

This module plugs into python-social-auth as a custom OAuth2 provider.
It connects every tenant site on the platform to fab.club as the
identity provider.

Flow:
  1. User clicks "Sign in with FAB" on any FT tenant site
  2. Redirect to fab.club/oauth/authorize/?client_id=...&scope=...&code_challenge=...
  3. User approves → redirect back with code
  4. FT backend exchanges code for access token
  5. FT backend calls fab.club/oauth/userinfo/ → gets member profile
  6. python-social-auth creates/updates local User record and attaches FAB data

Configuration lives in the panel, under Settings, not in .env and not in
settings.py. The client id and secret are columns on SiteSettings, and
get_key_and_secret() below reads them from there. Scope stays in settings
because it is not a credential:

    SOCIAL_AUTH_FABCLUB_SCOPE  = ['openid', 'profile', 'email', 'points']

PKCE (S256) is always used — FAB Club requires it.
"""
import hashlib
import base64
import os

from social_core.backends.oauth import BaseOAuth2


class FABClubOAuth2(BaseOAuth2):
    """
    python-social-auth backend for FAB Club.
    Drop this class into AUTHENTICATION_BACKENDS alongside Django's default.
    """
    name = 'fabclub'

    # ── Endpoints ──────────────────────────────────────────────────────────────
    BASE_URL            = 'https://fab.club'
    AUTHORIZATION_URL   = f'{BASE_URL}/oauth/authorize/'
    ACCESS_TOKEN_URL    = f'{BASE_URL}/oauth/token/'
    REFRESH_TOKEN_URL   = f'{BASE_URL}/oauth/token/'
    REVOKE_TOKEN_URL    = f'{BASE_URL}/oauth/revoke-token/'
    USERINFO_URL        = f'{BASE_URL}/oauth/userinfo/'
    ACCESS_TOKEN_METHOD = 'POST'
    REDIRECT_STATE      = False
    DEFAULT_SCOPE       = ['openid', 'profile', 'email']
    EXTRA_DATA          = [
        ('fab_number',   'fab_number'),
        ('tier',         'fab_tier'),
        ('tier_colour',  'fab_tier_colour'),
        ('points',       'fab_points'),
        ('wallet_balance_pence', 'fab_wallet_pence'),
    ]

    # ── Credentials ────────────────────────────────────────────────────────────
    def get_key_and_secret(self):
        """
        The client id and secret, from the database.

        social-core's default asks the strategy, which reads
        SOCIAL_AUTH_FABCLUB_KEY and SOCIAL_AUTH_FABCLUB_SECRET out of Django
        settings, which read .env. That is the one place a live secret still
        had to sit in a file. Overriding this single method moves it into the
        encrypted column without touching the rest of social-auth.

        Falls back to the strategy so a tenant that has not filled Settings in
        yet behaves exactly as before rather than breaking.
        """
        key = secret = ''
        try:
            from apps.pages.api_keys import _site
            site = _site(getattr(getattr(self, 'strategy', None), 'request', None)
                         and getattr(self.strategy.request, 'tenant', None))
            if site:
                key    = (site.fab_oauth_client_id or '').strip()
                secret = (site.fab_oauth_client_secret or '').strip()
        except Exception:
            pass
        if key and secret:
            return key, secret
        return super().get_key_and_secret()

    # ── PKCE (S256) — required by FAB Club ─────────────────────────────────────
    def auth_params(self, state=None):
        """
        Inject PKCE code_challenge into the authorization URL.
        We store the verifier in the session so it can be sent with the
        token exchange in request_access_token().
        """
        params = super().auth_params(state=state)

        verifier = base64.urlsafe_b64encode(os.urandom(32)).rstrip(b'=').decode()
        challenge = base64.urlsafe_b64encode(
            hashlib.sha256(verifier.encode()).digest()
        ).rstrip(b'=').decode()

        # Persist verifier for use in token exchange
        self.strategy.session_set('fab_pkce_verifier', verifier)

        params['code_challenge']        = challenge
        params['code_challenge_method'] = 'S256'
        return params

    def auth_complete_params(self, state=None):
        """Send code_verifier when exchanging authorization code for token."""
        params = super().auth_complete_params(state=state)
        verifier = self.strategy.session_get('fab_pkce_verifier')
        if verifier:
            params['code_verifier'] = verifier
        return params

    # ── User data ───────────────────────────────────────────────────────────────
    def user_data(self, access_token, *args, **kwargs):
        """
        Call FAB Club userinfo endpoint and return member data dict.
        The dict is passed to get_user_details() and create_user_from_details().
        """
        return self.get_json(
            self.USERINFO_URL,
            headers={'Authorization': f'Bearer {access_token}'},
        )

    def get_user_details(self, response):
        """
        Map FAB Club userinfo fields → python-social-auth standard fields.
        """
        return {
            'username':    response.get('fab_number', ''),
            'email':       response.get('email', ''),
            'first_name':  response.get('given_name', ''),
            'last_name':   response.get('family_name', ''),
            # Extra FAB-specific fields (stored in social_auth extra_data)
            'fab_number':  response.get('fab_number'),
            'fab_tier':    response.get('tier'),
            'fab_points':  response.get('points', 0),
            'fab_wallet_pence': response.get('wallet_balance_pence', 0),
        }

    def get_user_id(self, details, response):
        """
        Use the stable FAB Club UUID (sub) as the unique identifier.
        This never changes even if the member updates their email.
        """
        return response.get('sub')
