"""前台订单路由 — 创建订单 / 顾客查看自己的订单

协议：
- 价格由后端 pricing.py 统一计算，不接受前端传入的最终价格
- 库存校验 + 扣减在下单事务中完成
- 支付成功后原子更新积分、订单状态
"""
import logging
import math
import secrets
import uuid
from datetime import datetime

logger = logging.getLogger(__name__)
from decimal import Decimal
from typing import List, Optional

from fastapi import APIRouter, Depends, HTTPException, Query
from pydantic import BaseModel, Field
from sqlalchemy import select, func
from sqlalchemy.ext.asyncio import AsyncSession

from app.api.deps import get_db, get_current_customer, get_optional_customer, get_tenant_by_appid_or_domain as get_tenant_by_domain
from app.core.models.customer import Customer
from app.core.models.member import MemberLevel, MemberPointsLedger
from app.core.models.order import Order, OrderItem
from app.core.models.product import Product
from app.core.models.discount import Discount
from app.core.models.discount_usage_log import DiscountUsageLog
from app.core.models.currency import Currency
from app.core.models.wallet import CustomerWallet, WalletTransaction

from app.core.services import pricing as pricing_svc
from app.core.services import inventory as inv_svc
from app.core.services.checkout_facts import (
    build_pricing_cart_items,
    delivery_extra,
    resolve_is_pickup,
    shipping_facts_address,
)
from app.core.signals import order_created, order_paid, order_status_changed
from app.core.models.tenant_settings import TenantSettings
from app.services.email import build_smtp_config, send_notification, build_items_table, _format_extra_fields_html
from app.core.services.shipping_calculator import ShippingCalculator
from app.config import settings as _settings


router = APIRouter(prefix="/store/orders", tags=["前台订单"])


# ── Schemas ────────────────────────────────────────────────────────────────

class CartItemIn(BaseModel):
    product_id: int = Field(..., ge=1)
    qty: Decimal = Field(Decimal("0.01"), gt=0, decimal_places=2)
    variant_id: Optional[int] = Field(None, ge=1)
    unit_name: Optional[str] = None  # selling unit name; None = buy in base units


class CreateOrderIn(BaseModel):
    items: List[CartItemIn]
    recv_name: str
    recv_phone: str
    recv_email: str = ""
    recv_country: str = ""
    recv_zip_code: str = ""
    recv_province: str
    recv_city: str
    recv_district: Optional[str] = None
    recv_addr: str
    recv_extra_fields: dict = {}
    shipping_method_id: Optional[int] = None
    purchase_order_number: Optional[str] = None
    delivery_type: Optional[int] = None
    delivery_mode: Optional[str] = Field(None, description="高级运费：配送模式标识（standard/express），自提由 delivery_type 决定")
    advanced_quote: Optional[dict] = Field(None, description="高级运费：所选报价的标识（shipping_method_id + quote_group），金额一律服务端重算")
    pay_method: str = "alipay"
    wallet_amount: Decimal = Field(Decimal("0"), ge=0, description="使用余额金额（0=不使用）")
    coupon_code: Optional[str] = None
    points_to_use: int = Field(0, ge=0)
    remark: Optional[str] = None
    display_currency_code: Optional[str] = Field(None, description="顾客选择的展示货币代码（如 NZD），不传则使用基准货币")


class OrderCancelIn(BaseModel):
    reason: Optional[str] = None


class OrderItemOut(BaseModel):
    id: int
    product_id: Optional[int] = None
    product_name: str
    quantity: Decimal
    unit_price: float
    total_price: float
    variant_id: Optional[int] = None
    cover_url: Optional[str] = None
    variant_text: Optional[str] = None
    unit_name: Optional[str] = None
    product_snapshot: Optional[dict] = None

    model_config = {"from_attributes": True}


class OrderListOut(BaseModel):
    id: int
    order_no: str
    status: str
    total: float
    created_at: Optional[str] = None
    items_count: int = 0

    model_config = {"from_attributes": True}


class OrderDetailOut(BaseModel):
    id: int
    order_no: str
    status: str
    subtotal: float
    discount_total: float
    shipping_total: float
    grand_total: float
    member_discount: float = 0.0
    wallet_amount: float = 0.0
    payable_total: float = 0.0
    created_at: Optional[str] = None
    paid_at: Optional[str] = None
    items: List[OrderItemOut] = []
    shipping_address: Optional[dict] = None
    purchase_order_number: Optional[str] = None
    delivery_type: Optional[int] = None
    pickup_code: Optional[str] = None
    shipping_method_id: Optional[int] = None
    note: Optional[str] = None
    coupon_code: Optional[str] = None
    coupon_label: Optional[str] = None
    pay_method: Optional[str] = None
    promotions: List[dict] = []
    points_used: int = 0
    carrier: Optional[str] = None
    tracking_no: Optional[str] = None
    estimated_delivery: Optional[str] = None
    shipments: List[dict] = []
    adjustments: List[dict] = []


