"""进程内限流后端: 与 Redis 版同一契约的六道闸实现(D3 双后端)。

语义蓝本 CHS `app/coordination/limiter.py`: 并发 = 带 TTL 的租约(持有者
死亡后过期回收);RPM/TPM = 分钟**固定窗口**计数(`int(now/60)`,与 CHS
Lua 同款口径);TPM 入场按 est 预扣,settle
按实际结算多退少补且退款落 acquire 时的窗口。检查-占用在单次同步段内完成
(无 await 穿插),单事件循环下天然原子;本实现不跨进程,是单进程部署的
正确答案(多 worker 用 M2 Redis 后端)。
"""

from __future__ import annotations

import asyncio
import time
import uuid
from typing import TYPE_CHECKING

from polygateway.errors import SourceNotConfiguredError
from polygateway.types import GlobalLimits, SourceConfig, SourceStats

if TYPE_CHECKING:
    from collections.abc import Callable

_GLOBAL = "__global__"
_WINDOW_S = 60.0


class _MemoryPermit:
    """入场许可;release/settle 幂等(CHS _RedisPermit 同款 flag 语义)。"""

    def __init__(
        self, limiter: InMemoryLimiter, source: str, lease_id: str, est: int, window: int
    ) -> None:
        self._limiter = limiter
        self._source = source
        self._lease_id = lease_id
        self._est = est
        self._window = window
        self._released = False
        self._settled = False

    async def release(self) -> None:
        if self._released:
            return
        self._released = True
        self._limiter._release_lease(self._source, self._lease_id)

    async def settle(self, actual_tokens: int) -> None:
        if self._settled:
            return
        self._settled = True
        delta = actual_tokens - self._est
        if delta:
            self._limiter._settle_tpm(self._source, delta, self._window)


class InMemoryLimiter:
    """六道闸: 全局/单源 × 并发/RPM/TPM;限额 0 = 该闸不启用。"""

    def __init__(
        self,
        *,
        scope: str,
        sources: dict[str, SourceConfig],
        global_limits: GlobalLimits,
        lease_ttl_s: float = 1500.0,
        now: Callable[[], float] = time.monotonic,
        sleep: Callable[[float], object] = asyncio.sleep,
        poll_interval_s: float = 0.05,
    ) -> None:
        if lease_ttl_s <= 0 or poll_interval_s <= 0:
            raise ValueError("lease_ttl_s 与 poll_interval_s 必须 > 0")
        self._scope = scope
        self._sources = dict(sources)
        self._global = global_limits
        self._lease_ttl_s = lease_ttl_s
        self._now = now
        self._sleep = sleep
        self._poll_interval_s = poll_interval_s
        # source → lease_id → 过期时刻
        self._leases: dict[str, dict[str, float]] = {name: {} for name in self._sources}
        # (source|__global__, window_id) → 计数
        self._rpm: dict[tuple[str, int], int] = {}
        self._tpm: dict[tuple[str, int], int] = {}
        self._progress_at: float | None = None

    # —— 内部原语(同步,单事件循环下原子)——

    def _cfg(self, source_key: str) -> SourceConfig:
        cfg = self._sources.get(source_key)
        if cfg is None:
            raise SourceNotConfiguredError(f"未知源 {source_key!r}(scope={self._scope})")
        return cfg

    def _window(self) -> int:
        return int(self._now() / _WINDOW_S)

    def _purge_leases(self) -> None:
        now = self._now()
        for leases in self._leases.values():
            expired = [lid for lid, expiry in leases.items() if expiry <= now]
            for lid in expired:
                del leases[lid]

    def _inflight(self, source_key: str) -> int:
        return len(self._leases[source_key])

    def _inflight_total(self) -> int:
        return sum(len(leases) for leases in self._leases.values())

    def _release_lease(self, source_key: str, lease_id: str) -> None:
        self._leases[source_key].pop(lease_id, None)

    def _settle_tpm(self, source_key: str, delta: int, window: int) -> None:
        # 退款/补记落 acquire 时的窗口,计数下限 0(CHS Lua 同款)
        for key in (source_key, _GLOBAL):
            slot = (key, window)
            self._tpm[slot] = max(0, self._tpm.get(slot, 0) + delta)

    def _gates_pass(self, cfg: SourceConfig, est_tokens: int, window: int) -> bool:
        checks = (
            (self._global.max_concurrency, self._inflight_total() + 1),
            (cfg.max_concurrency, self._inflight(cfg.name) + 1),
            (self._global.rpm, self._rpm.get((_GLOBAL, window), 0) + 1),
            (cfg.rpm, self._rpm.get((cfg.name, window), 0) + 1),
            (self._global.tpm, self._tpm.get((_GLOBAL, window), 0) + est_tokens),
            (cfg.tpm, self._tpm.get((cfg.name, window), 0) + est_tokens),
        )
        return all(limit <= 0 or would_be <= limit for limit, would_be in checks)

    # —— 契约方法 ——

    async def try_acquire(self, source_key: str, est_tokens: int) -> _MemoryPermit | None:
        """非阻塞准入: 六闸全过才占用;任一满则返回 None 且零副作用。"""
        cfg = self._cfg(source_key)
        self._purge_leases()
        window = self._window()
        if not self._gates_pass(cfg, est_tokens, window):
            return None
        lease_id = uuid.uuid4().hex
        self._leases[source_key][lease_id] = self._now() + self._lease_ttl_s
        for key in (source_key, _GLOBAL):
            self._rpm[(key, window)] = self._rpm.get((key, window), 0) + 1
            if est_tokens:
                self._tpm[(key, window)] = self._tpm.get((key, window), 0) + est_tokens
        return _MemoryPermit(self, source_key, lease_id, est_tokens, window)

    async def acquire(self, source_key: str, est_tokens: int) -> _MemoryPermit:
        """阻塞准入: 轮询直到拿到 permit;sleep 可被取消(取消穿透铁律)。"""
        while True:
            permit = await self.try_acquire(source_key, est_tokens)
            if permit is not None:
                return permit
            await self._sleep(self._poll_interval_s)

    async def source_stats(self, source_key: str) -> SourceStats:
        self._cfg(source_key)
        self._purge_leases()
        window = self._window()
        return SourceStats(
            inflight=self._inflight(source_key),
            rpm_used=self._rpm.get((source_key, window), 0),
            tpm_used=self._tpm.get((source_key, window), 0),
        )

    async def mark_progress(self) -> None:
        """记录"最近一次出餐"时刻,供背压 stall 判定(M2)读取。"""
        self._progress_at = self._now()

    async def progress_age_s(self) -> float:
        if self._progress_at is None:
            return float("inf")
        return self._now() - self._progress_at
