"""
The two redirect endpoints of website social sign-in.

Plain Django views, not DRF: what a browser follows is a 302, and a view whose
whole output is a `Location` header has no use for a renderer, a serializer or
an authentication class. They hold `state` and `nonce` in the session for the
length of the round trip, the same way `views.py` holds the desktop request.
"""

import logging
import urllib.parse

from django.conf import settings
from django.http import HttpResponseRedirect
from django.shortcuts import redirect, render
from django.urls import reverse
from django.views.decorators.http import require_http_methods

from .auth_codes import generate_handoff_code, hash_code
from .models import CustomUser, OAuthHandoffCode
from .oauth import OAuthError, authorize_url, exchange_code, identity_from, new_state, provider_config

logger = logging.getLogger(__name__)

SESSION_KEY = 'web_oauth_request'


def _redirect_uri(request, provider):
    """
    The callback URL handed to the provider, and registered in its console.

    `OAUTH_REDIRECT_BASE` wins when it is set, because `build_absolute_uri`
    reads the Host header — behind a proxy that terminates TLS it can come out
    as `http://`, and the provider compares the string exactly.
    """
    path = reverse('users:oauth-callback', kwargs={'provider': provider})

    if settings.OAUTH_REDIRECT_BASE:
        return f"{settings.OAUTH_REDIRECT_BASE.rstrip('/')}{path}"

    return request.build_absolute_uri(path)


def _back_to_site(fragment):
    """
    Hand the browser back to the website's auth page.

    A FRAGMENT, NOT A QUERY STRING. A fragment is never sent to a server, so
    the code cannot leak into this site's access log, the next request's
    `Referer`, or any analytics call the page makes.
    """
    return HttpResponseRedirect(f'{settings.LIFEY_WEB_AUTH_URL}#{fragment}')


def _fail(message):
    return _back_to_site(urllib.parse.urlencode({'error': message}))


def _to_pwa(fragment):
    """
    Hand the browser straight to the app, skipping the website entirely.

    Same fragment-not-query-string reasoning as `_back_to_site` — the code
    must not reach `LIFEY_PWA_URL`'s access log or the next request's
    `Referer`. `core.views.pwa_index` is what answers at that URL.
    """
    return HttpResponseRedirect(f"{settings.LIFEY_PWA_URL.rstrip('/')}/#{fragment}")


# `next` is an ALLOWLISTED KEYWORD, never a URL — accepting a URL here is the
# open redirect every one of these flows gets wrong once. `pwa` sends the
# browser straight to `/app/`; `app` and the unlisted default ('site') both
# still go through `_back_to_site` and are told apart there by `pending['next']`.
NEXT_KEYWORDS = {'app', 'pwa'}


@require_http_methods(['GET'])
def oauth_start(request, provider):
    """Send the visitor to Google or Microsoft."""
    try:
        provider_config(provider)
        state = new_state()
        nonce = new_state()
        redirect_uri = _redirect_uri(request, provider)
        url = authorize_url(provider, redirect_uri, state, nonce)
    except OAuthError as error:
        return _fail(str(error))

    # `next` decides where the PAGE goes after the exchange — see NEXT_KEYWORDS.
    requested_next = request.GET.get('next')
    request.session[SESSION_KEY] = {
        'provider': provider,
        'state': state,
        'nonce': nonce,
        'redirect_uri': redirect_uri,
        'next': requested_next if requested_next in NEXT_KEYWORDS else 'site',
    }

    return HttpResponseRedirect(url)


@require_http_methods(['GET'])
def oauth_callback(request, provider):
    """Turn the provider's code into a Lifey handoff code."""
    pending = request.session.pop(SESSION_KEY, None)

    if not pending or pending.get('provider') != provider:
        return _fail('That sign-in has expired. Try again.')

    # The provider reports a user pressing Cancel this way, and it is not an
    # error worth a message of its own.
    if request.GET.get('error'):
        logger.info('OAuth denied by user or provider (%s): %s', provider, request.GET.get('error'))
        return _fail('Sign-in was cancelled.')

    state = request.GET.get('state', '')
    code = request.GET.get('code', '')

    if not code or not state or state != pending['state']:
        return _fail('That sign-in could not be verified. Try again.')

    try:
        tokens = exchange_code(provider, code, pending['redirect_uri'])
        identity = identity_from(provider, tokens, pending['nonce'])
    except OAuthError as error:
        return _fail(str(error))

    user, created = CustomUser.objects.get_or_create(
        email=identity['email'],
        defaults={
            'first_name': identity['first_name'],
            'last_name': identity['last_name'],
            'oauth_provider': provider,
            # The provider has already proved the address — `identity_from`
            # refuses a token that says otherwise — so asking the user to read
            # a confirmation email would be theatre.
            'email_verified': True,
        },
    )

    if not created and not user.email_verified:
        # An account that signed up with a password and never confirmed, now
        # arriving through the provider that owns the address. That is the
        # proof the confirmation email was asking for.
        user.email_verified = True
        user.save(update_fields=['email_verified'])

    if created:
        # NO PASSWORD IS SET, AND NONE IS GUESSABLE. An unusable password means
        # the account cannot be signed into by the password form until the user
        # deliberately sets one through a reset.
        user.set_unusable_password()
        user.save(update_fields=['password'])
    elif not user.is_active:
        return _fail('This account is not active.')

    handoff = generate_handoff_code()
    OAuthHandoffCode.objects.sweep()
    OAuthHandoffCode.objects.create(
        code_hash=hash_code(handoff),
        user=user,
        provider=provider,
    )

    logger.info('OAuth sign-in (%s), new account: %s', provider, created)

    # `pwa` skips the website's own continue screen — the PWA has no sign-in
    # screen of its own (docs/pwa-boundary.md §2) and `main.web.jsx` redeems
    # `#code=…` through this same `oauth_exchange` before React mounts.
    if pending['next'] == 'pwa':
        return _to_pwa(urllib.parse.urlencode({'code': handoff}))

    return _back_to_site(urllib.parse.urlencode({
        'code': handoff,
        'provider': provider,
        'next': pending['next'],
    }))
