"""美团搜索翻页游标(searchId)缓存 —— 「距离最近」tab 翻页提速。

**问题**:美团 query_coupon 的搜索结果只能靠 searchId 续页(pageNo 翻不动),而我们的 HTTP 接口
是无状态的、客户端只传页码。原实现因此每次都从第 1 页顺序重放到第 N 页 —— 取第 N 页要向美团发
N 次请求,用户越往下滑越慢(第 5 页 ≈ 第 1 页的 5 倍耗时,且每次调用都可能撞 402 限流)。

**做法**:把「翻到第 n 页所需的 searchId」按 (量化坐标, platform, 关键词) 记下来,
下次请求第 n 页直接一发命中;未命中才从缓存里最深的那一页往后重放,并把沿途游标补进缓存。
稳态下每翻一页恒定 1 次请求。

**坐标量化**到 2 位小数(~1km),与 `utils/meituan_city` 同口径:原始 GPS 每次抖动到小数点后 5~6 位,
不量化几乎不命中缓存;而同一路搜索的排序原点相差 1km 以内,对券列表的实际顺序无感知差别。

**TTL** 取 10 分钟:searchId 属于上游会话态,放太久会翻到过期游标(调用失败 → 调用方回退重放)。
缓存是**进程内**的,多 worker 各存一份、不共享,这没问题:命中率只影响快慢,不影响正确性。
"""
from __future__ import annotations

import threading
import time

# 缓存键:(量化纬度, 量化经度, platform, 搜索词)
RouteKey = tuple[float, float, int, str]

_TTL_SEC = 10 * 60
_MAX_ROUTES = 512          # 最多缓存多少条搜索路线(每条 = 一个坐标×平台×关键词)
_MAX_PAGES_PER_ROUTE = 60  # 单条路线最多记多少页游标,防某个坐标被无限翻页撑爆

# {route_key: {page_no: (search_id, 写入时刻)}};page_no 表示「用这个 searchId 能取到第几页」。
# 第 1 页不需要 searchId(直接 pageNo=1),故永远不入表。
_cursors: dict[RouteKey, dict[int, tuple[str, float]]] = {}
_lock = threading.Lock()


def route_key(latitude: float, longitude: float, platform: int, keyword: str) -> RouteKey:
    """构造缓存键(坐标量化到 ~1km)。"""
    return (round(latitude, 2), round(longitude, 2), platform, keyword)


def lookup(key: RouteKey, page: int) -> tuple[int, str | None]:
    """找到能最省调用地拿到第 `page` 页的起点。

    返回 (起始页, 该页要用的 search_id):
      - (page, "xxx")  缓存命中 → 调用方一发直达第 page 页
      - (m, "xxx")     命中更浅的第 m 页(1 < m < page)→ 从第 m 页重放到第 page 页
      - (1, None)      全未命中 → 从第 1 页(pageNo=1)重放
    """
    if page <= 1:
        return 1, None
    now = time.time()
    with _lock:
        pages = _cursors.get(key)
        if not pages:
            return 1, None
        for p in range(page, 1, -1):
            hit = pages.get(p)
            if hit is None:
                continue
            search_id, wrote_at = hit
            if now - wrote_at > _TTL_SEC:
                pages.pop(p, None)   # 过期即清,继续往浅了找
                continue
            return p, search_id
    return 1, None


def remember(key: RouteKey, page: int, search_id: str) -> None:
    """记下「用 search_id 可取到第 page 页」。page<=1 无意义(第 1 页不用游标)。"""
    if page <= 1 or not search_id:
        return
    now = time.time()
    with _lock:
        pages = _cursors.get(key)
        if pages is None:
            if len(_cursors) >= _MAX_ROUTES:
                _evict_oldest_route_locked()
            pages = _cursors[key] = {}
        pages[page] = (search_id, now)
        if len(pages) > _MAX_PAGES_PER_ROUTE:
            # 越浅的页越容易被重新走到,优先丢最深的那些(它们下次多半也过期了)
            for p in sorted(pages, reverse=True)[: len(pages) - _MAX_PAGES_PER_ROUTE]:
                pages.pop(p, None)


def drop(key: RouteKey) -> None:
    """整条路线作废。用在「拿缓存游标去请求却失败」时(多半是上游 searchId 过期)。"""
    with _lock:
        _cursors.pop(key, None)


def _evict_oldest_route_locked() -> None:
    """淘汰最久没更新过的一条路线(调用方须已持锁)。"""
    oldest_key = min(
        _cursors,
        key=lambda k: max((w for _, w in _cursors[k].values()), default=0.0),
    )
    _cursors.pop(oldest_key, None)
