"""pricebot 上游实例选择：按 trace_id 一致性 hash(ketama 风格)选实例。

单机多进程下,保证同一 trace_id(一次比价/领券的所有帧)落同一 pricebot 进程,
从而用进程内内存维护 session/coordinator,不需要 Redis。

为什么用一致性 hash 而非简单取模(crc32 % N):
  加/减实例时,取模会让几乎所有 trace 重新映射(扩缩容全量中断进行中的比价);
  一致性 hash(虚拟节点)只重映射约 1/N 的 trace,扩缩容对存量冲击最小。
  ⚠️ 但内存态下,被重映射的那 1/N trace 仍会丢失 session(状态没外置),
     所以扩缩容仍建议挑低峰。

hash 用 md5(纯函数),app-server 多 worker / 多实例算出的环一致,亲和不会因
app-server 自身水平扩展而被破坏。
"""
from __future__ import annotations

import bisect
import hashlib
from typing import Dict, List, Optional

from app.core.config import settings

# 每个真实实例在 hash 环上的虚拟节点数。越多分布越均匀。实例数少时(6~10)需要更多
# 虚拟节点才均匀:实测 150→负载偏差~16%、1000→~6%。环只在启动/实例变更时构建一次,
# 1000×N 个点的构建与 bisect 查找开销都可忽略。
_VNODES_PER_NODE = 1000


def _hash(s: str) -> int:
    """取 md5 前 8 hex(32-bit)做环坐标。确定性、跨进程一致。"""
    return int(hashlib.md5(s.encode("utf-8")).hexdigest()[:8], 16)


class _HashRing:
    """ketama 风格一致性 hash 环。不可变,实例列表变化时整体重建(见 _get_ring)。"""

    def __init__(self, nodes: List[str]):
        self._keys: List[int] = []                # 升序的环坐标
        self._key_to_node: Dict[int, str] = {}
        for node in nodes:
            for v in range(_VNODES_PER_NODE):
                h = _hash(f"{node}#{v}")
                # 极小概率撞坐标,撞了跳过该虚拟节点(不影响正确性,仅少一个 vnode)
                if h not in self._key_to_node:
                    self._key_to_node[h] = node
                    self._keys.append(h)
        self._keys.sort()

    def pick(self, key: str) -> str:
        h = _hash(key)
        idx = bisect.bisect(self._keys, h)
        if idx == len(self._keys):
            idx = 0                                # 环回绕
        return self._key_to_node[self._keys[idx]]


# 按实例列表缓存环,列表变了(改 PRICEBOT_INSTANCES + 重启)才重建。
_ring: Optional[_HashRing] = None
_ring_nodes: tuple = ()


def _get_ring(nodes: List[str]) -> _HashRing:
    global _ring, _ring_nodes
    key = tuple(nodes)
    if _ring is None or key != _ring_nodes:
        _ring = _HashRing(nodes)
        _ring_nodes = key
    return _ring


def pick_pricebot(trace_id: Optional[str]) -> str:
    """按 trace_id 选 pricebot 实例 base url。

    - 实例列表来自 settings.pricebot_instances(空则单实例兜底 [PRICEBOT_BASE_URL])
    - trace_id 缺失/空 或 单实例 → 直接返回第一个,不进环
    """
    nodes = settings.pricebot_instances
    if len(nodes) == 1 or not trace_id:
        return nodes[0]
    # str() 防御:协议保证 trace_id 是 str,但万一传入非 str(如 int)也不至于在
    # _hash 的 .encode() 处炸,确定性地选到实例。
    return _get_ring(nodes).pick(str(trace_id))
