# -*- coding: utf-8 -*-
"""FileStateCache LRU 文件状态缓存（Phase 6 Tier 1-4 AQ）。

借鉴 zhikuncode-main FileStateCache.java：
- OrderedDict accessOrder=true 实现 LRU
- 大小限制：100 条 / 25MB 任一超限淘汰
- 路径规范化（os.path.realpath）作为 key
- 存储 (mtime_ns, size, content) 三元组
- 读取时比对 mtime_ns + size，未变则命中
"""
from collections import OrderedDict
import os
import threading
from typing import Optional, Tuple

__all__ = ["FileStateCache", "get_default_cache"]


class FileStateCache:
    """LRU 文件状态缓存。

    - 路径规范化（os.path.realpath）作为 key
    - 存储 (mtime_ns, size, content) 三元组
    - 读取时比对 mtime_ns + size，未变则命中；否则淘汰并返回 None
    - 任一超限（max_entries 或 max_total_bytes）触发 LRU 淘汰
    """

    MAX_ENTRIES = 100
    MAX_TOTAL_BYTES = 25 * 1024 * 1024  # 25MB

    def __init__(self, max_entries: Optional[int] = None, max_total_bytes: Optional[int] = None):
        # 允许子类/测试覆盖默认值
        if max_entries is not None:
            self.MAX_ENTRIES = max_entries
        if max_total_bytes is not None:
            self.MAX_TOTAL_BYTES = max_total_bytes
        self._cache: "OrderedDict[str, Tuple[int, int, str]]" = OrderedDict()
        self._total_bytes = 0
        self._lock = threading.Lock()

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

    def get(self, path: str) -> Optional[str]:
        """读取缓存。命中返回内容，未命中或已失效返回 None。"""
        try:
            real = os.path.realpath(path)
        except OSError:
            return None
        try:
            st = os.stat(real)
        except OSError:
            # 文件不存在：若缓存里有则清理，否则直接返回 None
            with self._lock:
                if real in self._cache:
                    self._evict_locked(real)
            return None

        with self._lock:
            entry = self._cache.get(real)
            if entry is None:
                return None
            mtime_ns, size, content = entry
            if st.st_mtime_ns != mtime_ns or st.st_size != size:
                # 文件已被外部修改，失效
                self._evict_locked(real)
                return None
            # 命中：移到末尾（最近使用）
            self._cache.move_to_end(real)
            return content

    def put(self, path: str, content: str) -> None:
        """写入缓存。若文件不存在则忽略（无法记录 mtime）。"""
        try:
            real = os.path.realpath(path)
        except OSError:
            return
        try:
            st = os.stat(real)
        except OSError:
            # 文件不存在：不缓存（避免后续 get 误命中）
            return

        size = len(content.encode("utf-8"))

        with self._lock:
            # 若已存在，先移除旧条目
            if real in self._cache:
                self._evict_locked(real)
            # 容量预检 + 淘汰
            while (len(self._cache) >= self.MAX_ENTRIES or
                   (self._total_bytes + size > self.MAX_TOTAL_BYTES)):
                if not self._cache:
                    break
                self._evict_oldest_locked()
            # 写入
            self._cache[real] = (st.st_mtime_ns, st.st_size, content)
            self._total_bytes += size

    def invalidate(self, path: str) -> None:
        """显式失效某条目。不存在的路径不抛异常。"""
        try:
            real = os.path.realpath(path)
        except OSError:
            return
        with self._lock:
            if real in self._cache:
                self._evict_locked(real)

    def clear(self) -> None:
        """清空所有缓存。"""
        with self._lock:
            self._cache.clear()
            self._total_bytes = 0

    # ------------------------------------------------------------------ 内部方法（调用方已持锁）

    def _evict_locked(self, key: str) -> None:
        """移除指定 key（调用方持锁）。"""
        entry = self._cache.pop(key, None)
        if entry is not None:
            _, _, content = entry
            self._total_bytes -= len(content.encode("utf-8"))
            if self._total_bytes < 0:
                self._total_bytes = 0

    def _evict_oldest_locked(self) -> None:
        """淘汰最久未使用条目（OrderedDict 首项，调用方持锁）。"""
        if not self._cache:
            return
        key, entry = self._cache.popitem(last=False)
        _, _, content = entry
        self._total_bytes -= len(content.encode("utf-8"))
        if self._total_bytes < 0:
            self._total_bytes = 0


# ------------------------------------------------------------------ 模块级单例

_default_cache: Optional[FileStateCache] = None
_default_lock = threading.Lock()


def get_default_cache() -> FileStateCache:
    """返回模块级默认 FileStateCache 单例。"""
    global _default_cache
    if _default_cache is None:
        with _default_lock:
            if _default_cache is None:
                _default_cache = FileStateCache()
    return _default_cache