def build_order_extra_attributes(
    *, body, pricing, coupon_obj, is_pickup: bool, purchase_order_number: str,
    delivery_type, quote_snapshot: Optional[dict], snapshot_required: bool,
) -> dict:
    """组装 Order.extra_attributes。**在 db.add(order) 之前调用**：

    强制模式下报价快照缺失就在这里抛错，订单还没写库，等于整单回滚——
    绝不允许出现"按高级运费收了钱、却查不到当时按哪条规则算的"订单。
    """
    extra: dict = {
        "pay_method": body.pay_method,
        "coupon_code": body.coupon_code,
        "coupon_label": _coupon_label(coupon_obj),
        "member_discount": float(pricing.member_discount),
        "promotions": [
            {"id": r.discount_id, "label": r.label, "amount": float(r.amount)}
            for r in pricing.promotions_applied
            if r.applied
        ],
        "points_used": body.points_to_use,
    }
    if purchase_order_number:
        extra["purchase_order_number"] = purchase_order_number
    extra.update(delivery_extra(delivery_type=delivery_type, is_pickup=is_pickup))

    from app.plugins.advanced_shipping_rules.store_quote import attach_quote_snapshot

    return attach_quote_snapshot(extra, snapshot=quote_snapshot,
                                 snapshot_required=snapshot_required)


def _coupon_label(coupon_obj) -> Optional[str]:
    """根据优惠券对象生成展示名称，无优惠券时返回 None。"""
    if not coupon_obj:
        return None
    code = coupon_obj.code or ""
    t = coupon_obj.type
    v = coupon_obj.value
    if t == "percentage":
        return f"{code}（{int(100 - v)}% 折扣）"
    if t == "fixed":
        return f"{code}（减 ¥{v:.2f}）"
    if t == "free_shipping":
        return f"{code}（免运费）"
    return code


# ── 创建订单 ──────────────────────────────────────────────────────────────

