"""高级运费规则：影子对比事件。

影子模式只观测、不计费。三条硬规则：
1. **脱敏**：只记金额、币种、方案/分组、规则 id 与修订号、reason key。
   绝不记地址、邮编、优惠券码、购物车明细——这些是个人数据，观测用不到。
2. **限流**：按租户采样，避免大促把 advanced_shipping_shadow_events 写爆。
3. **隔离**：对比跑在 legacy 定价之后、订单事务之外，整段包 try/except，
   出错只写日志，绝不影响结账。
"""
import logging
import time
from decimal import Decimal

logger = logging.getLogger(__name__)

#: 同一租户两次影子事件之间的最小间隔（秒）。
# ponytail: 进程内字典，上限是"每个 worker 各限一次"；真要全局精确限流再换 Redis INCR + EXPIRE。
SHADOW_MIN_INTERVAL_SECONDS = 5.0

_last_recorded: dict[int, float] = {}


def should_record_shadow(tenant_id: int, *, state: dict | None = None, now=time.monotonic) -> bool:
    """按租户限流。被抑制时不刷新时间戳，否则高频请求会把冷却窗口无限推后。"""
    store = _last_recorded if state is None else state
    current = now()
    previous = store.get(tenant_id)
    if previous is not None and current - previous < SHADOW_MIN_INTERVAL_SECONDS:
        return False
    store[tenant_id] = current
    return True


def _money(value) -> str | None:
    return None if value is None else str(Decimal(str(value)).quantize(Decimal("0.01")))


def build_shadow_payload(*, legacy_fee, quote_set, facts) -> dict:
    """脱敏的观测载荷。字段是白名单式逐个拼出来的，不是把上下文 dump 进去。"""
    selected = quote_set.selected
    legacy = None if legacy_fee is None else Decimal(str(legacy_fee))
    delta = None if (legacy is None or selected is None) else selected.fee - legacy
    return {
        "currency": facts.base_currency,
        "delivery_mode": facts.delivery_mode,
        "legacy_fee": _money(legacy),
        "selected_fee": _money(selected.fee) if selected else None,
        "delta": _money(delta),
        "reason_keys": list(quote_set.reason_keys),
        "candidates": [
            {
                "shipping_method_id": q.shipping_method_id,
                "quote_group": q.quote_group,
                "delivery_mode": q.delivery_mode,
                "fee": _money(q.fee),
                "rule_ids": list(q.rule_ids),
                "revision_ids": list(q.revision_ids),
            }
            for q in quote_set.quotes
        ],
    }


async def record_shadow_event(db, tenant_id: int, *, legacy_fee, quote_set, facts,
                              event_type: str = "quote_compare", check_rate_limit: bool = True) -> bool:
    """写一条影子事件到**传入的**会话。被限流时返回 False。不 commit。

    结账路径请走 run_shadow_comparison：它会另开一个会话，
    这样影子事件既不会随订单回滚而丢，更重要的是——它写失败也炸不到订单事务。
    """
    # 懒加载且走聚合模块：app.core.models 会反向 import 本插件的 models（见 cache.py 说明）
    from app.core.models import AdvancedShippingShadowEvent

    if check_rate_limit and not should_record_shadow(tenant_id):
        return False
    selected = quote_set.selected
    db.add(AdvancedShippingShadowEvent(
        tenant_id=tenant_id,
        shipping_method_id=selected.shipping_method_id if selected else 0,
        rule_id=selected.rule_ids[0] if selected and selected.rule_ids else None,
        quote_group=selected.quote_group if selected else None,
        event_type=event_type,
        legacy_amount=None if legacy_fee is None else Decimal(str(legacy_fee)),
        candidate_amount=selected.fee if selected else None,
        payload=build_shadow_payload(legacy_fee=legacy_fee, quote_set=quote_set, facts=facts),
    ))
    return True


async def run_shadow_comparison(tenant_id: int, *, legacy_fee, quote_set, facts,
                                event_type: str = "quote_compare") -> bool:
    """Task 5 的入口：在 legacy 定价算完之后调用。

    **真正独立的会话**：db.add 到订单那个会话里，行会被 autoflush 带进订单事务，
    于是"影子行写不进去"就等于"客户下不了单"——正是本模式明令禁止的事。
    这里自己开会话、自己 commit，连 flush 失败都伤不到订单。

    任何异常都只落日志。影子观测出错绝不能让客户结不了账。
    """
    from app.database import AsyncSessionLocal

    if not should_record_shadow(tenant_id):
        return False        # 被限流的那 99% 连会话都不该开
    try:
        async with AsyncSessionLocal() as session:
            recorded = await record_shadow_event(
                session, tenant_id, legacy_fee=legacy_fee, quote_set=quote_set,
                facts=facts, event_type=event_type, check_rate_limit=False)
            if recorded:
                await session.commit()
            return recorded
    except Exception as exc:                     # noqa: BLE001 — 观测失败必须静默
        logger.warning("高级运费影子对比失败 tenant=%s: %s", tenant_id, exc)
        return False


__all__ = [
    "SHADOW_MIN_INTERVAL_SECONDS", "build_shadow_payload", "record_shadow_event",
    "run_shadow_comparison", "should_record_shadow",
]
