"""Swiftly bearer-JWT auto-mint via Firebase REST.

Replaces the static ``SWIFTLY_BEARER_TOKEN`` env var. The Lucky California
storefront is a Firebase Web SDK client of the ``swiftly-lu-prod`` project;
its public ``config.json`` exposes the ``firebaseApiKey``, and Firebase
Identity Toolkit's anonymous-signup endpoint mints a fresh ID token on
demand. The token is identical in shape to the one a real browser session
produces, and the Swiftly product API accepts it.

Process-local cache only — a backend restart re-mints. Mint cost is
~500ms; a 1h-TTL token is reused for ~55min of that hour. Spec:
``docs/specs/2026-05-06-swiftly-token-auto-mint.md``.
"""
from __future__ import annotations

import base64
import json
import logging
import threading
import time
from typing import Any, Dict, Tuple

import requests

logger = logging.getLogger(__name__)


class SwiftlyAuthMintError(Exception):
    """Raised when minting a fresh Swiftly JWT fails.

    Surface the message verbatim to ``ScrapeLog.error_message`` so the
    operator sees which Firebase-or-Lucky path broke.
    """


_CONFIG_URL = "https://luckysupermarkets.com/config.json"
_FIREBASE_SIGNUP_URL = "https://identitytoolkit.googleapis.com/v1/accounts:signUp"
_EXPECTED_ISS = "https://securetoken.google.com/swiftly-lu-prod"
_USER_AGENT = (
    "MealPlannerBot/1.0 (+https://mealplanner.local; contact peter@research.bike)"
)
# Refresh when the cached token has < this many seconds of life left.
_REFRESH_MARGIN_SECONDS = 300
# Reject minted tokens whose exp is suspiciously close to now.
_MIN_FRESH_TTL_SECONDS = 60

_lock = threading.Lock()
_state: Dict[str, Any] = {"token": None, "exp": 0.0, "api_key": None}


def _reset_cache_for_tests() -> None:
    """Clear the process-local cache. Test-only helper."""
    with _lock:
        _state["token"] = None
        _state["exp"] = 0.0
        _state["api_key"] = None


def _decode_jwt_payload(token: str) -> Dict[str, Any]:
    parts = token.split(".")
    if len(parts) != 3:
        raise SwiftlyAuthMintError("malformed JWT (expected 3 segments)")
    payload_b64 = parts[1]
    payload_b64 += "=" * (-len(payload_b64) % 4)
    try:
        decoded = base64.urlsafe_b64decode(payload_b64.encode("ascii"))
        return json.loads(decoded)
    except (ValueError, json.JSONDecodeError) as exc:
        raise SwiftlyAuthMintError(f"could not decode JWT payload: {exc}") from exc


def _fetch_firebase_api_key(timeout: int) -> str:
    cached = _state.get("api_key")
    if cached:
        return cached
    try:
        resp = requests.get(
            _CONFIG_URL,
            timeout=timeout,
            headers={"User-Agent": _USER_AGENT, "Accept": "application/json"},
        )
    except requests.RequestException as exc:
        raise SwiftlyAuthMintError(
            f"could not fetch {_CONFIG_URL}: {exc}"
        ) from exc
    if resp.status_code != 200:
        raise SwiftlyAuthMintError(
            f"{_CONFIG_URL} returned {resp.status_code}: {resp.text[:200]}"
        )
    try:
        data = resp.json()
    except ValueError as exc:
        raise SwiftlyAuthMintError(
            f"{_CONFIG_URL} returned non-JSON: {exc}"
        ) from exc
    api_key = data.get("firebaseApiKey")
    if not api_key:
        raise SwiftlyAuthMintError(
            "config.json missing firebaseApiKey — Lucky may have changed config shape"
        )
    _state["api_key"] = api_key
    return api_key


def mint_anonymous_token(timeout: int = 10) -> Tuple[str, float]:
    """Mint a fresh anonymous Firebase ID token.

    Returns ``(token, exp_epoch_seconds)``. Raises ``SwiftlyAuthMintError``
    on any failure. Does not touch the cache — callers handle caching.
    """
    api_key = _fetch_firebase_api_key(timeout=timeout)
    headers = {
        "Content-Type": "application/json",
        "Origin": "https://luckysupermarkets.com",
        "Referer": "https://luckysupermarkets.com/",
        "User-Agent": _USER_AGENT,
    }
    try:
        resp = requests.post(
            _FIREBASE_SIGNUP_URL,
            params={"key": api_key},
            json={"returnSecureToken": True},
            headers=headers,
            timeout=timeout,
        )
    except requests.RequestException as exc:
        raise SwiftlyAuthMintError(
            f"Firebase signUp request failed: {exc}"
        ) from exc
    if resp.status_code != 200:
        raise SwiftlyAuthMintError(
            f"Firebase signUp returned {resp.status_code}: {resp.text[:200]}"
        )
    try:
        body = resp.json()
    except ValueError as exc:
        raise SwiftlyAuthMintError(
            f"Firebase signUp returned non-JSON: {exc}"
        ) from exc
    id_token = body.get("idToken")
    if not id_token:
        raise SwiftlyAuthMintError("Firebase signUp response missing idToken")

    payload = _decode_jwt_payload(id_token)
    iss = payload.get("iss")
    if iss != _EXPECTED_ISS:
        raise SwiftlyAuthMintError(
            f"unexpected JWT iss: {iss!r} (expected {_EXPECTED_ISS!r})"
        )
    exp = payload.get("exp")
    if not isinstance(exp, (int, float)):
        raise SwiftlyAuthMintError("JWT payload missing numeric exp claim")
    if float(exp) <= time.time() + _MIN_FRESH_TTL_SECONDS:
        raise SwiftlyAuthMintError(
            f"minted JWT already near expiry (exp={exp}, now={time.time():.0f})"
        )
    return id_token, float(exp)


def get_token() -> str:
    """Return a valid Swiftly bearer JWT, minting one if needed.

    A cached token is reused when it has more than ``_REFRESH_MARGIN_SECONDS``
    of life remaining; otherwise a fresh token is minted via Firebase REST.
    """
    with _lock:
        now = time.time()
        cached = _state.get("token")
        if cached and _state.get("exp", 0.0) > now + _REFRESH_MARGIN_SECONDS:
            return cached
        token, exp = mint_anonymous_token()
        _state["token"] = token
        _state["exp"] = exp
        logger.info(
            "swiftly_auth: minted fresh JWT (exp in %.0fs)", exp - time.time()
        )
        return token
