from __future__ import annotations

import json
import re
import threading
import time
from collections import OrderedDict
from pathlib import Path
from typing import Any
from typing import Generic, TypeVar

T = TypeVar("T")


class TTLCache(Generic[T]):
    def __init__(self, ttl_seconds: int, max_entries: int) -> None:
        self.ttl_seconds = ttl_seconds
        self.max_entries = max_entries
        self._store: OrderedDict[str, list[Any]] = OrderedDict()
        self._lock = threading.Lock()

    def get(self, key: str) -> T | None:
        now = time.time()
        with self._lock:
            item = self._store.get(key)
            if item is None:
                return None

            _created_at, expires_at, hit_count, value = item
            if expires_at <= now:
                self._store.pop(key, None)
                return None

            item[2] = hit_count + 1

            return value

    def set(self, key: str, value: T) -> None:
        created_at = time.time()
        expires_at = created_at + self.ttl_seconds
        with self._lock:
            if key in self._store:
                self._store.pop(key, None)

            self._store[key] = [created_at, expires_at, 0, value]
            self._evict_if_needed()

    def snapshot(self) -> list[dict[str, Any]]:
        now = time.time()
        result: list[dict[str, Any]] = []
        with self._lock:
            for key, (created_at, expires_at, hit_count, _value) in self._store.items():
                if expires_at > now:
                    result.append(
                        {
                            "key": key,
                            "created_at": created_at,
                            "expires_at": expires_at,
                            "hit_count": hit_count,
                        }
                    )
        return result

    def _evict_if_needed(self) -> None:
        while len(self._store) > self.max_entries:
            self._store.popitem(last=False)


class FileCache:
    def __init__(self, base_dir: Path, ttl_seconds: int = 0) -> None:
        self.base_dir = base_dir
        self.ttl_seconds = ttl_seconds
        self._lock = threading.Lock()
        self.base_dir.mkdir(parents=True, exist_ok=True)

    def _key_to_path(self, key: str) -> Path:
        safe_key = re.sub(r"[^a-zA-Z0-9_\-]", "_", key)
        return self.base_dir / f"{safe_key}.json"

    def get(self, key: str) -> dict[str, Any] | None:
        path = self._key_to_path(key)
        if not path.exists():
            return None

        try:
            raw = path.read_text(encoding="utf-8")
            entry = json.loads(raw)
        except (OSError, json.JSONDecodeError):
            return None

        if self.ttl_seconds > 0:
            created_at = entry.get("_created_at", 0)
            if time.time() - created_at > self.ttl_seconds:
                try:
                    path.unlink(missing_ok=True)
                except OSError:
                    pass
                return None

        data = entry.get("data")
        if isinstance(data, dict):
            return data
        return None

    def set(self, key: str, value: dict[str, Any]) -> None:
        path = self._key_to_path(key)
        tmp_path = path.with_suffix(".tmp")
        entry = {
            "_created_at": time.time(),
            "data": value,
        }

        with self._lock:
            try:
                tmp_path.write_text(
                    json.dumps(entry, ensure_ascii=False),
                    encoding="utf-8",
                )
                tmp_path.replace(path)
            except OSError:
                try:
                    tmp_path.unlink(missing_ok=True)
                except OSError:
                    pass