@router.post("", summary="创建订单（需顾客登录）")
async def create_order(
    body: CreateOrderIn,
    tid: int = Depends(get_tenant_by_domain),
    customer: Customer = Depends(get_current_customer),
    db: AsyncSession = Depends(get_db),
):
    tenant_id = tid
    if not body.items:
        raise HTTPException(status_code=400, detail="订单商品不能为空")

    # 1. 构造 PricingCartItem（含单位换算）
    from app.core.services.plugin_helper import is_plugin_active

    unit_split_active = await is_plugin_active("unit_split", db, tenant_id)
    # 到店取货：运费为 0，核销码在下方生成（插件启用时）
    is_pickup = await resolve_is_pickup(db, tenant_id, body.delivery_type)

    cart_items = await build_pricing_cart_items(db, tenant_id, body.items,
                                                unit_split_active=unit_split_active)

    # 2. 调用定价服务计算价格（含运费）
    #    高级运费插件未启用 / 影子模式时，quote_checkout_pricing 原样返回 legacy 定价结果。
    from app.plugins.advanced_shipping_rules.store_quote import quote_checkout_pricing

    quote = await quote_checkout_pricing(
        db, tenant_id,
        customer=customer,
        items=cart_items,
        address=shipping_facts_address(body),
        is_pickup=is_pickup,
        coupon_code=body.coupon_code,
        points_to_use=body.points_to_use,
        shipping_method_id=body.shipping_method_id,
        delivery_mode=body.delivery_mode,
        selection=body.advanced_quote,
        payment_method=body.pay_method,
        require_quote=True, record_shadow=True,
    )
    pricing = quote.pricing

    # 2b. 获取手动输入优惠券对象（仅用于生成展示标签）
    coupon_obj = None
    if body.coupon_code:
        cr = await db.execute(
            select(Discount).where(
                Discount.tenant_id == tenant_id,
                Discount.code == body.coupon_code.upper(),
            )
        )
        coupon_obj = cr.scalar_one_or_none()

    # 3. 校验库存（含 variant）
    await inv_svc.validate_stock(db=db, tenant_id=tenant_id, items=cart_items)

    # 4. 构建收货地址快照
    recv_extra_fields = dict(body.recv_extra_fields or {})
    purchase_order_number = (body.purchase_order_number or "").strip()
    delivery_type = body.delivery_type

    shipping_addr = {
        "name": body.recv_name,
        "phone": body.recv_phone,
        "email": body.recv_email,
        "country": body.recv_country,
        "zip_code": body.recv_zip_code,
        "province": body.recv_province,
        "city": body.recv_city,
        "district": body.recv_district or "",
        "address": body.recv_addr,
        "extra_fields": recv_extra_fields,
    }

    # 5. 生成订单号
    _date_str = datetime.now().strftime('%Y%m%d')
    order_no = f"ORD-{_date_str}-{uuid.uuid4().hex[:6].upper()}"

    # 6. 查询默认货币
    _cur_result = await db.execute(
        select(Currency.code).where(Currency.tenant_id == tenant_id, Currency.is_default == 1).limit(1)
    )
    _default_currency = _cur_result.scalar_one_or_none() or "NZD"

    # 6b. 查询顾客选择的展示货币
    _display_cur = None
    if body.display_currency_code and body.display_currency_code != _default_currency:
        _dcr = await db.execute(
            select(Currency).where(
                Currency.tenant_id == tenant_id,
                Currency.code == body.display_currency_code,
                Currency.is_active == True,
            ).limit(1)
        )
        _display_cur = _dcr.scalar_one_or_none()

    # 7. 创建 Order（快照缺失会在这里抛错，订单尚未写库）
    extra_attributes = build_order_extra_attributes(
        body=body, pricing=pricing, coupon_obj=coupon_obj, is_pickup=is_pickup,
        purchase_order_number=purchase_order_number, delivery_type=delivery_type,
        quote_snapshot=quote.snapshot, snapshot_required=quote.snapshot_required,
    )

    # 自动从客户 profile 填写账单地址
    _ed = customer.extra_data or {}
    _billing_address = None
    if any(_ed.get(k) for k in ("bill_company", "bill_line1", "bill_city", "vat_number")):
        _billing_address = {
            "company":     _ed.get("bill_company") or _ed.get("company") or "",
            "line1":       _ed.get("bill_line1") or "",
            "line2":       _ed.get("bill_line2") or "",
            "city":        _ed.get("bill_city") or "",
            "state":       _ed.get("bill_state") or "",
            "postal_code": _ed.get("bill_postal_code") or "",
            "country":     _ed.get("bill_country") or "",
            "vat_number":  _ed.get("vat_number") or "",
        }

    order = Order(
        tenant_id=tenant_id,
        customer_id=customer.id,
        order_no=order_no,
        status="pending",
        subtotal=pricing.subtotal,
        discount_total=pricing.coupon_discount + pricing.points_discount,
        shipping_total=pricing.shipping_total,
        tax_total=pricing.tax_total,
        grand_total=pricing.grand_total,
        currency=_default_currency,
        display_currency=_display_cur.code if _display_cur else None,
        display_currency_symbol=_display_cur.symbol if _display_cur else None,
        display_exchange_rate=_display_cur.exchange_rate if _display_cur else None,
        display_grand_total=Decimal(str(round(float(pricing.grand_total) * float(_display_cur.exchange_rate), 2))) if _display_cur else None,
        shipping_address=shipping_addr,
        billing_address=_billing_address,
        # 强制模式下方案由引擎选定；没有快照（插件关闭/影子/自提）才用客户端选的那个
        shipping_method_id=quote.snapshot["shipping_method_id"] if quote.snapshot else body.shipping_method_id,
        note=body.remark,
        extra_attributes=extra_attributes,
    )
    db.add(order)
    await db.flush()  # 获取 order.id

    # 7. 创建 OrderItem（含快照）并扣库存
    tax_by_product = {
        (td.product_id, td.variant_id): (td.tax_rate, td.tax_amount)
        for td in pricing.tax_details
    }
    for line in pricing.lines:
        # Find the original CartItemIn matching this line
        orig_ci = next((c for c in body.items if c.product_id == line.product_id), None)
        snapshot = {
            "name": line.name,
            "sku": line.sku,
            "cover": line.snapshot.get("cover"),
            "attributes": line.snapshot.get("attributes", {}),
            "variant_text": line.snapshot.get("variant_text", ""),
            "unit_price": float(line.unit_price),
        }
        if line.rule_snapshot:
            # 写盘：订单项快照留命中规则摘要，便于事后审计与展示
            snapshot["price_rule"] = line.rule_snapshot
        if orig_ci and orig_ci.unit_name and unit_split_active:
            snapshot["display_unit_name"] = orig_ci.unit_name
            snapshot["display_qty"] = orig_ci.qty
        tax_rate, tax_amount = tax_by_product.get(
            (line.product_id, line.variant_id), (Decimal("0"), Decimal("0"))
        )
        oi = OrderItem(
            order_id=order.id,
            tenant_id=tenant_id,
            product_id=line.product_id,
            variant_id=line.variant_id,
            product_snapshot=snapshot,
            quantity=line.qty,
            unit_price=line.unit_price,
            total_price=line.line_total,
            tax_rate=tax_rate,
            tax_amount=tax_amount,
        )
        db.add(oi)

    # 8. 扣减库存（按租户配置决定是否在 pending 状态扣）
    _ts_r = await db.execute(select(TenantSettings).where(TenantSettings.tenant_id == tenant_id))
    _ts = _ts_r.scalar_one_or_none()
    _deduct_statuses = inv_svc.get_deduct_statuses(_ts.extra if _ts else None)
    # 接管租户「下单即锁库」由库存状态机自决，不受遗留 deduct_statuses 配置影响
    if await inv_svc.is_inventory_takeover(db, tenant_id) or "pending" in _deduct_statuses:
        await inv_svc.deduct_stock(
            db=db,
            tenant_id=tenant_id,
            items=cart_items,
            order_id=order.id,
        )

    # 9. 积分扣减（从用户账户扣）
    if body.points_to_use > 0:
        customer.points_balance -= body.points_to_use

    # 9c. 余额支付 / 余额抵扣
    wallet_used = Decimal("0")
    if body.wallet_amount > Decimal("0") or body.pay_method == "balance":
        wallet_r = await db.execute(
            select(CustomerWallet).where(CustomerWallet.customer_id == customer.id)
        )
        wallet = wallet_r.scalar_one_or_none()
        available = wallet.balance if wallet else Decimal("0")

        if body.pay_method == "balance":
            # 全额余额支付
            wallet_used = pricing.grand_total
        else:
            # 部分余额抵扣
            wallet_used = min(body.wallet_amount, pricing.grand_total)

        if available < wallet_used:
            raise HTTPException(status_code=400, detail=f"余额不足（可用：{available}，需要：{wallet_used}）")

        if wallet:
            wallet.balance -= wallet_used
        new_bal = (wallet.balance if wallet else Decimal("0"))

        db.add(WalletTransaction(
            tenant_id=customer.tenant_id,
            customer_id=customer.id,
            amount=-wallet_used,
            balance_after=new_bal,
            type="order_pay",
            source_type="order",
            source_id=order.id,
            note=f"订单支付 {order_no}",
        ))

        # 余额抵扣金额写入订单（供后续 Latipay 等支付接口计算实收金额）
        order.set_attribute("wallet_amount", float(wallet_used))

        # 余额全额支付时，订单直接变为已付款
        if body.pay_method == "balance":
            order.status = "paid"
            order.paid_at = datetime.now()  # type: ignore[assignment]

    # 9b. 所有已命中促销：更新 used_count + 写使用日志
    applied_promos = [r for r in pricing.promotions_applied if r.applied]
    if applied_promos:
        applied_ids = [r.discount_id for r in applied_promos]
        dr = await db.execute(
            select(Discount).where(Discount.id.in_(applied_ids))
        )
        discount_map = {d.id: d for d in dr.scalars().all()}
        for pr in applied_promos:
            disc = discount_map.get(pr.discount_id)
            if disc:
                disc.used_count = (disc.used_count or 0) + 1
            db.add(DiscountUsageLog(
                tenant_id=tenant_id,
                discount_id=pr.discount_id,
                customer_id=customer.id,
                order_id=order.id,
                discount_amount=pr.amount,   # 多券叠加时按券拆分，报表才能算准单券抵扣
            ))

    await db.commit()
    await db.refresh(order)

    # 发送订单确认邮件
    try:
        if customer.email:
            ts_r = await db.execute(select(TenantSettings).where(TenantSettings.tenant_id == tenant_id))
            ts = ts_r.scalar_one_or_none()

            # 查租户默认语言
            from app.services.email import _get_tenant_default_locale, get_email_template as _get_email_tpl
            default_locale = await _get_tenant_default_locale(db, tenant_id)
            # 探查实际会用的模板语言，确保表头与邮件正文语言一致
            table_locale = default_locale

            # 查默认货币符号
            _cur_sym_r = await db.execute(
                select(Currency.symbol).where(Currency.tenant_id == tenant_id, Currency.is_default == 1).limit(1)
            )
            _sym = _cur_sym_r.scalar_one_or_none() or ""

            # 查默认税种名称
            from app.plugins.tax.models import TaxClass as _TaxClass
            _tax_name_r = await db.execute(
                select(_TaxClass.name).where(_TaxClass.tenant_id == tenant_id, _TaxClass.is_default == 1).limit(1)
            )
            _tax_name = _tax_name_r.scalar_one_or_none() or "Tax"

            def _fmt(amount) -> str:
                return f"{_sym}{float(amount):.2f}"

            items_for_email = [
                {
                    "name":     line.name + (f" / {line.snapshot.get('variant_text')}" if line.snapshot.get("variant_text") else ""),
                    "variant":  line.snapshot.get("sku", ""),
                    "quantity": line.qty,
                    "price":    _fmt(line.unit_price),
                }
                for line in pricing.lines
            ]
            extra_vars = {
                k: (str(v) if not isinstance(v, (list, dict)) else "")
                for k, v in (order.extra_attributes or {}).items()
            }
            field_defs = (ts.extra or {}).get("address_custom_fields", []) if ts else []
            addr_extra_vars = {
                "recv_extra_fields": _format_extra_fields_html(recv_extra_fields, field_defs),
                **{f"recv_field_{f.get('key')}": str(recv_extra_fields.get(f.get("key"), "") or "") for f in field_defs if f.get("key")},
            }
            from app.core.services.customer_fields import normalize_fields, build_customer_email_vars as _build_cvars
            _cpf_fields = normalize_fields((ts.extra or {}).get("customer_profile_fields") or [] if ts else [])
            _customer_email_vars = _build_cvars(customer.extra_data or {}, _cpf_fields)
            from app.tasks.email_tasks import send_notification_task
            adjustments = (order.extra_attributes or {}).get("adjustments", []) if isinstance(order.extra_attributes, dict) else []
            send_notification_task.delay(
                tenant_id=tenant_id,
                template_key="order_created",
                to_email=customer.email,
                variables={
                    **extra_vars,
                    **addr_extra_vars,
                    **_customer_email_vars,
                    "name":             customer.name,
                    "order_no":         order_no,
                    "subtotal":         _fmt(pricing.subtotal),
                    "tax_total":        _fmt(pricing.tax_total) if pricing.tax_total else "",
                    "shipping_total":   _fmt(pricing.shipping_total) if pricing.shipping_total else "",
                    "tax_line":         f"<p>{_tax_name}: {_fmt(pricing.tax_total)}</p>" if pricing.tax_total else "",
                    "shipping_line":    f"<p>Shipping: {_fmt(pricing.shipping_total)}</p>" if pricing.shipping_total else "",
                    "total":            _fmt(order.grand_total),
                    "prices_include_tax": "1" if pricing.prices_include_tax else "",
                    "purchase_order_number": purchase_order_number or "",
                    "adjustments":      adjustments,
                    "items_table":      build_items_table(
                        items_for_email,
                        adjustments=adjustments,
                        locale=table_locale,
                        currency_symbol=_sym,
                    ),
                    # Keep every address value independent so each template controls its own order and layout.
                    "shipping_address": shipping_addr["address"],
                    "recv_street":      shipping_addr["address"],
                    "recv_district":    shipping_addr["district"],
                    "recv_city":        shipping_addr["city"],
                    "recv_state":       shipping_addr["province"],
                    "recv_postcode":    shipping_addr["zip_code"],
                    "recv_country":     shipping_addr["country"],
                    "recv_email":       shipping_addr["email"],
                    "store_name":       ts.store_name if ts else "SME Store",
                    "note":             order.note or "",
                },
                smtp_config=build_smtp_config(ts),
            )
    except Exception:
        pass

    # 10. 发送 order_created 信号
    order_created.send(sender=Order, order=order, tenant_id=tenant_id)

    return {
        "id": order.id,
        "order_no": order.order_no,
        "status": order.status,
        "grand_total": float(order.grand_total),
        "subtotal": float(order.subtotal),
        "shipping_total": float(order.shipping_total),
        "discount_total": float(order.discount_total),
        "adjustments": [
            {"type": adj.type, "label": adj.label, "amount": float(adj.amount)}
            for adj in pricing.adjustments
        ],
        "created_at": order.created_at.isoformat() if order.created_at else None,
    }


