"""The app's OWN signed session cookie — the thing that keeps a user logged
in AFTER a BW Auth login (Pattern B, PUBLIC mode).

BW's id_token is a one-shot 300s bootstrap: we verify it in /auth/callback,
read the identity, and DISCARD it. 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 (same shape as the retired shim grant, but app-signed and longer-lived):

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

No host-binding: this cookie is set by THIS app for THIS host and never
travels; the value proves nothing except "this app minted a session for
<username> until <exp>".
"""
from __future__ import annotations

import hashlib
import hmac
import time

# Cookie the browser carries. HttpOnly + Secure + SameSite=Lax (set by the
# route that mints it). Distinct name from the retired shim's cookie.
SESSION_COOKIE = "garden2_session"

# PUBLIC-mode silent-probe loop-guard cookies (mirror example-flask):
#   PROBED  — set briefly before firing a silent probe so an anonymous visitor
#             is probed AT MOST once per window (kills redirect loops).
#   OPTOUT  — set by /logout; while present the silent probe is skipped so a
#             signed-out user isn't instantly signed back in. Cleared by an
#             explicit "Sign in" click.
PROBED_COOKIE = "bw_probed"
OPTOUT_COOKIE = "bw_optout"
PROBE_MAX_AGE = 600  # seconds
OPTOUT_MAX_AGE = 60 * 60 * 24 * 365  # a year


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 — no timing oracle between
    # forged and expired-but-valid.
    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. Rejects protocol-relative (`//host`)
    and backslash-normalized (`/\\host`) open-redirect vectors — startswith
    '/' alone is NOT enough (see memory: login-gate-next-param-open-redirect).
    """
    if not raw or not raw.startswith("/") or raw.startswith("//") or raw.startswith("/\\"):
        return "/"
    return raw
