"""
防封模块 — 三级频率限制器
L1 全局限流 / L2 设备限流 / L3 动作限流（按平台+操作类型）

所有自动化操作必须经过本限流器检查，超频则等待或拒绝。
依赖 Redis；Redis 不可用时退化为内存字典（重启丢失）。
"""

import asyncio
import logging
import random
import time
from typing import Dict, Optional, Tuple
from zoneinfo import ZoneInfo

logger = logging.getLogger(__name__)

# 业务操作时段按中国时区（Docker 默认 UTC 会误拦白天请求）
OPERATION_TZ = ZoneInfo("Asia/Shanghai")

_redis = None
_redis_last_check: float = 0.0
_REDIS_CHECK_INTERVAL = 60.0


async def _get_redis():
    global _redis, _redis_last_check
    now = time.time()
    if _redis is not None:
        if now - _redis_last_check > _REDIS_CHECK_INTERVAL:
            try:
                await _redis.ping()
                _redis_last_check = now
            except Exception:
                logger.warning("Redis 连接失效，重置并退化内存模式")
                _redis = None
        if _redis is not None:
            return _redis
    try:
        import redis.asyncio as aioredis
        from config import settings
        _redis = aioredis.from_url(settings.REDIS_URL, decode_responses=True)
        await _redis.ping()
        _redis_last_check = now
        return _redis
    except Exception as e:
        logger.warning(f"Redis 不可用，退化为内存限流: {e}")
        _redis = None
        return None


def human_delay(lo: float, hi: float, skew: float = 0.35) -> float:
    """
    AB-06 拟人化延迟：右偏截断高斯分布（替代均匀分布）。

    全网检索（2026-06-02）：真实停顿呈右偏分布，多数集中在区间偏低处、带长尾；
    均匀分布方差仅真实的 ~36%，易被风控识别为机械节律。
    本函数将采样集中在 [lo, lo+skew*(hi-lo)] 附近并保留右尾，再截断到 [lo, hi]。
    """
    if hi <= lo:
        return max(0.0, lo)
    span = hi - lo
    mu = lo + skew * span
    sigma = max(span * 0.25, 1e-3)
    val = random.gauss(mu, sigma)
    # 截断到 [lo, hi]
    if val < lo:
        val = lo + abs(val - lo) * 0.3  # 反射回区间内，避免堆在下界
    if val > hi:
        val = hi
    return round(max(lo, min(hi, val)), 3)


_memory_store: Dict[str, float] = {}
_memory_daily: Dict[str, int] = {}
_daily_date: str = ""

# AB-03 长会话中断 + 高危动作组合互斥（内存态，按设备维度）
_action_seq: Dict[str, int] = {}            # 设备累计动作数（用于长暂停节拍）
_long_pause_until: Dict[str, float] = {}    # 设备长暂停截止时间
_next_pause_at: Dict[str, int] = {}         # 设备下一次触发长暂停的动作序号
_last_highrisk: Dict[str, Tuple[str, float]] = {}  # 设备最近一次高危动作(type, ts)

# AB-02 静默限流熔断（内存态，按设备维度）
_silent_throttle_until: Dict[str, float] = {}   # 熔断截止时间
_silent_throttle_hits: Dict[str, int] = {}      # 累计触发次数（用于退避升级）
SILENT_THROTTLE_COOLDOWN = (1800, 3600)         # 首次熔断 30-60min，逐次×1.5

# 高危动作：同窗口内混跑会显著提升风控命中，需互斥
HIGH_RISK_ACTIONS = {
    "add_friend", "batch_add_friend", "batch_send", "mass_send",
    "create_group", "invite_to_group",
}
HIGH_RISK_COMBO_WINDOW = 600   # 10min 内禁止混跑不同类高危动作
LONG_PAUSE_RANGE = (600, 1800)  # 长暂停 10-30min
LONG_PAUSE_EVERY = (8, 12)      # 每 8-12 个动作插入一次长暂停


def _today() -> str:
    from datetime import datetime
    return datetime.now(OPERATION_TZ).strftime("%Y-%m-%d")


