"""Instance package switching: list packages, quote, and apply the change."""
from __future__ import annotations

import logging
from datetime import datetime, timezone
from decimal import Decimal, ROUND_CEILING, ROUND_HALF_UP
from typing import Any

from fastapi import APIRouter, Depends, HTTPException
from pydantic import BaseModel, Field
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession

from app.core.crypto import decrypt_secret
from app.core.deps import get_current_user
from app.database import get_db
from app.models.balance_log import BalanceChangeType, BalanceLog
from app.models.cloud_server import CloudServer
from app.models.instance import Instance, InstanceStatus
from app.models.instance_disk_addon import InstanceDiskAddon, InstanceDiskAddonStatus
from app.models.order import Order, OrderStatus
from app.models.product import Product, ProductStatus
from app.models.product_pricing import ProductPricing
from app.models.user import User
from app.schemas.common import ApiResponse
from app.services.dcim_cloud_client import DcimCloudClient, DcimCloudCredentials, DcimCloudError
from app.services import agent_service, membership_service, spend_service, zjmf_instance_adapter
from app.utils.specs import parse_memory_to_mb

log = logging.getLogger("qycw.instances.upgrade")

router = APIRouter()


class UpgradeQuoteBody(BaseModel):
    target_product_id: int = Field(..., ge=1)


class UpgradeExecuteBody(BaseModel):
    target_product_id: int = Field(..., ge=1)


CYCLE_SECONDS: dict[str, int] = {
    "hourly": 3600,
    "daily": 86400,
    "weekly": 7 * 86400,
    "monthly": 30 * 86400,
    "quarterly": 90 * 86400,
    "semi_annually": 180 * 86400,
    "semiannually": 180 * 86400,
    "annually": 365 * 86400,
    "yearly": 365 * 86400,
    "biennially": 730 * 86400,
    "triennially": 1095 * 86400,
}


def _cycle_candidates(cycle: str) -> list[str]:
    aliases = {
        "semiannually": ["semiannually", "semi_annually"],
        "semi_annually": ["semi_annually", "semiannually"],
        "yearly": ["yearly", "annually"],
        "annually": ["annually", "yearly"],
    }
    values = aliases.get(cycle, [cycle])
    result: list[str] = []
    for value in values:
        if value not in result:
            result.append(value)
    return result


def _cycle_seconds(cycle: str) -> int:
    return CYCLE_SECONDS.get(cycle.lower(), 30 * 86400)


def _yuan_to_cents(value: Decimal) -> int:
    return int((value * Decimal("100")).quantize(Decimal("1"), rounding=ROUND_HALF_UP))


def _to_int_or_none(value: Any) -> int | None:
    if value in (None, ""):
        return None
    if isinstance(value, str):
        value = value.split("|", 1)[0].strip()
    try:
        return int(float(value))
    except (TypeError, ValueError):
        return None


def _config_value(product: Product | None, *keys: str) -> Any:
    if not product or not isinstance(product.provision_config, list):
        return None
    key_set = set(keys)
    for field in product.provision_config:
        if not isinstance(field, dict):
            continue
        field_key = str(field.get("def", "")).split("|", 1)[0].strip()
        if field_key in key_set and field.get("type", "fixed") == "fixed":
            value = field.get("value")
            return None if value in (None, "") else value
    return None


def _spec_value(product: Product | None, *keys: str) -> int | None:
    return _to_int_or_none(_config_value(product, *keys))


def _product_specs(product: Product) -> dict[str, int | None]:
    in_bw = _spec_value(product, "in_bw")
    out_bw = _spec_value(product, "out_bw")
    return {
        "cpu": product.vcpu if product.vcpu is not None else _spec_value(product, "cpu"),
        "memory": parse_memory_to_mb(product.memory) or _spec_value(product, "memory"),
        "in_bw": in_bw,
        "out_bw": out_bw,
        "bandwidth": out_bw or in_bw,
        "traffic_quota": _spec_value(product, "traffic_quota"),
        "traffic_type": _spec_value(product, "traffic_type"),
        "nat_acl_limit": _spec_value(product, "nat_acl_limit"),
        "nat_web_limit": _spec_value(product, "nat_web_limit"),
    }