# ── 订单价格预览 ───────────────────────────────────────────────────────────

class PreviewIn(BaseModel):
    items: List[CartItemIn]
    coupon_code: Optional[str] = None
    points_to_use: int = Field(0, ge=0)
    country: str = ""
    province: str = ""
    # 新增字段全部有默认值：老调用方（购物车抽屉、cart 页）不传也照常工作。
    # 但运费规则可以按郊区/邮编收附加费，不传就会出现"预览便宜、下单变贵"。
    city: str = ""
    district: str = ""
    zip_code: str = ""
    shipping_method_id: Optional[int] = None
    delivery_type: Optional[int] = None
    delivery_mode: Optional[str] = None
    pay_method: Optional[str] = None
    advanced_quote: Optional[dict] = None


@router.post("/preview", summary="订单价格预览")
async def preview_order(
    body: PreviewIn,
    tid: int = Depends(get_tenant_by_domain),
    customer: Optional[Customer] = Depends(get_optional_customer),
    db: AsyncSession = Depends(get_db),
):
    eff_tenant = tid

    from app.core.services.plugin_helper import is_plugin_active
    _is_pickup = await resolve_is_pickup(db, eff_tenant, body.delivery_type)

    # 与下单同一套单位换算：预览少换算一次，unit_split 租户会被预览一个价、下单另一个价
    cart_items = await build_pricing_cart_items(
        db, eff_tenant, body.items,
        unit_split_active=await is_plugin_active("unit_split", db, eff_tenant),
    )

    # 与下单同一个入口、同一套地址事实：预览报的价就是下单要收的价
    from app.plugins.advanced_shipping_rules.store_quote import quote_checkout_pricing

    quote = await quote_checkout_pricing(
        db, eff_tenant,
        customer=customer,
        items=cart_items,
        address=shipping_facts_address(body, prefix=""),   # PreviewIn 用裸字段名
        is_pickup=_is_pickup,
        coupon_code=body.coupon_code,
        points_to_use=body.points_to_use,
        shipping_method_id=body.shipping_method_id,
        delivery_mode=body.delivery_mode,
        selection=body.advanced_quote,
        payment_method=body.pay_method,
        # 预览：只读，没有地址时不强制（购物车抽屉/aqua/b2b 都只传 items），也不记影子事件
        require_quote=False, record_shadow=False,
    )
    pricing = quote.pricing

    # 如果提供了地址，返回所有可用运费方案供用户选择
    # 强制模式下运费只能来自高级规则引擎，这里绝不再列 legacy 方案：
    # 列出来客户也选不了（下单会被判 stale_quote），只会看到"预览有、下单没有"的方案。
    shipping_methods: list[dict] = []
    if body.country and quote.mode != "enforced":   # 强制租户（含自提）一律不列 legacy 方案
        try:
            calc = ShippingCalculator(db, eff_tenant)
            zones = await calc._load_zones()
            matched_zone = calc._match_zone(zones, body.country, body.province or "")
            logger.info(
                "preview shipping: tenant=%s country=%s province=%s zones=%d matched_zone=%s",
                eff_tenant, body.country, body.province,
                len(zones), matched_zone.id if matched_zone else None,
            )
            options = await calc.get_available_methods(
                country=body.country,
                province=body.province,
                subtotal=pricing.subtotal,
                total_weight_kg=pricing.total_weight_kg,
                total_items=pricing.total_items,
            )
            logger.info(
                "preview shipping: methods_found=%d weight_kg=%s subtotal=%s shipping_total=%s",
                len(options), pricing.total_weight_kg, pricing.subtotal, pricing.shipping_total,
            )
            shipping_methods = [
                {
                    "method_id": o.method_id,
                    "name": o.name,
                    "carrier_name": o.carrier_name,
                    "estimated_days": o.estimated_days,
                    "fee": float(o.fee),
                    "is_free": o.is_free,
                }
                for o in options
            ]
        except Exception:
            logger.warning("get_available_methods failed in preview", exc_info=True)

    return {
        "lines": [
            {
                "product_id": line.product_id,
                "variant_id": line.variant_id,
                "unit_name": body.items[index].unit_name if index < len(body.items) else None,
                "name": line.name,
                "sku": line.sku,
                "qty": line.qty,
                "unit_price": float(line.unit_price),
                "line_total": float(line.line_total),
                "snapshot": line.snapshot,
            }
            for index, line in enumerate(pricing.lines)
        ],
        "subtotal": float(pricing.subtotal),
        "member_discount": float(pricing.member_discount),
        "coupon_discount": float(pricing.coupon_discount),
        "points_discount": float(pricing.points_discount),
        "shipping_total": float(pricing.shipping_total),
        "tax_total": float(pricing.tax_total),
        "prices_include_tax": pricing.prices_include_tax,
        "grand_total": float(pricing.grand_total),
        "shipping_methods": shipping_methods,
        # 高级运费候选（强制模式才有）。展示文案与选中态由 Task 8 处理，这里只给结构化数据。
        "advanced_shipping": None if quote.mode != "enforced" else {
            "mode": quote.mode,
            "reason_keys": list(quote.reason_keys),
            "quotes": [
                {
                    "shipping_method_id": q.shipping_method_id,
                    "quote_group": q.quote_group,
                    "delivery_mode": q.delivery_mode,
                    "fee": float(q.fee),
                    "currency": q.currency,
                    "title_map": dict(q.title_map),
                    "description_map": dict(q.description_map),
                }
                for q in quote.quotes
            ],
        },
        "adjustments": [
            {"type": adj.type, "label": adj.label, "amount": float(adj.amount)}
            for adj in pricing.adjustments
        ],
    }


