"""企业级运费计算引擎

支持计价方式：
  flat              固定运费
  weight            按实际重量
  dimensional_weight按体积重（取实重与体积重较大值）
  per_item          按件数
  order_value_tier  按订单金额阶梯

区域匹配逻辑（sort_order 升序）：
  1. 精确省份匹配（province_rules 中有该国家+省份）
  2. 国家匹配（countries 中有该国家）
  3. 全局兜底（is_all_countries=1）
"""
from __future__ import annotations

import math
from decimal import Decimal, ROUND_HALF_UP
from typing import Optional

from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import selectinload

from app.core.models.shipping import ShippingMethod
from app.core.models.shipping_carrier import ShippingCarrier
from app.core.models.shipping_zone import ShippingZone
from app.core.models.shipping_rate_tier import ShippingRateTier
from app.core.models.shipping_surcharge import ShippingSurcharge


class ShippingOption:
    def __init__(
        self,
        method_id: int,
        name: str,
        carrier_name: Optional[str],
        estimated_days: Optional[str],
        fee: Decimal,
        is_free: bool,
        pricing_method: str,
    ):
        self.method_id = method_id
        self.name = name
        self.carrier_name = carrier_name
        self.estimated_days = estimated_days
        self.fee = fee
        self.is_free = is_free
        self.pricing_method = pricing_method


