from __future__ import annotations

import threading
import time
from collections import OrderedDict
from dataclasses import dataclass
from typing import Generic, TypeVar

K = TypeVar("K")
V = TypeVar("V")


@dataclass(frozen=True, slots=True)
class CacheStats:
    size: int
    hits: int
    misses: int
    expired: int
    evicted: int


class TTLCache(Generic[K, V]):
    def __init__(self, *, maxsize: int = 1024, default_ttl: float | None = None, cleanup_interval: float = 30.0, start_cleanup_thread: bool = True) -> None:
        if not isinstance(maxsize, int) or maxsize <= 0:
            raise ValueError("maxsize must be a positive integer")
        if default_ttl is not None and default_ttl <= 0:
            raise ValueError("default_ttl must be > 0 when provided")
        if cleanup_interval <= 0:
            raise ValueError("cleanup_interval must be > 0")
        self.maxsize = maxsize
        self.default_ttl = default_ttl
        self.cleanup_interval = cleanup_interval
        self._data: OrderedDict[K, tuple[V, float | None]] = OrderedDict()
        self._lock = threading.RLock()
        self._stop = threading.Event()
        self._hits = 0
        self._misses = 0
        self._expired = 0
        self._evicted = 0
        self._cleanup_thread: threading.Thread | None = None
        if start_cleanup_thread:
            self._cleanup_thread = threading.Thread(target=self._cleanup_loop, name="TTLCacheCleanup", daemon=True)
            self._cleanup_thread.start()

    def _now(self) -> float:
        return time.monotonic()

    def _expires_at(self, ttl: float | None) -> float | None:
        if ttl is None:
            ttl = self.default_ttl
        if ttl is None:
            return None
        if ttl <= 0:
            raise ValueError("ttl must be > 0")
        return self._now() + ttl

    def _is_expired(self, expires_at: float | None) -> bool:
        return expires_at is not None and self._now() >= expires_at

    def _purge_expired_locked(self) -> None:
        expired_keys = [k for k, (_, exp) in self._data.items() if self._is_expired(exp)]
        for key in expired_keys:
            self._data.pop(key, None)
            self._expired += 1

    def _evict_if_needed_locked(self) -> None:
        while len(self._data) > self.maxsize:
            self._data.popitem(last=False)
            self._evicted += 1

    def set(self, key: K, value: V, *, ttl: float | None = None) -> None:
        with self._lock:
            expires_at = self._expires_at(ttl)
            if key in self._data:
                self._data.pop(key, None)
            self._data[key] = (value, expires_at)
            self._data.move_to_end(key)
            self._purge_expired_locked()
            self._evict_if_needed_locked()

    def get(self, key: K, default: V | None = None) -> V | None:
        with self._lock:
            item = self._data.get(key)
            if item is None:
                self._misses += 1
                return default
            value, expires_at = item
            if self._is_expired(expires_at):
                self._data.pop(key, None)
                self._expired += 1
                self._misses += 1
                return default
            self._data.move_to_end(key)
            self._hits += 1
            return value

    def pop(self, key: K, default: V | None = None) -> V | None:
        with self._lock:
            item = self._data.pop(key, None)
            if item is None:
                return default
            value, expires_at = item
            if self._is_expired(expires_at):
                self._expired += 1
                return default
            return value

    def delete(self, key: K) -> bool:
        with self._lock:
            return self._data.pop(key, None) is not None

    def touch(self, key: K, *, ttl: float | None = None) -> bool:
        with self._lock:
            item = self._data.get(key)
            if item is None:
                return False
            value, expires_at = item
            if self._is_expired(expires_at):
                self._data.pop(key, None)
                self._expired += 1
                return False
            self._data[key] = (value, self._expires_at(ttl))
            self._data.move_to_end(key)
            return True

    def cleanup(self) -> int:
        with self._lock:
            before = len(self._data)
            self._purge_expired_locked()
            return before - len(self._data)

    def items(self) -> list[tuple[K, V]]:
        with self._lock:
            self._purge_expired_locked()
            return [(k, v) for k, (v, _) in self._data.items()]

    def stats(self) -> CacheStats:
        with self._lock:
            return CacheStats(size=len(self._data), hits=self._hits, misses=self._misses, expired=self._expired, evicted=self._evicted)

    def __len__(self) -> int:
        with self._lock:
            self._purge_expired_locked()
            return len(self._data)

    def close(self) -> None:
        self._stop.set()
        if self._cleanup_thread is not None:
            self._cleanup_thread.join(timeout=2.0)

    def _cleanup_loop(self) -> None:
        while not self._stop.wait(self.cleanup_interval):
            self.cleanup()