async def _get_dcim_client(
    cloud_server_id: int, db: AsyncSession
) -> tuple[DcimCloudClient, CloudServer]:
    res = await db.execute(select(CloudServer).where(CloudServer.id == cloud_server_id))
    server = res.scalar_one_or_none()
    if not server:
        raise HTTPException(status_code=500, detail="云节点不存在")
    creds = DcimCloudCredentials(
        base_url=server.base_url,
        username=server.username,
        password=decrypt_secret(server.password_encrypted),
        account_type=server.account_type,
    )
    return DcimCloudClient(creds, server_id=server.id), server


async def _pricing_for_cycle(product_id: int, cycle: str, db: AsyncSession) -> ProductPricing | None:
    candidates = _cycle_candidates(cycle)
    res = await db.execute(
        select(ProductPricing).where(
            ProductPricing.product_id == product_id,
            ProductPricing.cycle.in_(candidates),
            ProductPricing.is_enabled.is_(True),
        )
    )
    items = list(res.scalars().all())
    if not items:
        return None
    order = {value: index for index, value in enumerate(candidates)}
    return sorted(items, key=lambda item: order.get(str(item.cycle), 999))[0]


async def _required_pricing(product: Product, cycle: str, db: AsyncSession) -> ProductPricing:
    pricing = await _pricing_for_cycle(product.id, cycle, db)
    if not pricing:
        raise HTTPException(status_code=400, detail=f"套餐“{product.name}”没有当前周期定价，无法切换")
    return pricing


async def _active_disk_addon_total(db: AsyncSession, instance_id: int) -> int:
    res = await db.execute(
        select(InstanceDiskAddon).where(
            InstanceDiskAddon.instance_id == instance_id,
            InstanceDiskAddon.status == InstanceDiskAddonStatus.ACTIVE,
        )
    )
    return sum(int(addon.recurring_amount or 0) for addon in res.scalars().all())


def _remaining_ratio(instance: Instance) -> tuple[int, int, Decimal]:
    period_seconds = _cycle_seconds(instance.cycle)
    if period_seconds <= 0:
        raise HTTPException(status_code=400, detail="当前计费周期暂不支持套餐切换")

    now = datetime.now(timezone.utc)
    expires_at = instance.expires_at
    if expires_at is None:
        remaining_seconds = period_seconds
    else:
        if expires_at.tzinfo is None:
            expires_at = expires_at.replace(tzinfo=timezone.utc)
        remaining_seconds = max(0, int((expires_at - now).total_seconds()))

    remaining_days = int((Decimal(remaining_seconds) / Decimal("86400")).quantize(Decimal("1"), rounding=ROUND_CEILING))
    ratio = Decimal(remaining_seconds) / Decimal(period_seconds)
    return remaining_seconds, remaining_days, ratio


def _product_payload(product: Product, pricing: ProductPricing, cycle: str) -> dict[str, Any]:
    cycle_price_cents = _yuan_to_cents(Decimal(str(pricing.price)))
    return {
        "id": product.id,
        "name": product.name,
        "description": getattr(product, "description", None),
        "cycle": cycle,
        "cycle_price": cycle_price_cents,
        "monthly_price": cycle_price_cents / 100,
        "memory_display": product.memory,
        "disk_display": product.disk,
        "bandwidth_display": product.bandwidth,
        **_product_specs(product),
    }