# ── 我的订单列表 ───────────────────────────────────────────────────────────

@router.get("", summary="我的订单（需顾客登录）")
async def my_orders(
    page: int = Query(1, ge=1),
    page_size: int = Query(10, ge=1, le=200),
    status: Optional[str] = None,
    tid: int = Depends(get_tenant_by_domain),
    customer: Customer = Depends(get_current_customer),
    db: AsyncSession = Depends(get_db),
):
    tenant_id = tid
    filters = [Order.customer_id == customer.id, Order.tenant_id == tenant_id]
    if status:
        filters.append(Order.status == status)

    count_q = select(func.count(Order.id)).where(*filters)
    total = (await db.execute(count_q)).scalar() or 0

    q = (
        select(Order)
        .where(*filters)
        .order_by(Order.created_at.desc())
        .offset((page - 1) * page_size)
        .limit(page_size)
    )
    orders = (await db.execute(q)).scalars().all()

    order_ids = [o.id for o in orders]
    # 批量加载所有订单的商品明细
    all_items: dict[int, list] = {o.id: [] for o in orders}
    if order_ids:
        ir = await db.execute(
            select(OrderItem).where(OrderItem.order_id.in_(order_ids))
        )
        for oi in ir.scalars().all():
            all_items[oi.order_id].append(_item_out(oi))

    return {
        "total": total,
        "page": page,
        "page_size": page_size,
        "items": [
            {
                "id": o.id,
                "order_no": o.order_no,
                "status": o.status,
                "total": float(o.grand_total or 0),
                "grand_total": float(o.grand_total or 0),
                "created_at": o.created_at.isoformat() if o.created_at else None,
                "items_count": len(all_items[o.id]),
                "items": [item.model_dump() for item in all_items[o.id]],
            }
            for o in orders
        ],
    }


