"""结算服务：购物车 → 扣余额 → 写订单 → 写实例 → 触发开通。"""
import logging
import random
import string
from datetime import datetime, timezone

from fastapi import HTTPException
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession

from app.models.balance_log import BalanceChangeType, BalanceLog
from app.models.cart_item import CartItem
from app.models.instance import Instance, InstanceStatus
from app.models.order import Order, OrderStatus
from app.models.product import Product, ProductApiType, ProductStatus
from app.models.product_group import ProductGroup
from app.models.product_pricing import ProductPricing
from app.models.user import User
from app.services import coupon_service, membership_service, provision_plugin_service, spend_service, stock_compensation_service, verification_service
from app.utils.provision_params import normalize_upstream_value
from app.utils.specs import parse_memory_to_mb, sync_provision_config_specs

log = logging.getLogger("qycw.order")


def _gen_order_no() -> str:
    ts = datetime.now(timezone.utc).strftime("%Y%m%d%H%M%S")
    suffix = "".join(random.choices(string.digits, k=6))
    return f"ORD{ts}{suffix}"


def _config_value(provision_config: list | None, slider_values: dict, key: str):
    if key in slider_values:
        return normalize_upstream_value(slider_values[key])
    for field in provision_config or []:
        if not isinstance(field, dict):
            continue
        field_key = str(field.get("def", "")).split("|", 1)[0].strip()
        if field_key == key and field.get("type", "fixed") == "fixed":
            value = field.get("value")
            return normalize_upstream_value(value) if value != "" else None
    return None


def _enum_value(value) -> str:
    return str(value.value if hasattr(value, "value") else value)


def _split_amount(amount: int, parts: int) -> list[int]:
    if parts <= 0:
        return []
    base = amount // parts
    remainder = amount % parts
    return [base + (1 if idx < remainder else 0) for idx in range(parts)]