def human_delay(lo: float, hi: float) -> float:
    """AB-06 拟人延迟：右偏截断高斯，替代 uniform（真实停顿方差更大、右偏）。

    真实人工停顿多落在区间前段、长尾偏右；均值偏向 lo+35% 区间，σ≈span/2，
    截断到 [lo, hi]。多次采样失败回退 uniform。
    """
    if hi <= lo:
        return max(0.0, lo)
    span = hi - lo
    mu = lo + span * 0.35
    sigma = span * 0.5
    for _ in range(6):
        v = random.gauss(mu, sigma)
        if lo <= v <= hi:
            return v
    return random.uniform(lo, hi)


PLATFORM_LIMITS: Dict[str, Dict[str, Dict]] = {
    "wechat": {
        "add_friend":      {"interval": (180, 300), "daily_max": 50,  "new_daily_max": 10},
        "send_message":    {"interval": (30, 60),   "daily_max": 200, "new_daily_max": 20},
        "post_moments":    {"interval": (7200, 10800), "daily_max": 5, "new_daily_max": 2},
        "batch_send":      {"interval": (300, 600), "daily_max": 50,  "new_daily_max": 5},
        "like_moments":    {"interval": (15, 45),   "daily_max": 100, "new_daily_max": 20},
        "comment_moments": {"interval": (60, 180),  "daily_max": 50,  "new_daily_max": 10},
        "send_group_message": {"interval": (60, 120), "daily_max": 100, "new_daily_max": 15},
    },
    "douyin": {
        "send_message": {"interval": (60, 120),  "daily_max": 100, "new_daily_max": 15},
        "follow":       {"interval": (30, 60),   "daily_max": 200, "new_daily_max": 30},
        "like":         {"interval": (5, 15),    "daily_max": 500, "new_daily_max": 100},
        "comment":      {"interval": (60, 180),  "daily_max": 50,  "new_daily_max": 10},
    },
    "xhs": {
        "send_message": {"interval": (60, 180),  "daily_max": 50,  "new_daily_max": 10},
        "like":         {"interval": (15, 30),   "daily_max": 300, "new_daily_max": 50},
        "collect":      {"interval": (30, 60),   "daily_max": 200, "new_daily_max": 30},
        "comment":      {"interval": (60, 180),  "daily_max": 30,  "new_daily_max": 5},
    },
    "xianyu": {
        "send_message": {"interval": (60, 120),  "daily_max": 80,  "new_daily_max": 15},
    },
    "soul": {
        "send_message": {"interval": (30, 90),   "daily_max": 100, "new_daily_max": 20},
    },
}

L1_GLOBAL_RPM = 100
L2_DEVICE_RPM = 10

OPERATION_HOURS = (7, 23)


class DailyLimitExceeded(Exception):
    """日操作上限已达"""
    pass


class OutsideOperationHours(Exception):
    """非操作时段"""
    pass


class IntervalNotReady(Exception):
    """防封间隔未到（HTTP 层 fast-fail，避免长 sleep 阻塞请求）"""

    def __init__(self, wait_seconds: float):
        self.wait_seconds = wait_seconds
        super().__init__(f"防封间隔未到，需再等 {wait_seconds:.0f}s")


class RateLimitWaiting(Exception):
    """需要等待（信息性，不应到达调用方）"""
    pass


class LongPauseRequired(Exception):
    """AB-03 长会话中断：每 8-12 动作插入 10-30min 长暂停（fast-fail，不长 sleep）"""

    def __init__(self, wait_seconds: float):
        self.wait_seconds = wait_seconds
        super().__init__(f"长会话中断，需再等 {wait_seconds:.0f}s（拟人节律）")


class HighRiskComboBlocked(Exception):
    """AB-03 高危动作组合互斥：短窗口内禁止混跑加好友/群发/建群等"""

    def __init__(self, prev_action: str, cur_action: str, wait_seconds: float):
        self.wait_seconds = wait_seconds
        super().__init__(
            f"高危动作组合互斥：{prev_action} 后 {wait_seconds:.0f}s 内禁止 {cur_action}"
        )