class ShippingCalculator:
    def __init__(self, db: AsyncSession, tenant_id: int):
        self.db = db
        self.tenant_id = tenant_id

    async def get_available_methods(
        self,
        country: str,
        province: str = "",
        subtotal: Decimal = Decimal("0"),
        total_weight_kg: Decimal = Decimal("0"),
        total_items: int = 1,
        length_cm: Optional[Decimal] = None,
        width_cm: Optional[Decimal] = None,
        height_cm: Optional[Decimal] = None,
    ) -> list[ShippingOption]:
        """返回当前地址可用的所有运费方案（已计算运费）"""
        # 加载所有激活区域（按 sort_order）
        zones = await self._load_zones()
        matched_zone = self._match_zone(zones, country, province)

        # 加载方案：zone_id 匹配 OR zone_id 为 NULL（全局方案）
        methods = await self._load_methods(matched_zone.id if matched_zone else None)

        # 加载附加费
        surcharges = await self._load_surcharges()

        options: list[ShippingOption] = []
        for method in methods:
            # 最低起运金额检查
            if method.min_order_amount and subtotal < method.min_order_amount:
                continue
            # 最大重量检查
            if method.max_weight and total_weight_kg > method.max_weight:
                continue

            fee, is_free = self._calculate_fee(
                method=method,
                subtotal=subtotal,
                total_weight_kg=total_weight_kg,
                total_items=total_items,
                length_cm=length_cm,
                width_cm=width_cm,
                height_cm=height_cm,
            )

            if not is_free:
                fee = self._apply_surcharges(
                    fee=fee,
                    surcharges=surcharges,
                    method_id=method.id,
                    matched_zone_id=matched_zone.id if matched_zone else None,
                    total_weight_kg=total_weight_kg,
                )

            carrier_name: Optional[str] = None
            if method.carrier_obj:
                carrier_name = method.carrier_obj.name
            elif method.carrier:
                carrier_name = method.carrier

            options.append(ShippingOption(
                method_id=method.id,
                name=method.name,
                carrier_name=carrier_name,
                estimated_days=method.estimated_days,
                fee=fee,
                is_free=is_free or fee == Decimal("0"),
                pricing_method=method.pricing_method,
            ))

        return sorted(options, key=lambda o: o.fee)

    async def calculate_for_order(
        self,
        method_id: int,
        country: str,
        province: str = "",
        subtotal: Decimal = Decimal("0"),
        total_weight_kg: Decimal = Decimal("0"),
        total_items: int = 1,
        length_cm: Optional[Decimal] = None,
        width_cm: Optional[Decimal] = None,
        height_cm: Optional[Decimal] = None,
    ) -> Decimal:
        """为指定方案计算运费（下单时使用）"""
        result = await self.db.execute(
            select(ShippingMethod)
            .where(
                ShippingMethod.id == method_id,
                ShippingMethod.tenant_id == self.tenant_id,
                ShippingMethod.is_active == 1,
            )
            .options(selectinload(ShippingMethod.tiers), selectinload(ShippingMethod.carrier_obj))
        )
        method = result.scalar_one_or_none()
        if not method:
            raise ValueError(f"运费方案 {method_id} 不存在或未启用")

        zones = await self._load_zones()
        matched_zone = self._match_zone(zones, country, province)
        surcharges = await self._load_surcharges()

        fee, is_free = self._calculate_fee(
            method=method,
            subtotal=subtotal,
            total_weight_kg=total_weight_kg,
            total_items=total_items,
            length_cm=length_cm,
            width_cm=width_cm,
            height_cm=height_cm,
        )

        if not is_free:
            fee = self._apply_surcharges(
                fee=fee,
                surcharges=surcharges,
                method_id=method.id,
                matched_zone_id=matched_zone.id if matched_zone else None,
                total_weight_kg=total_weight_kg,
            )

        return Decimal("0") if is_free else fee

    # ── 内部方法 ──────────────────────────────────────────────

    def _calculate_fee(
        self,
        method: ShippingMethod,
        subtotal: Decimal,
        total_weight_kg: Decimal,
        total_items: int,
        length_cm: Optional[Decimal],
        width_cm: Optional[Decimal],
        height_cm: Optional[Decimal],
    ) -> tuple[Decimal, bool]:
        """返回 (fee, is_free)"""

        # 满额免运费（优先判断）
        if method.free_threshold and subtotal >= method.free_threshold:
            return Decimal("0"), True

        pm = method.pricing_method

        if pm == "flat":
            return self._round(method.base_fee), False

        elif pm == "weight":
            return self._calc_weight_fee(method, total_weight_kg), False

        elif pm == "dimensional_weight":
            dim_weight = self._dimensional_weight(
                length_cm, width_cm, height_cm, method.dimensional_divisor
            )
            billable = max(total_weight_kg, dim_weight)
            return self._calc_weight_fee(method, billable), False

        elif pm == "per_item":
            return self._calc_per_item_fee(method, total_items), False

        elif pm == "order_value_tier":
            return self._calc_tier_fee(method, subtotal), False

        else:
            return self._round(method.base_fee), False

    def _calc_weight_fee(self, method: ShippingMethod, weight_kg: Decimal) -> Decimal:
        """首 first_weight kg 收 base_fee，超出部分每 unit_step 加 unit_fee"""
        if weight_kg <= Decimal("0"):
            return self._round(method.base_fee)
        first_weight = getattr(method, "first_weight", Decimal("1")) or Decimal("1")
        first_weight = first_weight if first_weight > 0 else Decimal("1")
        step = method.unit_step if method.unit_step > 0 else Decimal("1")
        extra_units = max(Decimal("0"), weight_kg - first_weight)
        extra_charges = math.ceil(float(extra_units / step)) * float(method.unit_fee)
        return self._round(method.base_fee + Decimal(str(extra_charges)))

    def _calc_per_item_fee(self, method: ShippingMethod, total_items: int) -> Decimal:
        """首件 base_fee，超出部分每件加 unit_fee"""
        if total_items <= 0:
            return self._round(method.base_fee)
        extra = max(0, total_items - 1)
        return self._round(method.base_fee + method.unit_fee * extra)

    def _calc_tier_fee(self, method: ShippingMethod, subtotal: Decimal) -> tuple[Decimal, bool]:
        """按阶梯表查找匹配档位"""
        tiers = sorted(method.tiers or [], key=lambda t: t.sort_order)
        for tier in tiers:
            if subtotal < tier.min_value:
                continue
            if tier.max_value is not None and subtotal >= tier.max_value:
                continue
            if tier.is_free:
                return Decimal("0"), True
            return self._round(tier.fee), False
        # 无匹配阶梯，使用 base_fee 兜底
        return self._round(method.base_fee), False

    def _apply_surcharges(
        self,
        fee: Decimal,
        surcharges: list[ShippingSurcharge],
        method_id: int,
        matched_zone_id: Optional[int],
        total_weight_kg: Decimal,
    ) -> Decimal:
        total = fee
        for s in surcharges:
            if not s.is_active:
                continue
            # 检查是否应用到此方案
            if s.method_id is not None and s.method_id != method_id:
                continue
            # 检查条件
            if s.condition_type == "zone_match":
                if not matched_zone_id or matched_zone_id not in (s.condition_zone_ids or []):
                    continue
            elif s.condition_type == "weight_over":
                if s.condition_weight_kg is None or total_weight_kg <= s.condition_weight_kg:
                    continue

            if s.surcharge_type == "percentage":
                total += self._round(fee * s.value / Decimal("100"))
            else:
                total += self._round(s.value)

        return self._round(total)

    @staticmethod
    def _dimensional_weight(
        length_cm: Optional[Decimal],
        width_cm: Optional[Decimal],
        height_cm: Optional[Decimal],
        divisor: int,
    ) -> Decimal:
        if length_cm and width_cm and height_cm and divisor > 0:
            volume = length_cm * width_cm * height_cm
            return volume / Decimal(str(divisor))
        return Decimal("0")

    @staticmethod
    def _round(value: Decimal) -> Decimal:
        return value.quantize(Decimal("0.01"), rounding=ROUND_HALF_UP)

    async def _load_zones(self) -> list[ShippingZone]:
        result = await self.db.execute(
            select(ShippingZone)
            .where(ShippingZone.tenant_id == self.tenant_id, ShippingZone.is_active == 1)
            .order_by(ShippingZone.sort_order, ShippingZone.id)
        )
        return list(result.scalars().all())

    def _match_zone(self, zones: list[ShippingZone], country: str, province: str) -> Optional[ShippingZone]:
        country = (country or "").upper().strip()
        province = (province or "").strip()

        for zone in zones:
            if zone.is_all_countries:
                continue  # 兜底最后处理

            # 省份精确匹配
            if zone.province_rules and country in zone.province_rules:
                allowed = zone.province_rules[country]
                if isinstance(allowed, list) and province in allowed:
                    return zone

            # 国家匹配
            if zone.countries and country in zone.countries:
                return zone

        # 兜底
        for zone in zones:
            if zone.is_all_countries:
                return zone

        return None

    async def _load_methods(self, zone_id: Optional[int]) -> list[ShippingMethod]:
        from sqlalchemy import or_
        q = (
            select(ShippingMethod)
            .where(
                ShippingMethod.tenant_id == self.tenant_id,
                ShippingMethod.is_active == 1,
            )
            .options(
                selectinload(ShippingMethod.tiers),
                selectinload(ShippingMethod.carrier_obj),
            )
            .order_by(ShippingMethod.sort_order, ShippingMethod.id)
        )
        if zone_id is not None:
            q = q.where(or_(ShippingMethod.zone_id == zone_id, ShippingMethod.zone_id.is_(None)))
        else:
            q = q.where(ShippingMethod.zone_id.is_(None))
        result = await self.db.execute(q)
        return list(result.scalars().all())

    async def _load_surcharges(self) -> list[ShippingSurcharge]:
        result = await self.db.execute(
            select(ShippingSurcharge)
            .where(ShippingSurcharge.tenant_id == self.tenant_id, ShippingSurcharge.is_active == 1)
            .order_by(ShippingSurcharge.sort_order)
        )
        return list(result.scalars().all())
