"""Cache backend abstraction (Phase 2 extension) with Redis PoC.

The in-memory cache remains authoritative for adaptive eviction heuristics.
This backend layer provides optional read-through / write-through to Redis
for latency & CPU comparison. It is intentionally minimal:

Environment:
  THEME_PREVIEW_REDIS_URL=redis://host:port/db  -> enable PoC if redis-py importable
  THEME_PREVIEW_REDIS_DISABLE=1                 -> hard disable even if URL present

Behavior:
  - On store: serialize payload + metadata into JSON and SETEX with TTL.
  - On get (memory miss only): attempt Redis GET and rehydrate (respect TTL).
  - Failures are swallowed; metrics track attempts/hits/errors.

No eviction coordination is attempted; Redis TTL handles expiry. The goal is
purely observational at this stage.
"""
from __future__ import annotations

from typing import Optional, Dict, Any, Tuple
import json
import os
import time

try:  # lazy optional dependency
    import redis  # type: ignore
except Exception:  # pragma: no cover - absence path
    redis = None  # type: ignore

_URL = os.getenv("THEME_PREVIEW_REDIS_URL")
_DISABLED = (os.getenv("THEME_PREVIEW_REDIS_DISABLE") or "").lower() in {"1","true","yes","on"}

_CLIENT = None
_INIT_ERR: str | None = None

def _init() -> None:
    global _CLIENT, _INIT_ERR
    if _CLIENT is not None or _INIT_ERR is not None:
        return
    if _DISABLED or not _URL or not redis:
        _INIT_ERR = "disabled_or_missing"
        return
    try:
        _CLIENT = redis.Redis.from_url(_URL, socket_timeout=0.25)  # type: ignore
        # lightweight ping (non-fatal)
        try:
            _CLIENT.ping()
        except Exception:
            pass
    except Exception as e:  # pragma: no cover - network/dep issues
        _INIT_ERR = f"init_error:{e}"[:120]


def backend_info() -> Dict[str, Any]:
    return {
        "enabled": bool(_CLIENT),
        "init_error": _INIT_ERR,
        "url_present": bool(_URL),
    }

def _serialize(key: Tuple[str, int, str | None, str | None, str], payload: Dict[str, Any], build_cost_ms: float) -> str:
    return json.dumps({
        "k": list(key),
        "p": payload,
        "bc": build_cost_ms,
        "ts": time.time(),
    }, separators=(",", ":"))

def redis_store(key: Tuple[str, int, str | None, str | None, str], payload: Dict[str, Any], ttl_seconds: int, build_cost_ms: float) -> bool:
    _init()
    if not _CLIENT:
        return False
    try:
        data = _serialize(key, payload, build_cost_ms)
        # Compose a simple namespaced key; join tuple parts with '|'
        skey = "tpv:" + "|".join([str(part) for part in key])
        _CLIENT.setex(skey, ttl_seconds, data)
        return True
    except Exception:  # pragma: no cover
        return False

def redis_get(key: Tuple[str, int, str | None, str | None, str]) -> Optional[Dict[str, Any]]:
    _init()
    if not _CLIENT:
        return None
    try:
        skey = "tpv:" + "|".join([str(part) for part in key])
        raw: bytes | None = _CLIENT.get(skey)  # type: ignore
        if not raw:
            return None
        obj = json.loads(raw.decode("utf-8"))
        # Expect shape from _serialize
        payload = obj.get("p")
        if not isinstance(payload, dict):
            return None
        return {
            "payload": payload,
            "_cached_at": float(obj.get("ts") or 0),
            "cached_at": float(obj.get("ts") or 0),
            "inserted_at": float(obj.get("ts") or 0),
            "last_access": float(obj.get("ts") or 0),
            "hit_count": 0,
            "build_cost_ms": float(obj.get("bc") or 0.0),
        }
    except Exception:  # pragma: no cover
        return None

__all__ = [
    "backend_info",
    "redis_store",
    "redis_get",
]