"""
grantflow/api/redis_store.py
----------------------------
Thin Redis facade used by the idempotency store and JTI denylist.

Falls back to no-op (in-process) when Redis is not configured or unreachable.
Configure via environment variable:

    GRANTFLOW_REDIS_URL=redis://127.0.0.1:6379/0

Leave the variable unset to keep purely in-process behaviour (default).
"""

from __future__ import annotations

import json
import logging
import os
import threading
from typing import Any, Optional

_log = logging.getLogger(__name__)

_redis_lock = threading.Lock()
_redis_client: Any = None
_redis_available: Optional[bool] = None  # None = not yet probed

_IDEM_TTL_SECONDS = 7 * 86_400  # 7 days
_JTI_DEFAULT_TTL_SECONDS = 8 * 86_400  # 8 days (covers max credential TTL + buffer)


def _redis_url() -> Optional[str]:
    url = (os.getenv("GRANTFLOW_REDIS_URL") or "").strip()
    return url if url else None


def _get_redis() -> Any:
    """Return a Redis client, or None if Redis is unavailable / not configured."""
    global _redis_client, _redis_available

    if _redis_available is False:
        return None
    if _redis_client is not None:
        return _redis_client

    url = _redis_url()
    if not url:
        _redis_available = False
        return None

    with _redis_lock:
        if _redis_client is not None:
            return _redis_client
        try:
            import redis

            client = redis.Redis.from_url(
                url,
                socket_connect_timeout=2,
                socket_timeout=1,
                decode_responses=True,
            )
            client.ping()
            _redis_client = client
            _redis_available = True
            _log.info("GrantFlow Redis store connected: %s", url)
        except Exception as exc:
            _log.warning("GrantFlow Redis store unavailable — falling back to in-process: %s", exc)
            _redis_available = False
            return None

    return _redis_client


def redis_available() -> bool:
    """True if a Redis connection has been established successfully."""
    return _get_redis() is not None


# ---------------------------------------------------------------------------
# JTI denylist
# ---------------------------------------------------------------------------


def redis_jti_revoke(jti: str, ttl_seconds: int = _JTI_DEFAULT_TTL_SECONDS) -> None:
    """Mark a JTI as revoked in Redis with a TTL.

    Errors are swallowed — in-process denylist remains the fallback.
    """
    r = _get_redis()
    if r is None:
        return
    try:
        key = f"grantflow:jti:revoked:{jti}"
        r.setex(key, ttl_seconds, "1")
    except Exception as exc:
        _log.warning("redis_jti_revoke failed for %s: %s", jti, exc)


def redis_jti_is_revoked(jti: str) -> bool:
    """Return True if the JTI is in the Redis denylist.

    Returns False on any error (in-process set is the reliable fallback).
    """
    r = _get_redis()
    if r is None:
        return False
    try:
        key = f"grantflow:jti:revoked:{jti}"
        return bool(r.exists(key))
    except Exception as exc:
        _log.warning("redis_jti_is_revoked failed for %s: %s", jti, exc)
        return False


# ---------------------------------------------------------------------------
# Global idempotency store
# ---------------------------------------------------------------------------


def redis_idem_get(scope: str, request_id: str) -> Optional[dict[str, Any]]:
    """Retrieve a stored idempotency record from Redis.

    Returns None on any error (in-process dict is the fallback).
    """
    r = _get_redis()
    if r is None:
        return None
    try:
        key = f"grantflow:idem:{scope}:{request_id}"
        raw = r.get(key)
        if raw is None:
            return None
        return json.loads(raw)
    except Exception as exc:
        _log.warning("redis_idem_get failed for %s/%s: %s", scope, request_id, exc)
        return None


def redis_idem_set(scope: str, request_id: str, record: dict[str, Any]) -> None:
    """Persist an idempotency record in Redis with a 7-day TTL.

    Errors are swallowed — in-process dict remains the source of truth.
    """
    r = _get_redis()
    if r is None:
        return
    try:
        key = f"grantflow:idem:{scope}:{request_id}"
        r.setex(key, _IDEM_TTL_SECONDS, json.dumps(record, ensure_ascii=True))
    except Exception as exc:
        _log.warning("redis_idem_set failed for %s/%s: %s", scope, request_id, exc)
