"""Synchronous S3-FIFO cache decorator, modeled after functools.lru_cache."""

from __future__ import annotations

from collections.abc import Callable
from functools import _CacheInfo, _make_key, update_wrapper
from threading import RLock
from typing import Any, TypeVar, cast, overload

from s3fifo._s3fifo import GHOST_SIZE_RATIO, MOVE_TO_MAIN_THRESHOLD, SMALL_SIZE_RATIO, S3FIFOCache

_R = TypeVar("_R")
_SENTINEL = object()


class _InvalidDecoratorArgumentError(TypeError):
    def __init__(self) -> None:
        super().__init__("Expected first argument to be an integer, a callable, or None")


def _cache_parameters(
    *,
    maxsize: int,
    typed: bool,
    small_size_ratio: float,
    ghost_size_ratio: float,
    move_to_main_threshold: int,
) -> dict[str, Any]:
    return {
        "maxsize": maxsize,
        "typed": typed,
        "small_size_ratio": small_size_ratio,
        "ghost_size_ratio": ghost_size_ratio,
        "move_to_main_threshold": move_to_main_threshold,
    }


def _s3fifo_cache_wrapper(
    user_function: Callable[..., _R],
    maxsize: int,
    typed: bool,
    *,
    small_size_ratio: float,
    ghost_size_ratio: float,
    move_to_main_threshold: int,
) -> Callable[..., _R]:
    cache = S3FIFOCache[_R](
        maxsize,
        small_size_ratio=small_size_ratio,
        ghost_size_ratio=ghost_size_ratio,
        move_to_main_threshold=move_to_main_threshold,
    )
    hits = misses = 0
    lock = RLock()

    def wrapper(*args: Any, **kwds: Any) -> _R:
        nonlocal hits, misses
        key = _make_key(args, kwds, typed)
        with lock:
            result = cache.get(key, _SENTINEL)
            if result is not _SENTINEL:
                hits += 1
                return cast(_R, result)
        result = user_function(*args, **kwds)
        with lock:
            cached_result = cache.get(key, _SENTINEL)
            if cached_result is not _SENTINEL:
                hits += 1
                return cast(_R, cached_result)
            misses += 1
            cache.put(key, result)
        return result

    def cache_info() -> _CacheInfo:
        with lock:
            return _CacheInfo(hits, misses, maxsize, len(cache))

    def cache_clear() -> None:
        nonlocal hits, misses
        with lock:
            cache.clear()
            hits = misses = 0

    def cache_parameters() -> dict[str, Any]:
        return _cache_parameters(
            maxsize=maxsize,
            typed=typed,
            small_size_ratio=small_size_ratio,
            ghost_size_ratio=ghost_size_ratio,
            move_to_main_threshold=move_to_main_threshold,
        )

    wrapper.cache_info = cache_info  # type: ignore[attr-defined]
    wrapper.cache_clear = cache_clear  # type: ignore[attr-defined]
    wrapper.cache_parameters = cache_parameters  # type: ignore[attr-defined]
    return wrapper


@overload
def s3fifo_cache(
    maxsize: int = 128,
    typed: bool = False,
    *,
    small_size_ratio: float = SMALL_SIZE_RATIO,
    ghost_size_ratio: float = GHOST_SIZE_RATIO,
    move_to_main_threshold: int = MOVE_TO_MAIN_THRESHOLD,
) -> Callable[[Callable[..., _R]], Callable[..., _R]]: ...


@overload
def s3fifo_cache(
    maxsize: Callable[..., _R],
    /,
    typed: bool = False,
    *,
    small_size_ratio: float = SMALL_SIZE_RATIO,
    ghost_size_ratio: float = GHOST_SIZE_RATIO,
    move_to_main_threshold: int = MOVE_TO_MAIN_THRESHOLD,
) -> Callable[..., _R]: ...


def s3fifo_cache(
    maxsize: int | Callable[..., _R] = 128,
    typed: bool = False,
    *,
    small_size_ratio: float = SMALL_SIZE_RATIO,
    ghost_size_ratio: float = GHOST_SIZE_RATIO,
    move_to_main_threshold: int = MOVE_TO_MAIN_THRESHOLD,
) -> Callable[[Callable[..., _R]], Callable[..., _R]] | Callable[..., _R]:
    """S3-FIFO cache decorator for synchronous functions.

    Drop-in replacement for functools.lru_cache using the S3-FIFO eviction
    algorithm instead of LRU.

    Usage::

        @s3fifo_cache
        def func(x):
            ...

        @s3fifo_cache(maxsize=256)
        def func(x):
            ...
    """
    if isinstance(maxsize, int):
        if maxsize < 0:
            maxsize = 0

        def decorating_function(user_function: Callable[..., _R]) -> Callable[..., _R]:
            if maxsize == 0:
                # No caching, just count misses
                miss_count = 0

                def wrapper(*args: Any, **kwds: Any) -> _R:
                    nonlocal miss_count
                    miss_count += 1
                    return user_function(*args, **kwds)

                def cache_info() -> _CacheInfo:
                    return _CacheInfo(0, miss_count, maxsize, 0)

                def cache_clear() -> None:
                    nonlocal miss_count
                    miss_count = 0

                def cache_parameters() -> dict[str, Any]:
                    return _cache_parameters(
                        maxsize=maxsize,
                        typed=typed,
                        small_size_ratio=small_size_ratio,
                        ghost_size_ratio=ghost_size_ratio,
                        move_to_main_threshold=move_to_main_threshold,
                    )

                wrapper.cache_info = cache_info  # type: ignore[attr-defined]
                wrapper.cache_clear = cache_clear  # type: ignore[attr-defined]
                wrapper.cache_parameters = cache_parameters  # type: ignore[attr-defined]
                return update_wrapper(wrapper, user_function)

            w = _s3fifo_cache_wrapper(
                user_function,
                maxsize,
                typed,
                small_size_ratio=small_size_ratio,
                ghost_size_ratio=ghost_size_ratio,
                move_to_main_threshold=move_to_main_threshold,
            )
            w.cache_parameters = lambda: _cache_parameters(  # type: ignore[attr-defined,assignment]
                maxsize=maxsize,
                typed=typed,
                small_size_ratio=small_size_ratio,
                ghost_size_ratio=ghost_size_ratio,
                move_to_main_threshold=move_to_main_threshold,
            )
            return update_wrapper(w, user_function)

        return decorating_function
    elif callable(maxsize):
        user_function = maxsize
        default_maxsize = 128
        wrapper = _s3fifo_cache_wrapper(
            user_function,
            default_maxsize,
            typed,
            small_size_ratio=small_size_ratio,
            ghost_size_ratio=ghost_size_ratio,
            move_to_main_threshold=move_to_main_threshold,
        )
        wrapper.cache_parameters = lambda: _cache_parameters(  # type: ignore[attr-defined,assignment]
            maxsize=default_maxsize,
            typed=typed,
            small_size_ratio=small_size_ratio,
            ghost_size_ratio=ghost_size_ratio,
            move_to_main_threshold=move_to_main_threshold,
        )
        return update_wrapper(wrapper, user_function)
    else:
        raise _InvalidDecoratorArgumentError()