# ── 订单详情（按 order_no）──────────────────────────────────────────────────

@router.get("/{order_no}", summary="订单详情（按订单号）")
async def order_detail(
    order_no: str,
    tid: int = Depends(get_tenant_by_domain),
    customer: Customer = Depends(get_current_customer),
    db: AsyncSession = Depends(get_db),
):
    tenant_id = tid
    r = await db.execute(
        select(Order).where(
            Order.order_no == order_no,
            Order.customer_id == customer.id,
            Order.tenant_id == tenant_id,
        )
    )
    order = r.scalar_one_or_none()
    if not order:
        raise HTTPException(status_code=404, detail="订单不存在")

    ir = await db.execute(select(OrderItem).where(OrderItem.order_id == order.id))
    order_items = ir.scalars().all()

    return _order_to_detail(order, order_items)


# ── 订单详情（按 order_id） — 合并自 orders_extended ───────────────────────

@router.get("/id/{order_id}", summary="订单详情（按 ID）")
async def get_order_by_id(
    order_id: int,
    tid: int = Depends(get_tenant_by_domain),
    customer: Customer = Depends(get_current_customer),
    db: AsyncSession = Depends(get_db),
):
    tenant_id = tid
    r = await db.execute(
        select(Order).where(
            Order.id == order_id,
            Order.customer_id == customer.id,
            Order.tenant_id == tenant_id,
        )
    )
    order = r.scalar_one_or_none()
    if not order:
        raise HTTPException(status_code=404, detail="订单不存在")

    ir = await db.execute(select(OrderItem).where(OrderItem.order_id == order.id))
    order_items = ir.scalars().all()

    return _order_to_detail(order, order_items)


