"""Simple cache wrapper with Redis primary and in-memory dict fallback.

Tries Redis first. Falls back to an in-memory dict with TTL if Redis
is unavailable or disabled.  Uses JSON serialization for values.
"""

from __future__ import annotations

import json
import threading
import time
from typing import Any

from packages.core.common.logging import get_logger
from packages.core.common.settings import get_settings

logger = get_logger(__name__)

# ---------------------------------------------------------------------------
# In-memory fallback backend
# ---------------------------------------------------------------------------

_memory_store: dict[str, tuple[float | None, str]] = (
    {}
)  # key -> (expires_at, json_value)
_lock = threading.Lock()


def _memory_get(key: str) -> Any | None:
    """Get a value from the in-memory store."""
    with _lock:
        entry = _memory_store.get(key)
        if entry is None:
            return None
        expires_at, json_value = entry
        if expires_at is not None and time.monotonic() > expires_at:
            del _memory_store[key]
            return None
        try:
            return json.loads(json_value)
        except json.JSONDecodeError:
            return None


def _memory_set(key: str, value: Any, ttl: int | None = None) -> None:
    """Set a value in the in-memory store with optional TTL (seconds)."""
    expires_at = (time.monotonic() + ttl) if ttl is not None else None
    with _lock:
        _memory_store[key] = (expires_at, json.dumps(value, default=str))


def _memory_delete(key: str) -> None:
    """Delete a key from the in-memory store."""
    with _lock:
        _memory_store.pop(key, None)


def _memory_clear() -> None:
    """Clear all entries from the in-memory store."""
    with _lock:
        _memory_store.clear()


# ---------------------------------------------------------------------------
# Redis backend (lazy init)
# ---------------------------------------------------------------------------

_redis_client: Any = None
_redis_available: bool | None = None


def _get_redis_client() -> Any | None:
    """Return the global Redis client, or None if unavailable/disabled."""
    global _redis_client, _redis_available

    if _redis_available is False:
        return None

    if _redis_client is not None:
        return _redis_client

    settings = get_settings()
    if not settings.redis.enabled:
        logger.info("Redis caching is disabled via settings, using in-memory fallback")
        _redis_available = False
        return None

    try:
        import redis as _redis  # type: ignore[import-untyped]

        _redis_client = _redis.from_url(
            settings.redis.url,
            decode_responses=True,
            socket_connect_timeout=2,
            socket_timeout=2,
        )
        _redis_client.ping()
        logger.info(
            "Redis cache backend active",
            extra={"url": settings.redis.url},
        )
        _redis_available = True
        return _redis_client
    except Exception as exc:
        logger.warning(
            "Redis unavailable, falling back to in-memory cache",
            extra={"error": str(exc)},
        )
        _redis_available = False
        return None


# ---------------------------------------------------------------------------
# Public API
# ---------------------------------------------------------------------------

_backend: str | None = None


def get_cache() -> str:
    """Return the name of the active cache backend ('redis' or 'memory')."""
    global _backend
    if _backend is None:
        if _get_redis_client() is not None:
            _backend = "redis"
        else:
            _backend = "memory"
        logger.info("Cache backend resolved", extra={"backend": _backend})
    return _backend


def cache_get(key: str) -> Any | None:
    """Retrieve a value from cache.

    Args:
        key: Cache key.

    Returns:
        Deserialized value or None if missing / expired.
    """
    client = _get_redis_client()
    if client is not None:
        try:
            raw = client.get(key)
            if raw is None:
                return None
            return json.loads(raw)
        except Exception as exc:
            logger.warning(
                "Redis cache_get failed, falling back", extra={"error": str(exc)}
            )
            return _memory_get(key)

    return _memory_get(key)


def cache_set(key: str, value: Any, ttl: int | None = None) -> None:
    """Store a value in cache.

    Args:
        key: Cache key.
        value: Value to store (must be JSON-serializable).
        ttl: Time-to-live in seconds.  Uses ``settings.redis.ttl_seconds``
             when *ttl* is ``None`` and Redis is the active backend.
    """
    if ttl is None:
        ttl = get_settings().redis.ttl_seconds

    client = _get_redis_client()
    if client is not None:
        try:
            client.setex(key, ttl, json.dumps(value, default=str))
            return
        except Exception as exc:
            logger.warning(
                "Redis cache_set failed, falling back", extra={"error": str(exc)}
            )

    _memory_set(key, value, ttl)


def cache_delete(key: str) -> None:
    """Remove a single key from cache."""
    client = _get_redis_client()
    if client is not None:
        try:
            client.delete(key)
            return
        except Exception:
            pass
    _memory_delete(key)


def cache_clear() -> None:
    """Flush the entire cache (both backends if available)."""
    client = _get_redis_client()
    if client is not None:
        try:
            client.flushdb()
        except Exception:
            pass
    _memory_clear()
