# backend/auth_cookies.py
"""Generic HMAC-signed cookie payloads, shared by operator auth (and, optionally
later, the portal). A signed value is f"{b64url(json)}.{hmac_sha256(b64)}"; read()
verifies the signature in constant time and enforces a server-side iat expiry.

The signing secret is the same SECRET_KEY the portal already reads, so a single
env var protects both cookies. Never store or log raw secrets."""
import os
import hmac
import json
import time
import base64
import hashlib
import logging

logger = logging.getLogger(__name__)

# Same env var the portal cookie uses — one secret protects both. The insecure
# default only exists so dev/test boots without config; set a real SECRET_KEY in prod.
SECRET_KEY = os.getenv("SECRET_KEY", "dev-insecure-change-me")
# Set COOKIE_SECURE=true once served over HTTPS; leave false on plain HTTP or the
# browser won't send the cookie.
COOKIE_SECURE = os.getenv("COOKIE_SECURE", "false").lower() in ("1", "true", "yes")


def _sign(body: str) -> str:
    return hmac.new(SECRET_KEY.encode(), body.encode(), hashlib.sha256).hexdigest()


def sign(payload: dict) -> str:
    """Serialize + sign a payload dict into a cookie-safe string."""
    body = base64.urlsafe_b64encode(json.dumps(payload).encode()).decode()
    return f"{body}.{_sign(body)}"


def read(raw, max_age: int):
    """Verify a signed value and return its payload dict, or None if missing,
    tampered, or older than max_age seconds (by its own `iat`)."""
    if not raw or not isinstance(raw, str):
        return None
    try:
        body, sig = raw.rsplit(".", 1)
    except (ValueError, AttributeError):
        return None
    if not hmac.compare_digest(sig, _sign(body)):
        return None
    try:
        data = json.loads(base64.urlsafe_b64decode(body.encode()))
    except Exception:
        return None
    if not isinstance(data, dict):
        return None
    iat = data.get("iat")
    if not isinstance(iat, (int, float)):
        return None
    now = time.time()
    # Reject implausibly future-dated tokens: the same server signs and verifies,
    # so there's no real clock skew — a far-future iat (e.g. to dodge max_age or
    # outlive a sessions_valid_from bump) is bogus. 60s of slack is generous.
    if iat - now > 60:
        return None
    if (now - iat) > max_age:
        return None
    return data