async def _get_instance_and_products(
    instance_id: int,
    target_product_id: int,
    current_user: User,
    db: AsyncSession,
    *,
    lock_instance: bool = False,
) -> tuple[Instance, Product, Product]:
    stmt = select(Instance).where(
        Instance.id == instance_id,
        Instance.user_id == current_user.id,
    )
    if lock_instance:
        stmt = stmt.with_for_update()
    res = await db.execute(stmt)
    instance = res.scalar_one_or_none()
    if not instance:
        raise HTTPException(status_code=404, detail="实例不存在")
    if await zjmf_instance_adapter.get_zjmf_server_for_instance(db, instance):
        raise HTTPException(status_code=400, detail=zjmf_instance_adapter.UPGRADE_UNSUPPORTED_MESSAGE)
    if instance.status not in (InstanceStatus.ACTIVE, InstanceStatus.SUSPENDED):
        raise HTTPException(status_code=400, detail="当前实例状态不支持套餐切换")

    res = await db.execute(select(Product).where(Product.id == instance.product_id))
    current_product = res.scalar_one_or_none()
    if not current_product:
        raise HTTPException(status_code=404, detail="当前套餐不存在")

    res = await db.execute(select(Product).where(Product.id == target_product_id))
    target_product = res.scalar_one_or_none()
    if not target_product:
        raise HTTPException(status_code=404, detail="目标套餐不存在")
    if target_product.status != ProductStatus.ACTIVE:
        raise HTTPException(status_code=400, detail="目标套餐不可用")
    if target_product.id == current_product.id:
        raise HTTPException(status_code=400, detail="请选择不同的目标套餐")
    if current_product.group_id != target_product.group_id:
        raise HTTPException(status_code=400, detail="目标套餐不在同一产品组，无法切换")

    return instance, current_product, target_product


async def _build_quote(
    instance: Instance,
    current_product: Product,
    target_product: Product,
    db: AsyncSession,
) -> dict[str, Any]:
    current_pricing = await _required_pricing(current_product, instance.cycle, db)
    target_pricing = await _required_pricing(target_product, instance.cycle, db)

    current_unit_price = int(instance.unit_price or _yuan_to_cents(Decimal(str(current_pricing.price))))
    target_unit_price = _yuan_to_cents(Decimal(str(target_pricing.price)))
    price_diff_cents = target_unit_price - current_unit_price

    remaining_seconds, remaining_days, ratio = _remaining_ratio(instance)
    charge = 0
    if price_diff_cents > 0 and remaining_seconds > 0:
        charge = int(
            (Decimal(price_diff_cents) * ratio).quantize(Decimal("1"), rounding=ROUND_CEILING)
        )

    disk_addon_total = await _active_disk_addon_total(db, instance.id)
    new_renew_amount = target_unit_price + disk_addon_total

    return {
        "current_product": {
            **_product_payload(current_product, current_pricing, instance.cycle),
            "cycle_price": current_unit_price,
            "monthly_price": current_unit_price / 100,
        },
        "target_product": _product_payload(target_product, target_pricing, instance.cycle),
        "is_upgrade": price_diff_cents > 0,
        "is_downgrade": price_diff_cents < 0,
        "price_diff_cents": price_diff_cents,
        "price_diff_yuan": price_diff_cents / 100,
        "current_charge": charge,
        "remaining_days": remaining_days,
        "remaining_seconds": remaining_seconds,
        "new_unit_price": target_unit_price,
        "new_renew_amount": new_renew_amount,
        "disk_addon_amount": disk_addon_total,
    }