def _allocate_final_amounts(amounts: list[int], final_amount: int, total_amount: int) -> list[int]:
    if not amounts:
        return []
    if total_amount <= 0:
        return [0 for _ in amounts]
    allocated = [(amount * final_amount) // total_amount for amount in amounts]
    remainder = final_amount - sum(allocated)
    for idx in range(min(remainder, len(allocated))):
        allocated[idx] += 1
    return allocated


async def _build_checkout_context(
    user: User,
    cart_item_ids: list[int],
    db: AsyncSession,
) -> tuple[list[CartItem], dict[int, Product], list[dict], int, dict[int, int]]:
    if not cart_item_ids:
        raise HTTPException(status_code=400, detail="请选择要结算的商品")

    res = await db.execute(
        select(CartItem).where(
            CartItem.id.in_(cart_item_ids),
            CartItem.user_id == user.id,
        )
    )
    cart_items = list(res.scalars().all())
    if len(cart_items) != len(cart_item_ids):
        raise HTTPException(status_code=400, detail="部分购物车项不存在或不属于当前用户")

    product_ids = [ci.product_id for ci in cart_items]
    prod_res = await db.execute(select(Product).where(Product.id.in_(product_ids)))
    products = {p.id: p for p in prod_res.scalars().all()}
    group_ids = {int(product.group_id) for product in products.values() if product.group_id}
    groups: dict[int, ProductGroup] = {}
    if group_ids:
        group_res = await db.execute(select(ProductGroup).where(ProductGroup.id.in_(group_ids)))
        groups = {group.id: group for group in group_res.scalars().all()}

    verification_service.require_verified_for_products(user, products, groups)

    items_snapshot: list[dict] = []
    item_totals: dict[int, int] = {}
    total_amount = 0

    for ci in cart_items:
        product = products.get(ci.product_id)
        if not product:
            raise HTTPException(status_code=400, detail=f"产品 {ci.product_id} 不存在")
        if product.status != ProductStatus.ACTIVE.value:
            raise HTTPException(status_code=400, detail=f"产品「{product.name}」已下架")

        if _enum_value(product.api_type) == ProductApiType.PLUGIN.value:
            plugin_id = str(product.api_identifier or "").strip()
            if not plugin_id:
                raise HTTPException(status_code=400, detail=f"plugin product {product.name} is not bound to a provision plugin")
            provision_plugin_service.validate_purchase_fields(plugin_id, ci.slider_values)

        if product.stock_policy == "tracked":
            available = int(product.stock or 0)
            if available <= 0:
                raise HTTPException(status_code=409, detail=f"产品「{product.name}」已售罄")
            if available < ci.quantity:
                raise HTTPException(
                    status_code=409,
                    detail=f"产品「{product.name}」库存不足，当前仅剩 {available} 台",
                )

        pricing_res = await db.execute(
            select(ProductPricing).where(
                ProductPricing.product_id == product.id,
                ProductPricing.cycle == ci.cycle,
                ProductPricing.is_enabled.is_(True),
            )
        )
        pricing = pricing_res.scalar_one_or_none()
        if not pricing:
            raise HTTPException(status_code=400, detail=f"产品「{product.name}」的 {ci.cycle} 定价不存在")

        unit_price_cents = int(float(ci.unit_price) * 100)
        addon_cents = int(float(ci.addon_price) * 100)
        setup_cents = int(float(ci.setup_fee) * 100)
        item_total = (unit_price_cents + addon_cents) * ci.quantity + setup_cents
        item_totals[ci.id] = item_total
        total_amount += item_total

        provision_config = sync_provision_config_specs(
            product.provision_config,
            vcpu=product.vcpu,
            memory=product.memory,
        )
        group = groups.get(int(product.group_id)) if product.group_id else None

        items_snapshot.append({
            "cart_item_id": ci.id,
            "product_id": product.id,
            "product_name": product.name,
            "sku": product.sku,
            "cycle": ci.cycle,
            "quantity": ci.quantity,
            "unit_price_cents": unit_price_cents,
            "addon_price_cents": addon_cents,
            "setup_fee_cents": setup_cents,
            "item_total_cents": item_total,
            "unit_price_yuan": f"{unit_price_cents / 100:.2f}",
            "os_image": ci.os_image,
            "alias": ci.alias,
            "slider_values": ci.slider_values,
            "auto_renew": ci.auto_renew,
            "cloud_server_id": product.cloud_server_id,
            "api_type": product.api_type,
            "api_identifier": product.api_identifier,
            "stock_policy": product.stock_policy,
            "stock_decremented": product.stock_policy == "tracked",
            "product_group_area": group.area if group else None,
            "product_group_node": group.node if group else None,
            "product_vcpu": product.vcpu,
            "product_memory_mb": parse_memory_to_mb(product.memory),
            "provision_config": provision_config,
        })

    return cart_items, products, items_snapshot, total_amount, item_totals


async def _lock_and_decrement_stock(db: AsyncSession, cart_items: list[CartItem]) -> None:
    required_by_product: dict[int, int] = {}
    for item in cart_items:
        required_by_product[item.product_id] = required_by_product.get(item.product_id, 0) + item.quantity

    if not required_by_product:
        return

    product_ids = sorted(required_by_product)
    res = await db.execute(
        select(Product)
        .where(Product.id.in_(product_ids))
        .order_by(Product.id.asc())
        .with_for_update()
        .execution_options(populate_existing=True)
    )
    locked_products = {product.id: product for product in res.scalars().all()}

    for product_id in product_ids:
        product = locked_products.get(product_id)
        if not product:
            raise HTTPException(status_code=400, detail=f"产品 {product_id} 不存在")
        if product.status != ProductStatus.ACTIVE.value:
            raise HTTPException(status_code=400, detail=f"产品「{product.name}」已下架")

        required = required_by_product[product_id]
        if product.stock_policy != "tracked":
            continue

        available = int(product.stock or 0)
        if available <= 0:
            raise HTTPException(status_code=409, detail=f"产品「{product.name}」已售罄")
        if available < required:
            raise HTTPException(
                status_code=409,
                detail=f"产品「{product.name}」库存不足，当前仅剩 {available} 台",
            )
        product.stock = available - required


async def _calculate_points_discount_for_user(
    db: AsyncSession,
    user_id: int,
    order_amount_cents: int,
    requested_points: int = 0,
) -> tuple[int, int]:
    requested_points = max(0, int(requested_points or 0))
    requested_points = (requested_points // membership_service.POINTS_TO_CENTS_RATE) * membership_service.POINTS_TO_CENTS_RATE
    if requested_points <= 0 or order_amount_cents <= 0:
        return 0, 0

    membership = await membership_service.get_or_create_membership(db, user_id)
    max_rate = 3500
    if membership.member_level_id:
        from app.models.member_level import MemberLevel

        level_res = await db.execute(select(MemberLevel).where(MemberLevel.id == membership.member_level_id))
        level = level_res.scalar_one_or_none()
        if level:
            max_rate = level.max_points_discount_rate

    actual_points, points_discount_cents = membership_service.calculate_points_discount(
        membership.available_points,
        order_amount_cents,
        max_rate,
    )
    if requested_points < actual_points:
        actual_points = requested_points
        points_discount_cents = min(
            order_amount_cents,
            (actual_points // membership_service.POINTS_TO_CENTS_RATE) * 100,
        )

    return actual_points, points_discount_cents


def _apply_coupon_to_snapshots(
    items_snapshot: list[dict],
    coupon_application: coupon_service.CouponApplication | None,
) -> int:
    item_discounts = coupon_application.item_discounts if coupon_application else {}
    payable_total = 0
    for snap in items_snapshot:
        cart_item_id = int(snap.get("cart_item_id") or 0)
        item_total = max(0, int(snap.get("item_total_cents") or 0))
        coupon_discount = min(item_total, max(0, int(item_discounts.get(cart_item_id, 0))))
        payable_before_points = max(0, item_total - coupon_discount)
        snap["coupon_discount_cents"] = coupon_discount
        snap["payable_before_points_cents"] = payable_before_points
        if coupon_application and coupon_discount > 0:
            snap["coupon_code"] = coupon_application.code
            snap["coupon_discount_level"] = coupon_application.discount_level
        payable_total += payable_before_points
    return payable_total


async def _resolve_order_aff_code(
    db: AsyncSession,
    user: User,
    coupon_application: coupon_service.CouponApplication | None,
) -> str | None:
    agent_user_id = None
    if coupon_application and getattr(coupon_application.promo_code, "issuer_type", None) == "agent":
        agent_user_id = getattr(coupon_application.promo_code, "issuer_user_id", None)
    if not agent_user_id:
        agent_user_id = getattr(user, "bound_agent_id", None)
    if not agent_user_id:
        return None

    result = await db.execute(select(User.invite_code).where(User.id == int(agent_user_id)))
    code = result.scalar_one_or_none()
    return str(code).strip().upper() if code else None


async def preview_coupon(
    user: User,
    cart_item_ids: list[int],
    db: AsyncSession,
    *,
    coupon_code: str | None = None,
    coupon_target_cart_item_id: int | None = None,
    use_points: int = 0,
) -> dict:
    cart_items, products, items_snapshot, total_amount, item_totals = await _build_checkout_context(
        user,
        cart_item_ids,
        db,
    )
    if coupon_service.normalize_coupon_code(coupon_code) and coupon_target_cart_item_id is None:
        target_selection = await coupon_service.get_product_coupon_target_selection(
            db,
            user_id=user.id,
            cart_items=cart_items,
            products=products,
            item_totals=item_totals,
            code=coupon_code,
        )
        if target_selection:
            return {
                "total_amount": total_amount,
                "coupon_discount_amount": 0,
                "points_discount_amount": 0,
                "discount_amount": 0,
                "final_amount": total_amount,
                "actual_use_points": 0,
                "coupon": target_selection["coupon"],
                "requires_target_selection": True,
                "target_options": target_selection["target_options"],
            }

    coupon_application = await coupon_service.calculate_cart_coupon(
        db,
        user_id=user.id,
        cart_items=cart_items,
        products=products,
        item_totals=item_totals,
        code=coupon_code,
        target_cart_item_id=coupon_target_cart_item_id,
        for_update=False,
    )
    coupon_discount_cents = coupon_application.discount_amount if coupon_application else 0
    after_coupon_amount = _apply_coupon_to_snapshots(items_snapshot, coupon_application)
    actual_use_points, points_discount_cents = await _calculate_points_discount_for_user(
        db,
        user.id,
        after_coupon_amount,
        use_points,
    )
    final_amount = max(0, after_coupon_amount - points_discount_cents)
    return {
        "total_amount": total_amount,
        "coupon_discount_amount": coupon_discount_cents,
        "points_discount_amount": points_discount_cents,
        "discount_amount": coupon_discount_cents + points_discount_cents,
        "final_amount": final_amount,
        "actual_use_points": actual_use_points,
        "coupon": (
            coupon_service.coupon_application_to_dict(coupon_application, final_amount=after_coupon_amount)
            if coupon_application
            else None
        ),
        "requires_target_selection": False,
        "target_options": [],
    }


async def checkout(
    user: User,
    cart_item_ids: list[int],
    db: AsyncSession,
    use_points: int = 0,
    coupon_code: str | None = None,
    coupon_target_cart_item_id: int | None = None,
) -> Order:
    """
    结算指定购物车项。

    流程：
    1. 加载并校验购物车项
    2. 计算商品总价、优惠券、积分抵扣和余额
    3. 开事务：扣余额 → 写 orders → 写优惠券/余额/积分流水 → 写 instances
    4. 删除已购购物车项
    5. 返回 Order（调用方负责触发开通）
    """
    cart_items, products, items_snapshot, total_amount, item_totals = await _build_checkout_context(
        user,
        cart_item_ids,
        db,
    )

    coupon_application = await coupon_service.calculate_cart_coupon(
        db,
        user_id=user.id,
        cart_items=cart_items,
        products=products,
        item_totals=item_totals,
        code=coupon_code,
        target_cart_item_id=coupon_target_cart_item_id,
        for_update=True,
    )
    coupon_discount_cents = coupon_application.discount_amount if coupon_application else 0
    after_coupon_amount = _apply_coupon_to_snapshots(items_snapshot, coupon_application)

    actual_use_points, points_discount_cents = await _calculate_points_discount_for_user(
        db,
        user.id,
        after_coupon_amount,
        use_points,
    )
    discount_amount = coupon_discount_cents + points_discount_cents
    final_amount = max(0, after_coupon_amount - points_discount_cents)

    await _lock_and_decrement_stock(db, cart_items)

    user_res = await db.execute(select(User).where(User.id == user.id).with_for_update())
    locked_user = user_res.scalar_one()
    if locked_user.balance < final_amount:
        raise HTTPException(
            status_code=402,
            detail=f"余额不足，需要 ¥{final_amount / 100:.2f}，当前余额 ¥{locked_user.balance / 100:.2f}",
        )

    locked_user.balance -= final_amount
    balance_after = locked_user.balance
    aff_code = await _resolve_order_aff_code(db, locked_user, coupon_application)

    out_order_no = _gen_order_no()
    order = Order(
        user_id=user.id,
        out_order_no=out_order_no,
        status=OrderStatus.PAID,
        total_amount=total_amount,
        discount_amount=discount_amount,
        promo_code_id=coupon_application.promo_code.id if coupon_application else None,
        aff_code=aff_code,
        items_snapshot=items_snapshot,
    )
    db.add(order)
    await db.flush()

    if coupon_application:
        await coupon_service.record_coupon_usage(
            db,
            application=coupon_application,
            user_id=user.id,
            order_id=order.id,
        )

    from app.services import agent_service as _agent_svc

    settlements = await _agent_svc.calculate_order_settlement(db, order, user.id, final_amount)
    for s in settlements:
        db.add(s)
    await db.flush()

    # Freeze agent balance when the order needs an agent subsidy.
    await _agent_svc.freeze_agent_subsidy_for_order(db, order)

    balance_log = BalanceLog(
        user_id=user.id,
        change_type=BalanceChangeType.ORDER_PAY,
        delta=-final_amount,
        balance_after=balance_after,
        ref_no=out_order_no,
        remark=f"购买服务 {out_order_no}",
    )
    db.add(balance_log)

    if actual_use_points > 0:
        await membership_service.use_points_for_order(
            db,
            user.id,
            actual_use_points,
            order.id,
            out_order_no,
        )

    instances: list[Instance] = []
    instance_snaps: list[tuple[Instance, dict]] = []
    instance_base_amounts: list[int] = []
    for snap in items_snapshot:
        base_amount = max(0, int(snap.get("payable_before_points_cents") or snap["item_total_cents"]))
        per_instance_amounts = _split_amount(base_amount, snap["quantity"])
        for idx in range(snap["quantity"]):
            slider_values = snap.get("slider_values") or {}
            provision_config = snap.get("provision_config") or []
            inst = Instance(
                user_id=user.id,
                order_id=order.id,
                product_id=snap["product_id"],
                cloud_server_id=snap.get("cloud_server_id"),
                status=InstanceStatus.PROVISIONING,
                cycle=snap["cycle"],
                unit_price=snap["unit_price_cents"] + snap["addon_price_cents"],
                next_renew_amount=snap["unit_price_cents"] + snap["addon_price_cents"],
                auto_renew=1 if snap.get("auto_renew") else 0,
                alias=snap.get("alias"),
                os_image=snap.get("os_image"),
                cpu=(
                    snap.get("product_vcpu")
                    if snap.get("product_vcpu") is not None
                    else _config_value(provision_config, slider_values, "cpu")
                ),
                memory=(
                    snap.get("product_memory_mb")
                    if snap.get("product_memory_mb") is not None
                    else _config_value(provision_config, slider_values, "memory")
                ),
                system_disk_size=_config_value(provision_config, slider_values, "system_disk_size"),
                data_disk_size=_config_value(provision_config, slider_values, "data_disk_size"),
                in_bw=_config_value(provision_config, slider_values, "in_bw"),
                out_bw=_config_value(provision_config, slider_values, "out_bw"),
                ip_num=_config_value(provision_config, slider_values, "ip_num"),
                nat_acl_limit=_config_value(provision_config, slider_values, "nat_acl_limit"),
                traffic_quota=_config_value(provision_config, slider_values, "traffic_quota"),
                traffic_type=_config_value(provision_config, slider_values, "traffic_type"),
            )
            db.add(inst)
            stock_compensation_service.mark_checkout_stock_state(
                inst,
                decremented=bool(snap.get("stock_decremented")),
                product_id=int(snap["product_id"]),
            )
            instances.append(inst)
            instance_snaps.append((inst, snap))
            instance_base_amounts.append(per_instance_amounts[idx] if idx < len(per_instance_amounts) else 0)

    await db.flush()

    instance_final_amounts = _allocate_final_amounts(instance_base_amounts, final_amount, after_coupon_amount)
    await membership_service.create_self_purchase_rewards_for_instances(
        db,
        user_id=user.id,
        order_id=order.id,
        ref_no=out_order_no,
        instance_amounts=[
            (instance.id, amount)
            for instance, amount in zip(instances, instance_final_amounts)
            if amount > 0
        ],
        source_type=spend_service.SpendEventType.SERVER_PURCHASE,
        status="pending",
    )

    for ci in cart_items:
        await db.delete(ci)

    await db.flush()

    await spend_service.record_spend_event(
        db,
        user_id=user.id,
        event_type=spend_service.SpendEventType.SERVER_PURCHASE,
        amount_cents=final_amount,
        event_key=f"{out_order_no}:server_purchase",
        order_id=order.id,
        ref_no=out_order_no,
        remark=f"购买服务 {out_order_no}",
        payload_snapshot={
            "total_amount": total_amount,
            "coupon_discount_amount": coupon_discount_cents,
            "points_discount_amount": points_discount_cents,
            "discount_amount": discount_amount,
            "aff_code": aff_code,
            "coupon": coupon_service.coupon_application_to_dict(coupon_application) if coupon_application else None,
        },
    )

    log.info(
        "checkout: user=%s order=%s total_cents=%s coupon_cents=%s points_cents=%s final_cents=%s instances=%s",
        user.id,
        out_order_no,
        total_amount,
        coupon_discount_cents,
        points_discount_cents,
        final_amount,
        len(instances),
    )

    order._instance_snaps = instance_snaps  # type: ignore[attr-defined]
    order._coupon_application = coupon_application  # type: ignore[attr-defined]
    order._paid_amount = final_amount  # type: ignore[attr-defined]
    order._coupon_discount_amount = coupon_discount_cents  # type: ignore[attr-defined]
    order._points_discount_amount = points_discount_cents  # type: ignore[attr-defined]
    return order