class SilentThrottleCooldown(Exception):
    """AB-02 静默限流熔断：发送 success 但无 msg_id，疑似降权，写类动作冷却"""

    def __init__(self, wait_seconds: float, hits: int):
        self.wait_seconds = wait_seconds
        self.hits = hits
        super().__init__(
            f"静默限流熔断中（第{hits}次触发），需再等 {wait_seconds:.0f}s 后重试写操作"
        )


# AB-06 风险分级：把防封信号映射到处置动作
#   block       —— 硬熔断/账号已限制，禁止继续，直接拒绝
#   needs_human  —— 疑似降权/连续失败，需人工介入复核
#   log          —— 节律未到/正常限流，记录即可（可重试）
RISK_BLOCK = "block"
RISK_NEEDS_HUMAN = "needs_human"
RISK_LOG = "log"


def classify_risk(reason: str) -> str:
    """AB-06：依据防封拦截原因文本，返回处置等级 block/needs_human/log。"""
    r = (reason or "").lower()
    # 账号级硬限制 / 熔断 → 需人工复核
    if any(k in reason for k in ("静默限流", "熔断", "降权", "限制", "封", "appeal", "needs_human")):
        return RISK_NEEDS_HUMAN
    if "block" in r or "高危动作组合" in reason:
        return RISK_BLOCK
    # 时段 / 日上限 / 长暂停 / 间隔未到 → 记录可重试
    if any(k in reason for k in ("操作时段", "日上限", "长会话", "间隔", "等待", "pause")):
        return RISK_LOG
    return RISK_LOG


