"""S3-FIFO cache eviction policy.

10% small FIFO + 90% main FIFO (2-bit Clock) + ghost

- Insert to small FIFO if not in ghost, else insert to main FIFO
- Evict from small FIFO:
    if freq >= move_to_main_threshold: reinsert to main
    else: evict and insert key to ghost
- Evict from main FIFO:
    if freq >= 1: reinsert with freq = min(freq, 3) - 1
    else: evict
"""

from __future__ import annotations

import dataclasses
from collections import OrderedDict
from collections.abc import Hashable, Iterator
from typing import Generic, TypeVar

_V = TypeVar("_V")
_D = TypeVar("_D")

SMALL_SIZE_RATIO = 0.10
GHOST_SIZE_RATIO = 0.90
MOVE_TO_MAIN_THRESHOLD = 2


class _InvalidMaxsizeError(ValueError):
    def __init__(self) -> None:
        super().__init__("maxsize must be >= 1")


class _InvalidSmallSizeRatioError(ValueError):
    def __init__(self) -> None:
        super().__init__("small_size_ratio must be between 0 and 1")


class _InvalidGhostSizeRatioError(ValueError):
    def __init__(self) -> None:
        super().__init__("ghost_size_ratio must be >= 0")


class _InvalidMoveToMainThresholdError(ValueError):
    def __init__(self) -> None:
        super().__init__("move_to_main_threshold must be >= 1")


@dataclasses.dataclass(slots=True)
class _Entry(Generic[_V]):
    value: _V
    freq: int = 0


class S3FIFOCache(Generic[_V]):
    __slots__ = (
        "_ghost",
        "_ghost_cap",
        "_has_evicted",
        "_main",
        "_main_cap",
        "_maxsize",
        "_move_to_main_threshold",
        "_small",
        "_small_cap",
    )

    def __init__(
        self,
        maxsize: int,
        *,
        small_size_ratio: float = SMALL_SIZE_RATIO,
        ghost_size_ratio: float = GHOST_SIZE_RATIO,
        move_to_main_threshold: int = MOVE_TO_MAIN_THRESHOLD,
    ) -> None:
        if maxsize < 1:
            raise _InvalidMaxsizeError()
        if not 0 <= small_size_ratio <= 1:
            raise _InvalidSmallSizeRatioError()
        if ghost_size_ratio < 0:
            raise _InvalidGhostSizeRatioError()
        if move_to_main_threshold < 1:
            raise _InvalidMoveToMainThresholdError()

        self._maxsize = maxsize
        self._small_cap = max(1, int(maxsize * small_size_ratio))
        self._main_cap = maxsize - self._small_cap
        self._ghost_cap = max(0, int(maxsize * ghost_size_ratio))
        self._move_to_main_threshold = move_to_main_threshold

        # If main_cap ends up 0 (maxsize==1), give everything to main
        if self._main_cap <= 0:
            self._small_cap = 0
            self._main_cap = maxsize

        self._small: OrderedDict[Hashable, _Entry[_V]] = OrderedDict()
        self._main: OrderedDict[Hashable, _Entry[_V]] = OrderedDict()
        self._ghost: OrderedDict[Hashable, None] = OrderedDict()
        self._has_evicted = False

    @property
    def maxsize(self) -> int:
        return self._maxsize

    def __len__(self) -> int:
        return len(self._small) + len(self._main)

    def _find_and_touch(self, key: Hashable) -> _Entry[_V] | None:
        entry = self._small.get(key)
        if entry is not None:
            entry.freq += 1
            return entry

        entry = self._main.get(key)
        if entry is not None:
            entry.freq += 1
            return entry

        return None

    def get(self, key: Hashable, default: _D | None = None) -> _V | _D | None:
        if (entry := self._find_and_touch(key)) is None:
            return default
        return entry.value

    def values(self) -> Iterator[_V]:
        for entry in self._small.values():
            yield entry.value
        for entry in self._main.values():
            yield entry.value

    def __contains__(self, key: Hashable) -> bool:
        return key in self._small or key in self._main

    def put(self, key: Hashable, value: _V) -> None:
        if (entry := self._small.get(key)) is not None:
            entry.value = value
            return
        if (entry := self._main.get(key)) is not None:
            entry.value = value
            return

        hit_on_ghost = self._ghost.pop(key, None) is not None

        # Evict before inserting (like the C reference)
        while len(self) >= self._maxsize:
            self._evict()

        entry = _Entry(value=value)
        if hit_on_ghost or self._should_insert_to_main():
            self._main[key] = entry
        elif self._small_cap > 0:
            self._small[key] = entry
        else:
            self._main[key] = entry

    def _should_insert_to_main(self) -> bool:
        return not self._has_evicted and self._small_cap > 0 and len(self._small) >= self._small_cap

    def remove(self, key: Hashable, *, include_ghost: bool = True) -> bool:
        if key in self._small:
            del self._small[key]
            return True
        if key in self._main:
            del self._main[key]
            return True
        if include_ghost and key in self._ghost:
            del self._ghost[key]
            return True
        return False

    def clear(self) -> None:
        self._small.clear()
        self._main.clear()
        self._ghost.clear()
        self._has_evicted = False

    def _evict(self) -> None:
        self._has_evicted = True

        if len(self._main) > self._main_cap or len(self._small) == 0:
            self._evict_main()
        else:
            self._evict_small()

    def _remember_ghost(self, key: Hashable) -> None:
        self._ghost[key] = None
        if self._ghost_cap <= 0:
            self._ghost.clear()
            return
        while len(self._ghost) > self._ghost_cap:
            self._ghost.popitem(last=False)

    def _evict_small(self) -> None:
        while self._small:
            key, entry = self._small.popitem(last=False)
            if entry.freq >= self._move_to_main_threshold:
                self._main[key] = entry
            else:
                self._remember_ghost(key)
                return
        # Small is empty, fall through to main
        self._evict_main()

    def _evict_main(self) -> None:
        while self._main:
            key, entry = self._main.popitem(last=False)
            if entry.freq >= 1:
                entry.freq = min(entry.freq, 3) - 1
                self._main[key] = entry
            else:
                return