@router.get("/{instance_id}/upgrade/products", response_model=ApiResponse)
async def get_upgrade_products(
    instance_id: int,
    current_user: User = Depends(get_current_user),
    db: AsyncSession = Depends(get_db),
):
    res = await db.execute(
        select(Instance).where(
            Instance.id == instance_id,
            Instance.user_id == current_user.id,
        )
    )
    instance = res.scalar_one_or_none()
    if not instance:
        raise HTTPException(status_code=404, detail="实例不存在")
    if await zjmf_instance_adapter.get_zjmf_server_for_instance(db, instance):
        raise HTTPException(status_code=400, detail=zjmf_instance_adapter.UPGRADE_UNSUPPORTED_MESSAGE)
    if instance.status not in (InstanceStatus.ACTIVE, InstanceStatus.SUSPENDED):
        raise HTTPException(status_code=400, detail="当前实例状态不支持套餐切换")

    res = await db.execute(select(Product).where(Product.id == instance.product_id))
    current_product = res.scalar_one_or_none()
    if not current_product:
        raise HTTPException(status_code=404, detail="当前套餐不存在")

    res = await db.execute(
        select(Product)
        .where(
            Product.group_id == current_product.group_id,
            Product.id != current_product.id,
            Product.status == ProductStatus.ACTIVE,
        )
        .order_by(Product.sort_order.asc(), Product.id.asc())
    )
    products = list(res.scalars().all())

    data: list[dict[str, Any]] = []
    for product in products:
        pricing = await _pricing_for_cycle(product.id, instance.cycle, db)
        if not pricing:
            continue
        data.append(_product_payload(product, pricing, instance.cycle))

    data.sort(key=lambda item: (item.get("cycle_price") or 0, item.get("id") or 0))
    return ApiResponse(data=data)


@router.post("/{instance_id}/upgrade/quote", response_model=ApiResponse)
async def get_upgrade_quote(
    instance_id: int,
    body: UpgradeQuoteBody,
    current_user: User = Depends(get_current_user),
    db: AsyncSession = Depends(get_db),
):
    instance, current_product, target_product = await _get_instance_and_products(
        instance_id, body.target_product_id, current_user, db
    )
    return ApiResponse(data=await _build_quote(instance, current_product, target_product, db))


