"""Dual-budget LRU cache (max items + max bytes).

OrderedDict-based: move-to-end on get, evict oldest on overflow.
Custom ``sizeof`` callable for accurate byte accounting.

See ADR 019, item 9.
"""

from __future__ import annotations

from collections import OrderedDict
from typing import Callable, Generic, TypeVar

V = TypeVar("V")


class LRUCache(Generic[V]):
    """LRU cache with item-count and byte-size budgets.

    Args:
        max_items: Maximum number of entries.
        max_bytes: Maximum total byte size (0 = unlimited).
        sizeof: Callable that returns the byte size of a value.
            Defaults to ``len(v)`` for bytes/bytearray, 0 otherwise.
    """

    def __init__(
        self,
        *,
        max_items: int = 1024,
        max_bytes: int = 0,
        sizeof: Callable[[V], int] | None = None,
    ) -> None:
        self._data: OrderedDict[str, V] = OrderedDict()
        self._max_items = max_items
        self._max_bytes = max_bytes
        self._sizeof = sizeof or _default_sizeof
        self._total_bytes: int = 0

    # -- public API --------------------------------------------------------

    def get(self, key: str) -> V | None:
        """Retrieve a value, promoting it to most-recent."""
        if key not in self._data:
            return None
        self._data.move_to_end(key)
        return self._data[key]

    def put(self, key: str, value: V) -> None:
        """Insert or overwrite a value, evicting as needed."""
        if key in self._data:
            self._total_bytes -= self._sizeof(self._data[key])
            del self._data[key]

        entry_bytes = self._sizeof(value)
        self._data[key] = value
        self._total_bytes += entry_bytes

        self._evict()

    def __contains__(self, key: str) -> bool:
        return key in self._data

    def clear(self) -> None:
        self._data.clear()
        self._total_bytes = 0

    @property
    def stats(self) -> dict[str, int]:
        return {
            "items": len(self._data),
            "bytes": self._total_bytes,
            "max_items": self._max_items,
            "max_bytes": self._max_bytes,
        }

    # -- internals ---------------------------------------------------------

    def _evict(self) -> None:
        while len(self._data) > self._max_items:
            self._pop_oldest()
        if self._max_bytes > 0:
            while self._total_bytes > self._max_bytes and self._data:
                self._pop_oldest()

    def _pop_oldest(self) -> None:
        key, value = self._data.popitem(last=False)
        self._total_bytes -= self._sizeof(value)


def _default_sizeof(v: object) -> int:
    if isinstance(v, (bytes, bytearray)):
        return len(v)
    return 0
