"""促销规则引擎 — 纯计算，无副作用

调用方（pricing.py）负责：
  1. 加载 Discount 列表（自动 + 优惠码）
  2. 传入购物车明细和客户信息
  3. 收集 PromoResult 列表并汇总折扣

支持规则类型（Discount.type / normalized_type）：
  order_amount_off     满减，支持阶梯 DiscountTier
  order_percent_off    满折，支持阶梯 DiscountTier
  product_amount_off   指定商品直减 / 折扣
  product_buyxgety     买 X 赠 Y（含同款 BOGO）
  category_percent_off 指定分类打折
  free_shipping        免运费
  points_multiplier    积分倍数活动（不产生金额折扣）
"""
from dataclasses import dataclass, field
from decimal import Decimal
from typing import Optional

from app.core.models.discount import Discount
from app.core.models.discount_tier import DiscountTier
from app.core.models.customer import Customer
from app.core.models.product import Product, ProductVariant


# ── 数据结构 ───────────────────────────────────────────────────

@dataclass
class CartLine:
    """引擎需要的购物车行快照（已含价格）"""
    product_id: int
    variant_id: Optional[int]
    qty: Decimal
    unit_price: Decimal
    line_total: Decimal
    category_id: Optional[int]


@dataclass
class PromoResult:
    discount_id: int
    rule_type: str
    label: str
    amount: Decimal = Decimal("0")       # 金额折扣（正数）
    free_shipping: bool = False
    points_multiplier: Decimal = Decimal("1")
    applied: bool = False


# ── 条件校验 ──────────────────────────────────────────────────

def check_conditions(
    promo: Discount,
    customer: Optional[Customer],
    subtotal: Decimal,
    lines: list[CartLine],
    customer_usage_count: int,
) -> Optional[str]:
    """
    校验促销前置条件。
    返回 None 表示通过；返回字符串为失败原因。
    """
    if not promo.is_active:
        return "已停用"

    if promo.min_order_amount and subtotal < promo.min_order_amount:
        return f"订单金额未满 {promo.min_order_amount}"

    if promo.condition_min_qty:
        total_qty = sum(l.qty for l in lines)
        if total_qty < promo.condition_min_qty:
            return f"商品件数未满 {promo.condition_min_qty}"

    if promo.usage_limit is not None and promo.used_count >= promo.usage_limit:
        return "已达总使用上限"

    if promo.usage_limit_per_customer is not None:
        if customer is None:
            return "仅限登录用户使用"
        if customer_usage_count >= promo.usage_limit_per_customer:
            return "已达每人使用上限"

    if promo.condition_customer_level_id is not None:
        if customer is None or customer.member_level_id != promo.condition_customer_level_id:
            return "会员等级不满足"

    if promo.condition_order_count_min is not None:
        if customer is None:
            return "仅限登录用户使用"
        if customer.orders_count < promo.condition_order_count_min:
            return f"历史订单数不足 {promo.condition_order_count_min}"

    if promo.condition_order_count_max is not None:
        if customer is None or customer.orders_count > promo.condition_order_count_max:
            return "不符合订单次数上限条件"

    return None


# ── 规则计算函数 ──────────────────────────────────────────────

def _resolve_tier_value(tiers: list[DiscountTier], subtotal: Decimal, total_qty: Decimal) -> Optional[Decimal]:
    """从阶梯列表中找到当前满足的最高档折扣值，无则返回 None。"""
    best: Optional[Decimal] = None
    for t in sorted(tiers, key=lambda x: x.sort_order):
        amount_ok = t.min_amount is None or subtotal >= t.min_amount
        qty_ok = t.min_qty is None or total_qty >= t.min_qty
        if amount_ok and qty_ok:
            best = t.discount_value
    return best


def _eval_order_amount_off(
    promo: Discount, tiers: list[DiscountTier], subtotal: Decimal, total_qty: Decimal
) -> Decimal:
    if tiers:
        v = _resolve_tier_value(tiers, subtotal, total_qty)
        return v if v is not None else Decimal("0")
    return promo.value


def _eval_order_percent_off(
    promo: Discount, tiers: list[DiscountTier], subtotal: Decimal, total_qty: Decimal
) -> Decimal:
    if tiers:
        pct = _resolve_tier_value(tiers, subtotal, total_qty)
        if pct is None:
            return Decimal("0")
    else:
        pct = promo.value
    return subtotal * (pct / Decimal("100"))


def _eval_product_amount_off(promo: Discount, lines: list[CartLine]) -> Decimal:
    """指定商品直减 / 折扣。
    discount_product_ids 为空时应用到所有行。
    """
    target_ids = set(promo.discount_product_ids or [])
    total = Decimal("0")
    for line in lines:
        if target_ids and line.product_id not in target_ids:
            continue
        if promo.type in ("percentage", "order_percent_off", "product_amount_off") and promo.value < 100:
            total += line.line_total * (promo.value / Decimal("100"))
        else:
            total += promo.value * line.qty
    return total


