"""Abstract base class for relay cache backends.

All cache backends must subclass :class:`CacheBackend` and implement every
abstract method. All methods are async to keep the interface compatible with
both I/O-bound (SQLite via aiosqlite, Redis) and in-process implementations.
"""

from __future__ import annotations

from abc import ABC, abstractmethod


class CacheBackend(ABC):
    """Abstract interface for a relay cache backend.

    Implementors must provide async implementations of all methods below.
    The contract for each method is described in its docstring.

    All keys are opaque strings; in practice they are SHA-256 hex digests
    produced by :func:`relay.utils.hashing.compute_cache_key`.
    """

    @abstractmethod
    async def get(self, cache_key: str) -> dict | None:
        """Return a cached response dict, or ``None`` on a miss.

        A successful ``get`` should also update the ``last_hit_at`` timestamp
        and increment the ``hit_count`` for the entry so that LRU eviction
        has accurate recency information.

        If the entry exists but has passed its TTL, it must be treated as a
        miss: the entry should be deleted and ``None`` returned.

        Args:
            cache_key: Opaque cache key string.

        Returns:
            The stored response dict on a hit, or ``None`` on a miss or TTL
            expiry.

        Raises:
            relay.exceptions.CacheError: If the underlying storage raises an
                unexpected error.
        """

    @abstractmethod
    async def put(
        self,
        cache_key: str,
        provider: str,
        model: str,
        response: dict,
        input_tokens: int,
        output_tokens: int,
        ttl: int | None = None,
    ) -> None:
        """Store a response in the cache.

        If an entry with the same *cache_key* already exists it must be
        replaced atomically.

        Args:
            cache_key: Opaque cache key string.
            provider: Provider identifier (e.g. ``"anthropic"``).
            model: Model identifier (e.g. ``"claude-opus-4-5"``).
            response: The raw response dict returned by the provider adapter.
                The backend is responsible for any serialization and
                compression.
            input_tokens: Number of input tokens consumed by this request.
            output_tokens: Number of output tokens generated by this request.
            ttl: Optional time-to-live in seconds. ``None`` means the entry
                never expires.

        Raises:
            relay.exceptions.CacheError: If the write fails.
        """

    @abstractmethod
    async def invalidate(self, cache_key: str) -> None:
        """Remove a single entry from the cache.

        A no-op if the key does not exist.

        Args:
            cache_key: Opaque cache key string.

        Raises:
            relay.exceptions.CacheError: If the deletion fails.
        """

    @abstractmethod
    async def invalidate_job(self, job_id: str) -> None:
        """Remove all cache entries that belong to a specific job.

        Implementations that do not store job-level associations may treat
        this as a no-op, but they should document that clearly.

        Args:
            job_id: The relay job identifier whose cached entries should be
                purged.

        Raises:
            relay.exceptions.CacheError: If the deletion fails.
        """

    @abstractmethod
    async def stats(self) -> dict:
        """Return a snapshot of cache statistics.

        The returned dict must contain at minimum the following keys:

        * ``"size_bytes"`` (int) — total storage used by cached entries.
        * ``"entry_count"`` (int) — number of entries currently in the cache.
        * ``"hit_rate"`` (float) — fraction of ``get`` calls that returned a
          non-``None`` result since the backend was last created or reset.
          Value is in the range ``[0.0, 1.0]``.

        Backends may include additional keys (e.g. ``"eviction_count"``,
        ``"compression_ratio"``).

        Returns:
            Dict containing cache statistics.

        Raises:
            relay.exceptions.CacheError: If the query fails.
        """

    @abstractmethod
    async def vacuum(self) -> None:
        """Expire TTL-exceeded entries and perform LRU eviction if needed.

        This method should:

        1. Delete all entries whose ``expires_at`` timestamp is in the past.
        2. If the backend has a configured maximum size, evict the
           least-recently-used entries until the cache is within bounds.

        It is safe to call ``vacuum`` concurrently with reads and writes;
        implementations must use appropriate locking or transactions.

        Raises:
            relay.exceptions.CacheError: If the maintenance operation fails.
        """
