from __future__ import annotations

import asyncio
import hashlib
import json
import os
import time
from collections import OrderedDict
from typing import Any, Dict, Optional

import redis.asyncio as aioredis

from ..settings import API_CACHE_TTL_SEC, REDIS_URL


def _norm_obj(obj: Any) -> Any:
    """Normalize Python objects into a canonical JSON-serializable form."""
    if obj is None:
        return None
    if isinstance(obj, dict):
        return {k: _norm_obj(obj[k]) for k in sorted(obj)}
    if isinstance(obj, list):
        return [_norm_obj(x) for x in obj]
    if isinstance(obj, str):
        return obj.strip()
    return obj


def make_cache_key(prefix: str, payload: Dict[str, Any]) -> str:
    """Create a stable cache key from a payload dict."""
    data = json.dumps(_norm_obj(payload), sort_keys=True, separators=(",", ":"))
    h = hashlib.sha256(data.encode()).hexdigest()
    return f"{prefix}:{h}"


class LruTtlCache:
    def __init__(self, maxsize: int = 512, ttl: int = API_CACHE_TTL_SEC):
        self.maxsize = maxsize
        self.ttl = int(ttl)
        self._data: OrderedDict[str, Any] = OrderedDict()
        self._meta: Dict[str, float] = {}
        self._lock = asyncio.Lock()

    async def get(self, key: str) -> Optional[Any]:
        if self.ttl <= 0:
            return None
        async with self._lock:
            if key in self._data:
                ts = self._meta.get(key, 0)
                if time.time() - ts <= self.ttl:
                    # bump to end (most-recent)
                    self._data.move_to_end(key)
                    return self._data[key]
                else:
                    self._data.pop(key, None)
                    self._meta.pop(key, None)
            return None

    async def set(self, key: str, value: Any):
        if self.ttl <= 0:
            return
        async with self._lock:
            self._data[key] = value
            self._meta[key] = time.time()
            self._data.move_to_end(key)
            while len(self._data) > self.maxsize:
                k, _ = self._data.popitem(last=False)
                self._meta.pop(k, None)


class HybridCache:
    """In-proc LRU + optional Redis JSON cache."""

    def __init__(
        self, namespace: str = "papi", lru_size: int = 512, ttl: int = API_CACHE_TTL_SEC
    ):
        self.ns = namespace
        self.lru = LruTtlCache(maxsize=lru_size, ttl=ttl)
        url = os.getenv("REDIS_URL", REDIS_URL or "")
        self.redis: Optional[aioredis.Redis]
        try:
            self.redis = aioredis.from_url(url, decode_responses=True) if url else None
        except Exception:
            self.redis = None
        self.ttl = int(ttl)

    async def get(self, key: str) -> Optional[Any]:
        if self.ttl <= 0:
            return None
        # local
        v = await self.lru.get(key)
        if v is not None:
            return v
        # redis
        if self.redis:
            try:
                raw = await self.redis.get(key)
                if raw:
                    data = json.loads(raw)
                    # also populate LRU
                    await self.lru.set(key, data)
                    return data
            except Exception:
                pass
        return None

    async def set(self, key: str, value: Any):
        if self.ttl <= 0:
            return
        await self.lru.set(key, value)
        if self.redis:
            try:
                await self.redis.set(key, json.dumps(value), ex=self.ttl)
            except Exception:
                pass
