"""
THE REFRESH TOKEN AS AN httpOnly COOKIE — for browser callers only.

There are three clients now and they cannot all hold a credential the same way.

    the desktop app   tokens in <userData>/auth.bin, encrypted by the OS
                      keychain through Electron's safeStorage. The renderer
                      never sees the refresh token at all.
    the website       the same JSON contract, tokens in page memory, because
                      lifey.planysoft.com signs you in and hands straight off
                      to the app.
    the PWA           a browser tab that has to survive being closed and
                      reopened, with no keychain and no main process.

For the third, the only two places a refresh token can live are `localStorage`
and an httpOnly cookie. `localStorage` is readable by any script that reaches
the page, and the PWA renders user-authored text on every screen — so one
injection is a thirty-day credential, exfiltrated silently. A cookie the page
cannot read costs an XSS the ability to steal the session outright; it can still
act as the user while the tab is open, which is a far smaller and far shorter
window.

---------------------------------------------------------------------------
IT IS OPT-IN, PER REQUEST, AND THAT IS LOAD-BEARING
---------------------------------------------------------------------------

`X-Lifey-Client: web` is what asks for it. Without the header every endpoint
answers exactly as it did before — same body, same fields, no `Set-Cookie`.
The desktop client's contract is therefore untouched by construction rather
than by care, which matters because `apiClient.js` re-saves the refresh token
it already held and `tests/test_refresh_contract.py` exists to catch anybody
breaking that.

WHEN THE COOKIE IS SET, `refresh` LEAVES THE BODY. Sending both would hand the
page the very string the cookie exists to keep away from it, and a client that
found it there would store it — which is the whole failure this prevents,
reintroduced by politeness.

---------------------------------------------------------------------------
WHY SameSite=Lax IS ENOUGH, AND WHY IT IS ALSO THE ONLY THING PROTECTING IT
---------------------------------------------------------------------------

`settings.base` sets `CORS_ALLOW_ALL_ORIGINS = True`, because a packaged
Electron renderer loads from `file://` and sends `Origin: null`, which no
allowlist can express. Turning `CORS_ALLOW_CREDENTIALS` on alongside it means
django-cors-headers echoes ANY origin with `Access-Control-Allow-Credentials:
true` — so the thing standing between a hostile page and this cookie is not
CORS. It is `SameSite`.

mylifey.planysoft.com and lifey.planysoft.com are the same SITE (same registrable
domain), so Lax sends the cookie on the PWA's requests. evil.com is not, so Lax
withholds it from theirs — including from a `fetch(..., {credentials:
'include'})` that CORS would otherwise permit. A response evil.com can read is
worthless to it without a credential to make the request with, and it has none.

`Strict` would be stronger by one case and would break sign-in: the OAuth
callback returns by top-level navigation, and Strict withholds the cookie on
exactly that.

`Path` is scoped to the auth endpoints, so the cookie is not attached to the
several hundred row requests a session makes, none of which can use it.

`Secure` follows `SESSION_COOKIE_SECURE`, so development over plain HTTP works
and production cannot accidentally ship a cookie that travels in clear.
"""

from django.conf import settings

# The header a caller sends to ask for cookie behaviour, and the value that
# means it. Spelled here rather than at three call sites.
CLIENT_HEADER = 'HTTP_X_LIFEY_CLIENT'
WEB_CLIENT = 'web'

REFRESH_COOKIE = 'lifey_refresh'

# Every path that can legitimately receive it: the refresh exchange itself, and
# logout, which has to blacklist and clear it. Both live under /api/, and
# /api/token/refresh/ and /api/auth/logout/ have no common prefix below that —
# so the scope is /api/ and the SameSite rule above is what does the real work.
REFRESH_COOKIE_PATH = '/api/'


def wants_cookie(request):
    """Is this the PWA asking to be given a cookie instead of a token string?"""
    return request.META.get(CLIENT_HEADER, '').strip().lower() == WEB_CLIENT


def read_refresh(request):
    """
    The refresh token this request carries, from the body or from the cookie.

    THE BODY WINS. A desktop client always sends one and never has a cookie; a
    PWA sends none and always has one. On the one caller that could have both —
    a browser that signed in before the cookie existed and still holds a token
    in memory — the body is the token it is actually trying to use, and
    preferring the cookie would silently refresh a different session.
    """
    supplied = request.data.get('refresh') if hasattr(request, 'data') else None
    if supplied:
        return supplied
    return request.COOKIES.get(REFRESH_COOKIE) or None


def attach_refresh(response, request):
    """
    Move `refresh` out of `payload` and into an httpOnly cookie, when asked.

    Returns the response, so it reads as one expression at the call site. A
    caller that did not ask for a cookie gets the response back untouched.
    """
    if not wants_cookie(request):
        return response

    token = response.data.pop('refresh', None) if hasattr(response, 'data') else None
    if not token:
        return response

    lifetime = settings.SIMPLE_JWT['REFRESH_TOKEN_LIFETIME']

    response.set_cookie(
        REFRESH_COOKIE,
        token,
        max_age=int(lifetime.total_seconds()),
        httponly=True,
        secure=settings.SESSION_COOKIE_SECURE,
        samesite='Lax',
        path=REFRESH_COOKIE_PATH,
    )
    return response


def clear_refresh(response):
    """
    Drop the cookie on the way out of a session.

    Unconditional, unlike `attach_refresh`. Logging out is the one moment a
    stale cookie must not survive, and a caller that never had one loses
    nothing by being told to delete it.
    """
    response.delete_cookie(REFRESH_COOKIE, path=REFRESH_COOKIE_PATH)
    return response
