"""
TieredCache: Combines multiple backends in a layered hierarchy.

L1 (fastest) → L2 → L3 (slowest / most persistent)

Read strategy: check each layer top-down, backfill on miss.
Write strategy: write-through (all layers) or write-back (L1 only).
"""

from __future__ import annotations

import asyncio
from enum import Enum
from typing import Any, Dict, List, Optional

from cachestack.base import BaseCache, CacheError


class WriteStrategy(str, Enum):
    """Write strategy for TieredCache."""
    WRITE_THROUGH = "write_through"  # Write to all layers immediately
    WRITE_BACK = "write_back"        # Write to L1 only; propagate lazily


class TieredCache(BaseCache):
    """
    Multi-layer cache with automatic read-through and backfill.

    On get:
        Checks L1 → L2 → L3. On a miss at L1 but hit at L2,
        backfills L1 with the found value.

    On set:
        WRITE_THROUGH: writes to all layers.
        WRITE_BACK: writes to L1 only.

    Stampede protection:
        Uses per-key asyncio locks to prevent multiple coroutines
        from concurrently fetching the same cold key.

    Args:
        backends: Ordered list of BaseCache instances, L1 first.
        write_strategy: WriteStrategy enum value.
        silent: If True, suppress errors and return None.

    Example:
        cache = TieredCache([
            MemoryBackend(),
            RedisBackend(RedisConfig(dsn="redis://localhost")),
            PostgresBackend(PostgresConfig(dsn="postgresql://localhost/db")),
        ])
    """

    def __init__(
        self,
        backends: List[BaseCache],
        write_strategy: WriteStrategy = WriteStrategy.WRITE_THROUGH,
        silent: bool = False,
    ) -> None:
        if not backends:
            raise ValueError("TieredCache requires at least one backend.")
        self._backends = backends
        self._write_strategy = write_strategy
        self._silent = silent
        self._locks: Dict[str, asyncio.Lock] = {}
        self._global_lock = asyncio.Lock()
        self._hits = 0
        self._misses = 0
        self._errors = 0

    async def _get_lock(self, key: str) -> asyncio.Lock:
        """Get or create a per-key lock for stampede protection."""
        async with self._global_lock:
            if key not in self._locks:
                self._locks[key] = asyncio.Lock()
            return self._locks[key]

    async def get(self, key: str) -> Optional[Any]:
        """
        Retrieve value from the first layer that has it.
        Backfills higher (faster) layers on a miss.
        Uses per-key locking to prevent cache stampedes.
        """
        lock = await self._get_lock(key)
        async with lock:
            hit_layer = -1
            value = None

            for i, backend in enumerate(self._backends):
                try:
                    value = await backend.get(key)
                    if value is not None:
                        hit_layer = i
                        break
                except Exception as e:
                    self._errors += 1
                    if not self._silent:
                        raise CacheError(f"TieredCache get failed at layer {i}: {e}") from e

            if hit_layer == -1:
                self._misses += 1
                return None

            # Backfill faster layers that missed
            for i in range(hit_layer):
                try:
                    await self._backends[i].set(key, value)
                except Exception:
                    pass  # Backfill is best-effort

            self._hits += 1
            return value

    async def set(self, key: str, value: Any, ttl: Optional[int] = None) -> None:
        """
        Store a value according to the write strategy.

        WRITE_THROUGH: all layers.
        WRITE_BACK: L1 only.
        """
        if self._write_strategy == WriteStrategy.WRITE_BACK:
            targets = [self._backends[0]]
        else:
            targets = self._backends

        for i, backend in enumerate(targets):
            try:
                await backend.set(key, value, ttl)
            except Exception as e:
                self._errors += 1
                if not self._silent:
                    raise CacheError(f"TieredCache set failed at layer {i}: {e}") from e

    async def delete(self, key: str) -> None:
        """Delete a key from all layers."""
        for i, backend in enumerate(self._backends):
            try:
                await backend.delete(key)
            except Exception as e:
                self._errors += 1
                if not self._silent:
                    raise CacheError(f"TieredCache delete failed at layer {i}: {e}") from e
        # Clean up lock
        async with self._global_lock:
            self._locks.pop(key, None)

    async def exists(self, key: str) -> bool:
        """Check if a key exists in any layer."""
        for backend in self._backends:
            try:
                if await backend.exists(key):
                    return True
            except Exception:
                pass
        return False

    async def clear(self) -> None:
        """Clear all layers."""
        for i, backend in enumerate(self._backends):
            try:
                await backend.clear()
            except Exception as e:
                self._errors += 1
                if not self._silent:
                    raise CacheError(f"TieredCache clear failed at layer {i}: {e}") from e
        async with self._global_lock:
            self._locks.clear()

    async def stats(self) -> Dict[str, Any]:
        """Return per-layer stats plus aggregate TieredCache stats."""
        layer_stats = []
        for i, backend in enumerate(self._backends):
            try:
                s = await backend.stats()
                s["layer"] = i
                layer_stats.append(s)
            except Exception:
                layer_stats.append({"layer": i, "error": "stats unavailable"})

        return {
            "backend": "tiered",
            "write_strategy": self._write_strategy.value,
            "layers": len(self._backends),
            "hits": self._hits,
            "misses": self._misses,
            "errors": self._errors,
            "layer_stats": layer_stats,
        }