def _item_out(oi: OrderItem) -> OrderItemOut:
    snap = oi.product_snapshot or {}
    variant_text = snap.get("variant_text", "")
    return OrderItemOut(
        id=oi.id,
        product_id=oi.product_id,
        product_name=snap.get("name", f"商品#{oi.product_id}"),
        quantity=oi.quantity,
        unit_price=float(oi.unit_price or 0),
        total_price=float(oi.total_price or 0),
        variant_id=oi.variant_id,
        cover_url=snap.get("cover"),
        variant_text=variant_text,
        unit_name=snap.get("display_unit_name"),
        product_snapshot={
            "name": snap.get("name", f"商品#{oi.product_id}"),
            "cover": snap.get("cover"),
            "variant": variant_text,
            "sku": snap.get("sku", ""),
            "attributes": snap.get("attributes", {}),
            "display_unit_name": snap.get("display_unit_name"),
        },
    )


def _order_to_detail(order: Order, order_items) -> OrderDetailOut:
    extra = order.extra_attributes or {}
    grand_total     = float(order.grand_total or 0)
    wallet_amount   = float(extra.get("wallet_amount") or 0)
    payable_total   = round(max(0.0, grand_total - wallet_amount), 2)
    member_discount = float(extra.get("member_discount") or 0)
    return OrderDetailOut(
        id=order.id,
        order_no=order.order_no,
        status=order.status,
        subtotal=float(order.subtotal or 0),
        discount_total=float(order.discount_total or 0),
        shipping_total=float(order.shipping_total or 0),
        grand_total=grand_total,
        member_discount=member_discount,
        wallet_amount=wallet_amount,
        payable_total=payable_total,
        created_at=order.created_at.isoformat() if order.created_at else None,
        paid_at=order.paid_at.isoformat() if order.paid_at else None,
        items=[_item_out(oi) for oi in order_items],
        shipping_address=order.shipping_address,
        purchase_order_number=extra.get("purchase_order_number"),
        delivery_type=extra.get("delivery_type"),
        pickup_code=extra.get("pickup_code"),
        shipping_method_id=order.shipping_method_id,
        note=order.note,
        coupon_code=extra.get("coupon_code"),
        coupon_label=extra.get("coupon_label") or extra.get("coupon_code"),
        pay_method=extra.get("pay_method"),
        promotions=extra.get("promotions") or [],
        points_used=extra.get("points_used") or 0,
        carrier=order.carrier,
        tracking_no=order.tracking_no,
        estimated_delivery=str(order.estimated_delivery) if order.estimated_delivery else None,
        shipments=extra.get("shipments") or [],
        adjustments=extra.get("adjustments") or [],
    )


# ── 取消订单 — 合并自 orders_extended ──────────────────────────────────────

@router.post("/{order_no}/cancel", summary="取消订单（按订单号，仅 pending）")
async def cancel_order_by_no(
    order_no: str,
    body: OrderCancelIn,
    tid: int = Depends(get_tenant_by_domain),
    customer: Customer = Depends(get_current_customer),
    db: AsyncSession = Depends(get_db),
):
    tenant_id = tid
    r = await db.execute(
        select(Order).where(
            Order.order_no == order_no,
            Order.customer_id == customer.id,
            Order.tenant_id == tenant_id,
        ).with_for_update()
    )
    order = r.scalar_one_or_none()
    if not order:
        raise HTTPException(status_code=404, detail="订单不存在")
    return await _do_cancel_order(order, body.reason, customer, db)


@router.post("/id/{order_id}/cancel", summary="取消订单（按 ID，仅 pending）")
async def cancel_order_by_id(
    order_id: int,
    body: OrderCancelIn,
    tid: int = Depends(get_tenant_by_domain),
    customer: Customer = Depends(get_current_customer),
    db: AsyncSession = Depends(get_db),
):
    tenant_id = tid
    r = await db.execute(
        select(Order).where(
            Order.id == order_id,
            Order.customer_id == customer.id,
            Order.tenant_id == tenant_id,
        ).with_for_update()
    )
    order = r.scalar_one_or_none()
    if not order:
        raise HTTPException(status_code=404, detail="订单不存在")
    return await _do_cancel_order(order, body.reason, customer, db)