@router.post("/{instance_id}/upgrade", response_model=ApiResponse)
async def execute_upgrade(
    instance_id: int,
    body: UpgradeExecuteBody,
    current_user: User = Depends(get_current_user),
    db: AsyncSession = Depends(get_db),
):
    instance, current_product, target_product = await _get_instance_and_products(
        instance_id, body.target_product_id, current_user, db, lock_instance=True
    )
    if not instance.upstream_id or not instance.cloud_server_id:
        raise HTTPException(status_code=400, detail="实例尚未开通，无法切换套餐")

    quote = await _build_quote(instance, current_product, target_product, db)
    charge = int(quote["current_charge"] or 0)

    res = await db.execute(select(User).where(User.id == current_user.id).with_for_update())
    user = res.scalar_one_or_none()
    if not user:
        raise HTTPException(status_code=404, detail="用户不存在")
    if charge > 0 and user.balance < charge:
        raise HTTPException(status_code=400, detail="余额不足，请先充值")

    target_specs = _product_specs(target_product)
    instance_payload: dict[str, Any] = {}
    for source_key, upstream_key in (
        ("cpu", "cpu"),
        ("memory", "memory"),
        ("traffic_quota", "traffic_quota"),
        ("traffic_type", "traffic_type"),
        ("nat_acl_limit", "nat_acl_limit"),
        ("nat_web_limit", "nat_web_limit"),
    ):
        value = target_specs.get(source_key)
        if value is not None:
            instance_payload[upstream_key] = value

    bw_payload: dict[str, Any] = {}
    if target_specs.get("in_bw") is not None:
        bw_payload["in_bw"] = target_specs["in_bw"]
    if target_specs.get("out_bw") is not None:
        bw_payload["out_bw"] = target_specs["out_bw"]

    if not instance_payload and not bw_payload:
        raise HTTPException(status_code=400, detail="目标套餐没有可应用的云服务器配置")

    client, _ = await _get_dcim_client(instance.cloud_server_id, db)
    try:
        if instance_payload:
            code, text, _ = await client.request(
                f"/clouds/{instance.upstream_id}",
                method="PUT",
                data=instance_payload,
                timeout=30.0,
            )
            if not (200 <= code < 300):
                raise HTTPException(status_code=502, detail=f"上游返回异常（HTTP {code}）：{text[:200]}")

        if bw_payload:
            code, text, _ = await client.request(
                f"/clouds/{instance.upstream_id}/bw",
                method="PUT",
                data=bw_payload,
                timeout=30.0,
            )
            if not (200 <= code < 300):
                raise HTTPException(status_code=502, detail=f"上游返回异常（HTTP {code}）：{text[:200]}")
    except DcimCloudError as e:
        raise HTTPException(status_code=502, detail=f"上游调用失败：{e}")

    order: Order | None = None
    ref_no: str | None = None
    if charge > 0:
        ref_no = f"UPG{datetime.now(timezone.utc).strftime('%Y%m%d%H%M%S%f')}"
        order = Order(
            user_id=user.id,
            out_order_no=ref_no,
            status=OrderStatus.PAID,
            total_amount=charge,
            discount_amount=0,
            items_snapshot=[{
                "type": "package_upgrade",
                "instance_id": instance.id,
                "from_product_id": current_product.id,
                "from_product_name": current_product.name,
                "to_product_id": target_product.id,
                "to_product_name": target_product.name,
                "current_charge_cents": charge,
                "remaining_days": quote["remaining_days"],
                "new_unit_price": quote["new_unit_price"],
                "new_renew_amount": quote["new_renew_amount"],
            }],
            remark=f"实例 #{instance.id} 套餐切换：{current_product.name} -> {target_product.name}",
        )
        db.add(order)
        await db.flush()

        user.balance -= charge
        db.add(BalanceLog(
            user_id=user.id,
            change_type=BalanceChangeType.ORDER_PAY,
            delta=-charge,
            balance_after=user.balance,
            ref_no=ref_no,
            remark=f"实例 #{instance.id} 套餐切换：{current_product.name} -> {target_product.name}",
        ))
        await spend_service.record_spend_event(
            db,
            user_id=user.id,
            event_type=spend_service.SpendEventType.PACKAGE_UPGRADE,
            amount_cents=charge,
            event_key=f"{ref_no}:package_upgrade",
            order_id=order.id,
            ref_no=ref_no,
            remark=f"实例 #{instance.id} 套餐切换：{current_product.name} -> {target_product.name}",
            payload_snapshot={
                "instance_id": instance.id,
                "from_product_id": current_product.id,
                "to_product_id": target_product.id,
            },
        )
        settlements = await agent_service.calculate_order_settlement(db, order, user.id, charge)
        for settlement in settlements:
            db.add(settlement)
        await db.flush()
        if settlements:
            await agent_service.settle_order(db, order)
        await membership_service.reward_referrer_for_order(db, order)
        await membership_service.create_self_purchase_point_reward(
            db,
            user_id=user.id,
            order_id=order.id,
            ref_no=order.out_order_no,
            amount_cents=charge,
            source_type=spend_service.SpendEventType.PACKAGE_UPGRADE,
            instance_id=instance.id,
            status="claimable",
            payload_snapshot={"instance_id": instance.id},
        )

    instance.product_id = target_product.id
    instance.unit_price = int(quote["new_unit_price"])
    instance.next_renew_amount = int(quote["new_renew_amount"])

    for key in ("cpu", "memory", "in_bw", "out_bw", "traffic_quota", "traffic_type", "nat_acl_limit"):
        value = target_specs.get(key)
        if value is not None:
            setattr(instance, key, value)

    await db.flush()
    log.info(
        "instance package switched: instance=%s from=%s to=%s charge=%s",
        instance.id,
        current_product.id,
        target_product.id,
        charge,
    )

    return ApiResponse(data={
        "instance_id": instance.id,
        "charged": charge,
        "balance": user.balance,
        "order_id": order.id if order else None,
        "order_no": ref_no,
        "new_unit_price": instance.unit_price,
        "new_renew_amount": instance.next_renew_amount,
        "is_downgrade": bool(quote["is_downgrade"]),
    })