class AntiDetectRateLimiter:
    """三级防封限流器（单例）"""

    def get_limit(self, platform: str, action: str) -> Optional[Dict]:
        plat = PLATFORM_LIMITS.get(platform)
        if not plat:
            return None
        for key in (action, action.replace("-", "_")):
            if key in plat:
                return plat[key]
        return None

    def check_operation_hours(self) -> bool:
        """检查当前时间是否在允许操作时段内（默认 7:00-23:00，Asia/Shanghai）"""
        from datetime import datetime
        hour = datetime.now(OPERATION_TZ).hour
        return OPERATION_HOURS[0] <= hour < OPERATION_HOURS[1]

    async def _check_l1_global(self) -> bool:
        """L1 全局限流：所有设备总请求 <= L1_GLOBAL_RPM/分钟"""
        now = time.time()
        window_key = f"rl:l1:global:{int(now // 60)}"
        r = await _get_redis()
        if r:
            try:
                count = await r.incr(window_key)
                if count == 1:
                    await r.expire(window_key, 120)
                if count > L1_GLOBAL_RPM:
                    logger.warning(f"[RateLimiter] L1 全局限流触发: {count}/{L1_GLOBAL_RPM} RPM")
                    return False
                return True
            except Exception:
                pass
        win_key = f"l1:{int(now // 60)}"
        _memory_daily[win_key] = _memory_daily.get(win_key, 0) + 1
        return _memory_daily[win_key] <= L1_GLOBAL_RPM

    async def _check_l2_device(self, device_id: str) -> bool:
        """L2 设备限流：单设备总请求 <= L2_DEVICE_RPM/分钟"""
        now = time.time()
        window_key = f"rl:l2:{device_id}:{int(now // 60)}"
        r = await _get_redis()
        if r:
            try:
                count = await r.incr(window_key)
                if count == 1:
                    await r.expire(window_key, 120)
                if count > L2_DEVICE_RPM:
                    logger.warning(f"[RateLimiter] L2 设备限流触发 {device_id}: {count}/{L2_DEVICE_RPM} RPM")
                    return False
                return True
            except Exception:
                pass
        win_key = f"l2:{device_id}:{int(now // 60)}"
        _memory_daily[win_key] = _memory_daily.get(win_key, 0) + 1
        return _memory_daily[win_key] <= L2_DEVICE_RPM

    async def check_and_wait(
        self,
        device_id: str,
        platform: str,
        action: str,
        is_new_account: bool = False,
        max_sleep: Optional[float] = None,
    ) -> float:
        """
        三级检查：L1全局 → L2设备 → L3动作 + 时段检查。
        返回实际等待的秒数。
        若日上限已到，抛 DailyLimitExceeded。
        """
        if not self.check_operation_hours() and action not in ["status", "heartbeat", "emergency_stop"]:
            raise OutsideOperationHours(
                f"当前不在操作时段 ({OPERATION_HOURS[0]}:00-{OPERATION_HOURS[1]}:00)"
            )

        # AB-02 熔断 + AB-03 长会话中断 + 高危组合互斥（仅对受限业务动作生效；读类/心跳不拦）
        if self.get_limit(platform, action) is not None:
            self._check_silent_throttle(device_id)
            self._check_long_pause(device_id)
            self._check_highrisk_combo(device_id, action)

        if not await self._check_l1_global():
            wait = random.uniform(3.0, 8.0)
            logger.info(f"[RateLimiter] L1 全局超限，等待 {wait:.1f}s")
            await asyncio.sleep(wait)

        if not await self._check_l2_device(device_id):
            wait = random.uniform(5.0, 15.0)
            logger.info(f"[RateLimiter] L2 设备超限 {device_id}，等待 {wait:.1f}s")
            await asyncio.sleep(wait)

        limit = self.get_limit(platform, action)
        if limit is None:
            await asyncio.sleep(random.uniform(0.5, 2.0))
            return 0.0

        daily_max = limit.get("new_daily_max" if is_new_account else "daily_max", 9999)
        daily_count = await self._get_daily_count(device_id, platform, action)
        if daily_count >= daily_max:
            raise DailyLimitExceeded(
                f"{platform}.{action} 已达日上限 {daily_max}（当前 {daily_count}）"
            )

        interval_range: Tuple[float, float] = limit["interval"]
        # AB-06：拟人化右偏高斯延迟（替代均匀分布，更贴近真实停顿节律）
        min_wait = human_delay(*interval_range)

        last_ts = await self._get_last_ts(device_id, platform, action)
        elapsed = time.time() - last_ts if last_ts else float("inf")
        wait = max(0.0, min_wait - elapsed)

        if wait > 0:
            jitter = random.uniform(-wait * 0.1, wait * 0.15)
            wait = max(0.3, wait + jitter)
            if max_sleep is not None and wait > max_sleep:
                raise IntervalNotReady(wait)
            logger.info(
                f"[RateLimiter] {device_id}/{platform}.{action} 等待 {wait:.1f}s "
                f"(interval={interval_range}, daily={daily_count}/{daily_max})"
            )
            await asyncio.sleep(wait)

        await self._record(device_id, platform, action)
        return wait

    def _check_long_pause(self, device_id: str) -> None:
        """每 8-12 个动作触发一次 10-30min 长暂停，模拟真人会话间断。"""
        now = time.time()
        until = _long_pause_until.get(device_id, 0.0)
        if now < until:
            raise LongPauseRequired(until - now)
        seq = _action_seq.get(device_id, 0) + 1
        _action_seq[device_id] = seq
        threshold = _next_pause_at.get(device_id)
        if threshold is None:
            threshold = random.randint(*LONG_PAUSE_EVERY)
            _next_pause_at[device_id] = threshold
        if seq >= threshold:
            pause = random.uniform(*LONG_PAUSE_RANGE)
            _long_pause_until[device_id] = now + pause
            _action_seq[device_id] = 0
            _next_pause_at[device_id] = random.randint(*LONG_PAUSE_EVERY)
            logger.info(f"[RateLimiter] AB-03 长会话中断 {device_id}: 暂停 {pause:.0f}s")
            raise LongPauseRequired(pause)

    def _check_silent_throttle(self, device_id: str) -> None:
        """AB-02：静默限流熔断期内拒绝写类动作（fast-fail）。"""
        until = _silent_throttle_until.get(device_id, 0.0)
        now = time.time()
        if now < until:
            raise SilentThrottleCooldown(until - now, _silent_throttle_hits.get(device_id, 1))

    def trip_silent_throttle(self, device_id: str) -> float:
        """AB-02：检测到 success 但无 msg_id 时调用，触发熔断并按次数退避升级。

        返回本次熔断秒数。
        """
        hits = _silent_throttle_hits.get(device_id, 0) + 1
        _silent_throttle_hits[device_id] = hits
        base = random.uniform(*SILENT_THROTTLE_COOLDOWN)
        cooldown = base * (1.5 ** (hits - 1))
        _silent_throttle_until[device_id] = time.time() + cooldown
        logger.warning(
            f"[RateLimiter] AB-02 静默限流熔断 {device_id}: 第{hits}次，冷却 {cooldown:.0f}s"
        )
        return cooldown

    def reset_silent_throttle(self, device_id: str) -> None:
        """真实 msg_id 回来后清除熔断退避计数。"""
        _silent_throttle_until.pop(device_id, None)
        _silent_throttle_hits.pop(device_id, None)

    def get_runtime_state(self, device_id: str) -> Dict:
        """AB-05 看板：单设备防封运行态（熔断/长暂停/动作节拍/最近高危）。"""
        now = time.time()
        st_until = _silent_throttle_until.get(device_id, 0.0)
        lp_until = _long_pause_until.get(device_id, 0.0)
        prev_hr = _last_highrisk.get(device_id)
        return {
            "silent_throttle": {
                "active": now < st_until,
                "until_in_s": max(0, round(st_until - now)),
                "hits": _silent_throttle_hits.get(device_id, 0),
            },
            "long_pause": {
                "active": now < lp_until,
                "until_in_s": max(0, round(lp_until - now)),
                "action_seq": _action_seq.get(device_id, 0),
                "next_pause_at": _next_pause_at.get(device_id),
            },
            "last_highrisk": (
                {"action": prev_hr[0], "ago_s": round(now - prev_hr[1])} if prev_hr else None
            ),
        }

    def _check_highrisk_combo(self, device_id: str, action: str) -> None:
        """短窗口内禁止混跑不同类高危动作（加好友/群发/建群等）。"""
        act = action.replace("-", "_")
        if act not in HIGH_RISK_ACTIONS:
            return
        prev = _last_highrisk.get(device_id)
        now = time.time()
        if prev:
            prev_act, prev_ts = prev
            if prev_act != act and (now - prev_ts) < HIGH_RISK_COMBO_WINDOW:
                raise HighRiskComboBlocked(prev_act, act, HIGH_RISK_COMBO_WINDOW - (now - prev_ts))
        _last_highrisk[device_id] = (act, now)

    async def get_daily_count(self, device_id: str, platform: str, action: str) -> int:
        return await self._get_daily_count(device_id, platform, action)

    def get_platform_config(self, platform: str, action: str) -> Optional[Dict]:
        """获取某平台某动作的限流配置（供风控看板展示）"""
        return self.get_limit(platform, action)

    # ---- 存储层 ----

    async def _get_last_ts(self, device_id: str, platform: str, action: str) -> float:
        key = f"rl:ts:{device_id}:{platform}:{action}"
        r = await _get_redis()
        if r:
            try:
                val = await r.get(key)
                return float(val) if val else 0.0
            except Exception:
                pass
        return _memory_store.get(key, 0.0)

    async def _get_daily_count(self, device_id: str, platform: str, action: str) -> int:
        global _daily_date
        today = _today()
        key = f"rl:daily:{today}:{device_id}:{platform}:{action}"
        r = await _get_redis()
        if r:
            try:
                val = await r.get(key)
                return int(val) if val else 0
            except Exception:
                pass
        if _daily_date != today:
            _memory_daily.clear()
            _daily_date = today
        return _memory_daily.get(key, 0)

    async def _record(self, device_id: str, platform: str, action: str):
        now = time.time()
        today = _today()
        ts_key = f"rl:ts:{device_id}:{platform}:{action}"
        daily_key = f"rl:daily:{today}:{device_id}:{platform}:{action}"
        r = await _get_redis()
        if r:
            try:
                pipe = r.pipeline()
                pipe.set(ts_key, str(now), ex=86400)
                pipe.incr(daily_key)
                pipe.expire(daily_key, 86400)
                await pipe.execute()
                return
            except Exception:
                pass
        _memory_store[ts_key] = now
        global _daily_date
        if _daily_date != today:
            _memory_daily.clear()
            _daily_date = today
        _memory_daily[daily_key] = _memory_daily.get(daily_key, 0) + 1


rate_limiter = AntiDetectRateLimiter()