async def _do_cancel_order(
    order: Order,
    reason: Optional[str],
    customer: Customer,
    db: AsyncSession,
):
    if order.status != "pending":
        raise HTTPException(status_code=400, detail=f"当前状态「{order.status}」无法取消，仅能取消待付款订单")
    old_status = order.status
    order.status = "cancelled"
    if reason:
        order.extra_attributes = {**(order.extra_attributes or {}), "cancel_reason": reason}

    # 恢复库存（仅当旧状态属于扣库存状态时才恢复）
    _ts_r = await db.execute(select(TenantSettings).where(TenantSettings.tenant_id == order.tenant_id))
    _ts = _ts_r.scalar_one_or_none()
    _deduct_statuses = inv_svc.get_deduct_statuses(_ts.extra if _ts else None)
    if old_status in _deduct_statuses:
        ir = await db.execute(select(OrderItem).where(OrderItem.order_id == order.id))
        order_items = ir.scalars().all()
        restore_items = [
            {"product_id": oi.product_id, "variant_id": oi.variant_id, "qty": oi.quantity}
            for oi in order_items
        ]
        await inv_svc.restore_stock(db=db, tenant_id=order.tenant_id, items=restore_items, order_id=order.id)

    # 恢复积分
    points_used = (order.extra_attributes or {}).get("points_used", 0)
    if points_used:
        customer.points_balance += points_used

    await db.commit()

    order_status_changed.send(
        sender=Order,
        order=order,
        old_status=old_status,
        new_status="cancelled",
        tenant_id=order.tenant_id,
    )

    return {"message": "订单已取消", "order_no": order.order_no, "status": "cancelled"}


# ── 模拟付款 ──────────────────────────────────────────────────────────────

@router.post("/{order_no}/pay", summary="模拟支付（仅开发环境可用）")
async def pay_order(
    order_no: str,
    tid: int = Depends(get_tenant_by_domain),
    customer: Customer = Depends(get_current_customer),
    db: AsyncSession = Depends(get_db),
):
    if not _settings.DEBUG:
        raise HTTPException(status_code=404, detail="Not found")
    tenant_id = tid
    r = await db.execute(
        select(Order).where(
            Order.order_no == order_no,
            Order.customer_id == customer.id,
            Order.tenant_id == tenant_id,
        ).with_for_update()
    )
    order = r.scalar_one_or_none()
    if not order:
        raise HTTPException(status_code=404, detail="订单不存在")

    # 幂等：已经是 paid 直接返回
    if order.status == "paid":
        return {"message": "支付成功", "order_no": order.order_no, "status": "paid"}

    if order.status != "pending":
        raise HTTPException(status_code=400, detail=f"当前订单状态为「{order.status}」，无法付款")

    # 获取会员等级（含积分倍率）
    member_level: Optional[MemberLevel] = None
    if customer.member_level_id:
        ml_r = await db.execute(
            select(MemberLevel).where(MemberLevel.id == customer.member_level_id)
        )
        member_level = ml_r.scalar_one_or_none()

    points_multiplier = Decimal("1.0000")
    if member_level:
        points_multiplier = member_level.points_multiplier

    # 计算本次订单积分
    points_earned = int(math.floor(order.grand_total * points_multiplier))

    # 更新订单状态
    old_status = order.status
    order.status = "paid"
    order.paid_at = datetime.now()
    db.add(order)

    # 更新顾客积分和消费统计
    customer.points_balance += points_earned
    customer.total_spent += order.grand_total
    customer.orders_count += 1
    db.add(customer)

    # 写入积分流水
    ledger = MemberPointsLedger(
        tenant_id=order.tenant_id,
        customer_id=customer.id,
        order_id=order.id,
        change_amount=points_earned,
        balance_after=customer.points_balance,
        reason="订单消费",
        note=f"订单 {order.order_no}",
    )
    db.add(ledger)

    # 检查会员升级条件
    if member_level and member_level.upgrade_spend:
        if customer.total_spent >= member_level.upgrade_spend:
            # 查下一等级
            next_r = await db.execute(
                select(MemberLevel).where(
                    MemberLevel.tenant_id == order.tenant_id,
                    MemberLevel.rank > member_level.rank,
                    MemberLevel.is_active == 1,
                ).order_by(MemberLevel.rank.asc()).limit(1)
            )
            next_level = next_r.scalar_one_or_none()
            if next_level:
                customer.member_level_id = next_level.id

    await db.commit()

    order_paid.send(sender=Order, order=order, tenant_id=order.tenant_id)
    order_status_changed.send(
        sender=Order,
        order=order,
        old_status=old_status,
        new_status="paid",
        tenant_id=order.tenant_id,
    )

    return {
        "message": "支付成功",
        "order_no": order.order_no,
        "status": "paid",
        "points_earned": points_earned,
        "points_balance": customer.points_balance,
    }


# ── 确认收货 ──────────────────────────────────────────────────────────────

@router.post("/{order_no}/confirm", summary="确认收货（将 shipped 订单改为 completed）")
async def confirm_receipt(
    order_no: str,
    tid: int = Depends(get_tenant_by_domain),
    customer: Customer = Depends(get_current_customer),
    db: AsyncSession = Depends(get_db),
):
    tenant_id = tid
    r = await db.execute(
        select(Order).where(
            Order.order_no == order_no,
            Order.customer_id == customer.id,
            Order.tenant_id == tenant_id,
        )
    )
    order = r.scalar_one_or_none()
    if not order:
        raise HTTPException(status_code=404, detail="订单不存在")
    if order.status != "shipped":
        raise HTTPException(status_code=400, detail="订单尚未发货，无法确认收货")
    old_status = order.status
    order.status = "completed"
    db.add(order)
    await db.commit()
    order_status_changed.send(
        sender=Order,
        order=order,
        old_status=old_status,
        new_status="completed",
        tenant_id=order.tenant_id,
    )
    return {"message": "确认收货成功", "order_no": order.order_no, "status": "completed"}
