"""
Redis-backed result cache.

Crop + district + month combinations repeat heavily in real usage —
many farmers in the same district asking about the same crop in the
same month. Caching the full analysis result avoids re-running 6 models
and an LLM call for an answer that hasn't changed.

Cache invalidation strategy: time-based TTL (REDIS_CACHE_TTL_SECONDS,
default 24h) rather than manual invalidation. This is intentional —
climate/market forecasts are inherently "as of today" data, so letting
them expire naturally is simpler and safer than trying to track every
upstream data change that should invalidate a cache key.

If Redis is down, every method here fails open (returns None / does
nothing) rather than raising — caching is a performance optimization,
not a correctness requirement, and a cache outage must never become a
full service outage.
"""

import hashlib
import json

from redis.asyncio import Redis

from app.core.config import get_settings
from app.core.logging_config import get_logger

settings = get_settings()
logger = get_logger(__name__)


def build_cache_key(district: str, crop: str, month: int, model_version: str) -> str:
    """
    Deterministic cache key. model_version is included so that retraining
    and deploying new models automatically invalidates old cached results —
    no manual cache flush needed on model updates.
    """
    raw = f"{district}:{crop}:{month}:{model_version}"
    digest = hashlib.sha256(raw.encode("utf-8")).hexdigest()[:16]
    return f"analysis:{digest}"


class CacheService:
    def __init__(self) -> None:
        self._redis: Redis | None = None

    async def init(self) -> None:
        try:
            self._redis = Redis.from_url(
                settings.REDIS_URL, decode_responses=True, socket_connect_timeout=3
            )
            await self._redis.ping()
            logger.info("Redis cache connection established")
        except Exception as exc:
            logger.error(f"Could not connect to Redis at startup, caching disabled: {exc}")
            self._redis = None

    async def get(self, key: str) -> dict | None:
        if self._redis is None:
            return None
        try:
            raw = await self._redis.get(key)
            if raw is None:
                return None
            return json.loads(raw)
        except Exception as exc:
            logger.warning(f"Cache read failed for key={key}, treating as cache miss: {exc}")
            return None

    async def set(self, key: str, value: dict, ttl_seconds: int | None = None) -> None:
        if self._redis is None:
            return
        try:
            ttl = ttl_seconds if ttl_seconds is not None else settings.REDIS_CACHE_TTL_SECONDS
            await self._redis.set(key, json.dumps(value), ex=ttl)
        except Exception as exc:
            logger.warning(f"Cache write failed for key={key}, continuing without cache: {exc}")

    async def health_check(self) -> bool:
        if self._redis is None:
            return False
        try:
            await self._redis.ping()
            return True
        except Exception:
            return False

    @property
    def raw_client(self) -> Redis | None:
        """Exposed for the rate limiter, which needs the same connection pool."""
        return self._redis


# Single shared instance — initialized once during app startup
cache_service = CacheService()
