"""The app's OWN signed session cookie — what keeps a user signed in after a BW
Auth login (Pattern B).

BW's id_token is a one-shot 300s bootstrap: it is verified in /auth/callback,
its identity read, and then DISCARDED. From then on the browser carries this
app-owned cookie, verified on every request by AuthMiddleware. It is entirely
separate from any id-auth/BW cookie.

Format:

    <username>|<exp_unix>|<hmac_hex>
    hmac = HMAC_SHA256(APP_SESSION_SECRET, "<username>|<exp>")

The value proves only "this app minted a session for <username> until <exp>".
It carries no role: roles are read from the database on each request, so
revoking access takes effect immediately instead of at cookie expiry.
"""

from __future__ import annotations

import hashlib
import hmac
import time

# The browser cookie. HttpOnly + Secure + SameSite=Lax, set by the route that
# mints it.
SESSION_COOKIE = "dailysplice_session"

# PUBLIC-mode silent-probe loop-guard cookies:
#   PROBED — set just before firing a silent probe so an anonymous visitor is
#            probed AT MOST once per window. This is what kills redirect loops.
#   OPTOUT — set by /logout; while present the silent probe is skipped so a user
#            who signed out is not instantly signed back in. An explicit
#            "Sign in" click clears it.
PROBED_COOKIE = "bw_probed"
OPTOUT_COOKIE = "bw_optout"
PROBE_MAX_AGE = 600  # seconds
OPTOUT_MAX_AGE = 60 * 60 * 24 * 365  # a year

# Where the callback sends the browser after the round-trip.
NEXT_COOKIE = "bw_next"

COOKIE_KW = {"httponly": True, "secure": True, "samesite": "lax", "path": "/"}


def sign_session(username: str, secret: str, ttl_seconds: int) -> str:
    exp = int(time.time()) + ttl_seconds
    payload = f"{username}|{exp}"
    sig = hmac.new(secret.encode(), payload.encode(), hashlib.sha256).hexdigest()
    return f"{payload}|{sig}"


def verify_session(cookie_value: str, secret: str) -> str | None:
    """Return the username for a valid, unexpired session, else None."""
    if not cookie_value or not secret:
        return None
    parts = cookie_value.split("|")
    if len(parts) != 3:
        return None
    username, exp_str, sig = parts
    if not username or not exp_str.isdigit():
        return None
    expected = hmac.new(
        secret.encode(), f"{username}|{exp_str}".encode(), hashlib.sha256
    ).hexdigest()
    # Signature first (constant-time), then expiry — so there is no timing
    # oracle distinguishing a forged cookie from an expired-but-genuine one.
    if not hmac.compare_digest(expected, sig):
        return None
    if int(exp_str) < time.time():
        return None
    return username


def safe_next(raw: str | None) -> str:
    """Same-origin relative path only.

    `startswith("/")` alone is NOT enough: `//evil.example` and `/\\evil.example`
    are both browser-normalized to another origin, which turns the next= param
    into an open redirect.
    """
    if (
        not raw
        or not raw.startswith("/")
        or raw.startswith("//")
        or raw.startswith("/\\")
    ):
        return "/"
    return raw