def _eval_category_percent_off(promo: Discount, lines: list[CartLine]) -> Decimal:
    """指定分类打折。
    condition_category_ids / discount_category_ids 均为空时对所有分类生效。
    """
    target_cats = set(promo.discount_category_ids or promo.condition_category_ids or [])
    total = Decimal("0")
    for line in lines:
        if target_cats and line.category_id not in target_cats:
            continue
        total += line.line_total * (promo.value / Decimal("100"))
    return total


def _eval_product_buyxgety(promo: Discount, lines: list[CartLine]) -> Decimal:
    """买 X 件（条件商品）赠 Y 件（目标商品，按最便宜/最贵优先）。

    condition_product_ids: 触发的商品，None = 所有商品
    condition_min_qty:     买几件触发，默认 1
    discount_product_ids:  赠品范围，None = 与条件商品相同
    discount_qty:          每组赠几件，默认 1
    apply_once:            是否仅享一次（0 = 按倍数叠加）
    discount_qualifier:    'least'（默认） | 'most'
    """
    cond_ids = set(promo.condition_product_ids or [])
    disc_ids = set(promo.discount_product_ids or cond_ids or [])
    min_qty = promo.condition_min_qty or 1
    free_per_set = promo.discount_qty or 1
    qualifier = promo.discount_qualifier or "least"

    # 统计条件商品总件数
    cond_qty = sum(
        l.qty for l in lines
        if not cond_ids or l.product_id in cond_ids
    )
    same_products = cond_ids == disc_ids
    required_qty = min_qty + free_per_set if same_products else min_qty
    if cond_qty < required_qty:
        return Decimal("0")

    sets = 1 if promo.apply_once else (cond_qty // required_qty)
    free_needed = sets * free_per_set

    # 候选赠品行（复制一份以便扣减）
    candidates = [
        [l.product_id, l.unit_price, l.qty]
        for l in lines
        if not disc_ids or l.product_id in disc_ids
    ]
    # 按价格排序
    candidates.sort(key=lambda x: x[1], reverse=(qualifier == "most"))

    discount = Decimal("0")
    for item in candidates:
        if free_needed <= 0:
            break
        take = min(item[2], free_needed)
        discount += item[1] * take
        free_needed -= take

    return discount


# ── 主入口 ────────────────────────────────────────────────────

def evaluate_promotion(
    promo: Discount,
    tiers: list[DiscountTier],
    customer: Optional[Customer],
    lines: list[CartLine],
    subtotal: Decimal,
    customer_usage_count: int,
) -> PromoResult:
    """
    对单个促销规则进行条件校验 + 折扣计算。
    返回 PromoResult（applied=False 表示条件不满足）。
    """
    result = PromoResult(
        discount_id=promo.id,
        rule_type=promo.normalized_type,
        label=promo.name or promo.code or f"促销#{promo.id}",
    )

    reason = check_conditions(promo, customer, subtotal, lines, customer_usage_count)
    if reason:
        return result  # applied=False

    total_qty = sum(l.qty for l in lines)
    rule = promo.normalized_type

    if rule == "order_amount_off":
        result.amount = _eval_order_amount_off(promo, tiers, subtotal, total_qty)

    elif rule == "order_percent_off":
        result.amount = _eval_order_percent_off(promo, tiers, subtotal, total_qty)

    elif rule == "product_amount_off":
        result.amount = _eval_product_amount_off(promo, lines)

    elif rule == "category_percent_off":
        result.amount = _eval_category_percent_off(promo, lines)

    elif rule == "product_buyxgety":
        result.amount = _eval_product_buyxgety(promo, lines)

    elif rule == "free_shipping":
        result.free_shipping = True

    elif rule == "points_multiplier":
        result.points_multiplier = promo.points_multiplier or Decimal("1")

    # A condition match without an actual benefit must not consume `stop` or
    # non-stackable precedence. This matters when a target product is absent.
    result.applied = result.amount > 0 or result.free_shipping or result.points_multiplier > 1
    return result


def apply_promotions(
    promos_with_tiers: list[tuple[Discount, list[DiscountTier]]],
    customer: Optional[Customer],
    lines: list[CartLine],
    subtotal: Decimal,
    usage_counts: dict[int, int],
) -> list[PromoResult]:
    """
    按 priority 降序依次评估所有促销，处理叠加/互斥/stop 逻辑。

    promos_with_tiers: [(Discount, [DiscountTier, ...]), ...]，已按 priority 降序排列
    usage_counts:      {discount_id: 该客户已使用次数}
    """
    results: list[PromoResult] = []
    non_stackable_applied = False

    for promo, tiers in promos_with_tiers:
        # 非叠加检查：已有非叠加促销命中，则跳过其他非叠加促销
        if not promo.is_stackable and non_stackable_applied:
            continue

        count = usage_counts.get(promo.id, 0)
        res = evaluate_promotion(promo, tiers, customer, lines, subtotal, count)

        if res.applied:
            results.append(res)
            if not promo.is_stackable:
                non_stackable_applied = True
            if promo.stop:
                break

    return results
