"""
The BROWSER half of desktop sign-in: HTML pages with a session cookie.

The desktop app opens these in the user's own browser rather than collecting a
password in an Electron window. What that buys: no password ever reaches the
renderer's memory or a crash dump, the browser's own password manager and 2FA
work, social sign-in later becomes a server-side change with no client work,
and this page can be fixed without shipping an app update.

What it costs is the deep link, which can fail for reasons the app cannot see —
so `done()` prints the code as text and the app can take it typed. On a Linux
AppImage that is not a fallback, it is the only path (§0a of the plan).
"""

import functools
import logging

from django.conf import settings
from django.http import Http404, 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_code, hash_code
from .forms import DesktopRegisterForm, DesktopSignInForm
from .models import DesktopAuthCode

logger = logging.getLogger(__name__)

SESSION_KEY = 'desktop_auth_request'
CALLBACK_URL = 'lifey://auth/callback'


def _remember_request(request):
    """
    Pull `state` and `code_challenge` off the query string and hold them in the
    session for the POST that follows.

    They are NOT round-tripped through hidden form fields: a hidden field is
    editable by whatever is rendering the page, and the whole point of the
    state is that the app can recognise its own request coming back.

    A REDIRECT URI IS NEVER ACCEPTED. The server knows the only one it will
    ever use, so a forged sign-in link cannot redirect anywhere else — which
    is the single most common way this flow is got wrong.
    """
    state = request.GET.get('state', '').strip()
    challenge = request.GET.get('code_challenge', '').strip()
    method = request.GET.get('code_challenge_method', '').strip().upper()

    if not state or not challenge or method != 'S256':
        return False

    request.session[SESSION_KEY] = {'state': state, 'code_challenge': challenge}
    return True


def _pending(request):
    return request.session.get(SESSION_KEY)


def _mint(request, user, pending):
    """Create the one-time code and hand the browser to the app."""
    DesktopAuthCode.objects.sweep()

    code = generate_code()
    DesktopAuthCode.objects.create(
        code_hash=hash_code(code),
        state=pending['state'],
        code_challenge=pending['code_challenge'],
        user=user,
    )

    request.session.pop(SESSION_KEY, None)
    # The code goes in the session for the done page to print ONCE. It is the
    # same 60-second credential either way, and the alternative — putting it
    # in the redirect's query string — writes it into browser history.
    request.session['desktop_auth_code'] = code
    request.session['desktop_auth_state'] = pending['state']

    return redirect(reverse('auth-browser:desktop-done'))


def enabled_only(view):
    """
    404 unless `LIFEY_DESKTOP_BROWSER_AUTH` is on.

    THE ROUTES STAY REGISTERED WHILE THE PAGES ARE DARK, and that is the point
    of gating here rather than in `browser_urls.py`. `_mint` calls
    `reverse('auth-browser:desktop-done')`; a conditional `urlpatterns` would
    make that raise `NoReverseMatch` — a 500 — the moment the flag came back
    on for a half-configured environment, and would also break every test that
    reverses one of these names.
    404 rather than 503: a path the product no longer serves is not a path
    that is temporarily unwell, and a 503 invites a retry.
    Read at call time, not import time, so `override_settings` works.
    """

    @functools.wraps(view)
    def wrapper(request, *args, **kwargs):
        if not getattr(settings, 'LIFEY_DESKTOP_BROWSER_AUTH', False):
            raise Http404('The browser sign-in pages are not enabled.')
        return view(request, *args, **kwargs)

    return wrapper


def _error(request, message, status=400):
    return render(request, 'auth/desktop/error.html', {'message': message}, status=status)


@enabled_only
@require_http_methods(['GET', 'POST'])
def desktop_sign_in(request):
    if request.method == 'GET' and not _remember_request(request):
        return _error(request, 'This sign-in link is incomplete. Press Sign in again in Lifey.')

    pending = _pending(request)
    if pending is None:
        return _error(request, 'This sign-in request has expired. Press Sign in again in Lifey.')

    form = DesktopSignInForm(request.POST or None, request=request)

    if request.method == 'POST' and form.is_valid():
        return _mint(request, form.user, pending)

    return render(request, 'auth/desktop/sign_in.html', {'form': form})


@enabled_only
@require_http_methods(['GET', 'POST'])
def desktop_register(request):
    pending = _pending(request)
    if pending is None:
        return _error(request, 'This sign-in request has expired. Press Sign in again in Lifey.')

    form = DesktopRegisterForm(request.POST or None)

    if request.method == 'POST' and form.is_valid():
        return _mint(request, form.save(), pending)

    return render(request, 'auth/desktop/register.html', {'form': form})


@enabled_only
@require_http_methods(['GET'])
def desktop_done(request):
    """
    "You can close this tab", plus the code as text.

    The page itself fires the deep link via a `<meta refresh>` to
    `lifey://auth/callback?...`, because a redirect to a custom scheme is not
    something every browser will follow from a 302.
    """
    code = request.session.pop('desktop_auth_code', None)
    state = request.session.pop('desktop_auth_state', None)

    if not code:
        return _error(request, 'There is nothing to hand back. Press Sign in again in Lifey.')

    return render(request, 'auth/desktop/done.html', {
        'code': code,
        'callback': f'{CALLBACK_URL}?code={code}&state={state}',
    })
