from typing import Any

from fastapi import HTTPException
from sqlalchemy import delete, func, select
from sqlalchemy.ext.asyncio import AsyncSession

from app.core.crypto import decrypt_secret
from app.models.cloud_server import CloudAccountType, CloudServer, CloudServerType
from app.models.product import Product, ProductApiType, ProductStatus, ProductType, StockPolicy
from app.models.product_group import ProductGroup
from app.models.product_pricing import BillingCycle, ProductPricing
from app.schemas.product import (
    CloudResourceItem,
    ProductCreate,
    ProductPricingItem,
    ProductUpdate,
)
from app.services.dcim_cloud_client import DcimCloudClient, DcimCloudCredentials, DcimCloudError
from app.services.zjmf_finance_client import (
    ZjmfFinanceClient,
    ZjmfFinanceCredentials,
    ZjmfFinanceError,
)
from app.services import provision_interface_service, provision_plugin_service
from app.utils.specs import sync_provision_config_specs


# ---------- query ----------


async def list_products(
    db: AsyncSession, group_id: int | None = None
) -> list[tuple[Product, list[ProductPricing]]]:
    stmt = select(Product).order_by(Product.sort_order.asc(), Product.id.desc())
    if group_id is not None:
        stmt = stmt.where(Product.group_id == group_id)
    res = await db.execute(stmt)
    products = list(res.scalars().all())
    if not products:
        return []
    ids = [p.id for p in products]
    pricing_res = await db.execute(
        select(ProductPricing)
        .where(ProductPricing.product_id.in_(ids))
        .order_by(ProductPricing.product_id.asc(), ProductPricing.sort_order.asc())
    )
    by_product: dict[int, list[ProductPricing]] = {}
    for pr in pricing_res.scalars().all():
        by_product.setdefault(pr.product_id, []).append(pr)
    return [(p, by_product.get(p.id, [])) for p in products]


async def get_product(
    db: AsyncSession, product_id: int
) -> tuple[Product, list[ProductPricing]]:
    res = await db.execute(select(Product).where(Product.id == product_id))
    product = res.scalar_one_or_none()
    if not product:
        raise HTTPException(status_code=404, detail="产品不存在")
    pricing_res = await db.execute(
        select(ProductPricing)
        .where(ProductPricing.product_id == product_id)
        .order_by(ProductPricing.sort_order.asc(), ProductPricing.id.asc())
    )
    return product, list(pricing_res.scalars().all())


# ---------- mutate ----------


async def create_product(
    db: AsyncSession, data: ProductCreate
) -> tuple[Product, list[ProductPricing]]:
    await _ensure_group(db, data.group_id)
    await _ensure_unique_sku(db, data.sku)
    _validate_api_binding(data.api_type, data.api_identifier, data.cloud_server_id)
    if data.api_type == ProductApiType.PLUGIN.value:
        if not data.provision_interface_id:
            raise HTTPException(status_code=400, detail="插件开通产品必须选择开通接口")
        await provision_interface_service.ensure_product_interface(
            db,
            data.provision_interface_id,
            data.api_identifier,
        )
    if data.cloud_server_id is not None:
        await _ensure_cloud_server(db, data.cloud_server_id, data.api_type)
    _validate_pricings(data.pricings)

    payload = data.model_dump(exclude={"pricings"})
    payload["type"] = ProductType(payload["type"]).value
    payload["status"] = ProductStatus(payload["status"]).value
    payload["stock_policy"] = StockPolicy(payload["stock_policy"]).value
    payload["api_type"] = ProductApiType(payload["api_type"]).value
    payload["provision_config"] = sync_provision_config_specs(
        payload.get("provision_config"),
        vcpu=payload.get("vcpu"),
        memory=payload.get("memory"),
    )

    product = Product(**payload)
    db.add(product)
    await db.flush()
    for item in data.pricings:
        db.add(_pricing_from_item(product.id, item))
    await db.flush()
    await db.refresh(product)
    return await get_product(db, product.id)


async def update_product(
    db: AsyncSession, product_id: int, data: ProductUpdate
) -> tuple[Product, list[ProductPricing]]:
    product, _ = await get_product(db, product_id)
    update = data.model_dump(exclude_unset=True)

    if "group_id" in update and update["group_id"] != product.group_id:
        await _ensure_group(db, update["group_id"])
    if "sku" in update and update["sku"] != product.sku:
        await _ensure_unique_sku(db, update["sku"], exclude_id=product_id)
    new_api_type = update.get("api_type", product.api_type)
    new_api_id = update.get("api_identifier", product.api_identifier)
    new_cs_id = update.get("cloud_server_id", product.cloud_server_id)
    new_interface_id = update.get("provision_interface_id", product.provision_interface_id)
    _validate_api_binding(new_api_type, new_api_id, new_cs_id)
    if new_api_type == ProductApiType.PLUGIN.value:
        if not new_interface_id:
            raise HTTPException(status_code=400, detail="插件开通产品必须选择开通接口")
        await provision_interface_service.ensure_product_interface(db, new_interface_id, new_api_id)
    else:
        update["provision_interface_id"] = None
    if new_cs_id is not None:
        await _ensure_cloud_server(db, new_cs_id, new_api_type)

    for k in ("type", "status", "stock_policy", "api_type"):
        if k in update and update[k] is not None:
            enum_cls = {
                "type": ProductType,
                "status": ProductStatus,
                "stock_policy": StockPolicy,
                "api_type": ProductApiType,
            }[k]
            update[k] = enum_cls(update[k]).value

    pricings_raw = update.pop("pricings", None)
    pricings: list[ProductPricingItem] | None = None
    if pricings_raw is not None:
        pricings = [
            p if isinstance(p, ProductPricingItem) else ProductPricingItem(**p)
            for p in pricings_raw
        ]
    for k, v in update.items():
        setattr(product, k, v)

    if "provision_config" in update or "vcpu" in update or "memory" in update:
        product.provision_config = sync_provision_config_specs(
            product.provision_config,
            vcpu=product.vcpu,
            memory=product.memory,
        )
    await db.flush()

    if pricings is not None:
        _validate_pricings(pricings)
        await db.execute(delete(ProductPricing).where(ProductPricing.product_id == product_id))
        for item in pricings:
            db.add(_pricing_from_item(product_id, item))
        await db.flush()

    return await get_product(db, product_id)


async def delete_product(db: AsyncSession, product_id: int) -> None:
    product, _ = await get_product(db, product_id)
    await db.delete(product)
    await db.flush()


# ---------- readiness check ----------


_LOCATION_SELECTOR_KEYS = {
    "node",
    "area",
    "node_group",
    "node_group_id",
    "ip_group",
    "ip_group_id",
    "ip",
    "ip_id",
}
_REQUIRED_PROVISION_KEYS = ("cpu", "memory", "system_disk_size", "in_bw", "out_bw", "ip_num")
_RECOMMENDED_PROVISION_KEYS = ("data_disk_size",)


async def check_group_readiness(db: AsyncSession, group_id: int) -> dict[str, Any]:
    group_res = await db.execute(select(ProductGroup).where(ProductGroup.id == group_id))
    group = group_res.scalar_one_or_none()
    if not group:
        raise HTTPException(status_code=404, detail="产品组不存在")

    rows = await list_products(db, group_id=group_id)
    active_server_rows = [
        (product, pricings)
        for product, pricings in rows
        if product.status == ProductStatus.ACTIVE.value and product.type == ProductType.SERVER.value
    ]

    group_issues: list[dict[str, Any]] = []
    product_results: list[dict[str, Any]] = []

    if not active_server_rows:
        group_issues.append(_issue(
            "warning",
            "group",
            "当前分组没有在售服务器产品，已跳过可售开通检查。",
            "如果这是测试分组，可以忽略；正式分组请先上架至少一个服务器产品。",
        ))
        return _readiness_payload(group, rows, active_server_rows, group_issues, product_results)

    os_options = group.os_options if isinstance(group.os_options, list) else []
    any_product_has_os = any(
        _has_config_value(product.provision_config, "os")
        for product, _ in active_server_rows
    )
    if not os_options and not any_product_has_os:
        group_issues.append(_issue(
            "error",
            "group",
            "产品组未配置系统镜像。",
            "在产品详情的系统列表中拉取并保存上游镜像。",
        ))

    group_has_location = group.node not in (None, "", 0, "0") or group.area not in (None, "", 0, "0")
    location_required_rows = [
        (product, pricings)
        for product, pricings in active_server_rows
        if product.api_type not in (ProductApiType.ZJMF_FINANCE.value, ProductApiType.PLUGIN.value)
    ]
    any_product_has_location = any(
        _has_config_location_selector(product.provision_config)
        for product, _ in location_required_rows
    )
    if location_required_rows and not group_has_location and not any_product_has_location:
        group_issues.append(_issue(
            "error",
            "group",
            "产品组未绑定区域或节点。",
            "在产品组中填写 area 或 node；推荐使用 node，避免上游要求必须选择节点。",
        ))
    elif location_required_rows and not group_has_location:
        group_issues.append(_issue(
            "warning",
            "group",
            "产品组未绑定 area/node，当前依赖产品开通参数中的位置选择器。",
            "建议把区域或节点配置到产品组，便于统一管理。",
        ))

    cloud_server_ids = {
        int(product.cloud_server_id)
        for product, _ in active_server_rows
        if product.cloud_server_id is not None
    }
    cloud_server_map: dict[int, CloudServer] = {}
    if cloud_server_ids:
        cloud_res = await db.execute(
            select(CloudServer).where(CloudServer.id.in_(cloud_server_ids))
        )
        cloud_server_map = {server.id: server for server in cloud_res.scalars().all()}

    for product, pricings in active_server_rows:
        issues = _check_product_readiness(product, pricings, group_has_location, cloud_server_map)
        product_results.append({
            "product_id": product.id,
            "sku": product.sku,
            "name": product.name,
            "status": _issues_status(issues),
            "error_count": sum(1 for item in issues if item["level"] == "error"),
            "warning_count": sum(1 for item in issues if item["level"] == "warning"),
            "issues": issues,
        })

    return _readiness_payload(group, rows, active_server_rows, group_issues, product_results)


# ---------- 上游辅助：列镜像/区域 ----------


async def list_cloud_resources(
    db: AsyncSession, cloud_server_id: int, kind: str
) -> list[CloudResourceItem]:
    """供产品表单"上游绑定"区域使用。

    kind: image / area。agent 模式下两者都返回空列表（mfcw 也没实现，让管理员手填）。
    """
    cs_res = await db.execute(select(CloudServer).where(CloudServer.id == cloud_server_id))
    cs = cs_res.scalar_one_or_none()
    if not cs:
        raise HTTPException(status_code=404, detail="云接口不存在")
    if cs.disabled:
        raise HTTPException(status_code=400, detail="该云接口已被禁用")
    if cs.server_type != CloudServerType.DCIMCLOUD.value:
        return []
    if cs.account_type == "agent":
        return []

    creds = DcimCloudCredentials(
        base_url=cs.base_url,
        username=cs.username,
        password=decrypt_secret(cs.password_encrypted),
        account_type=cs.account_type,
    )
    client = DcimCloudClient(creds, server_id=cs.id, timeout=15.0)
    if kind == "image":
        path, params = "/image", {"per_page": "9999", "sort": "asc"}
    elif kind == "area":
        path, params = "/areas", {"sort": "asc", "list_type": "all"}
    else:
        raise HTTPException(status_code=400, detail=f"未知 kind={kind}")

    try:
        code, _, body = await client.request(path, data=params, method="GET")
    except DcimCloudError as e:
        raise HTTPException(status_code=502, detail=f"上游错误：{e.message}") from e

    if code != 200 or not body:
        raise HTTPException(status_code=502, detail=f"上游 HTTP {code}")

    data_field = body.get("data")
    if isinstance(data_field, dict):
        rows = data_field.get("data") or data_field.get("list") or []
    elif isinstance(data_field, list):
        rows = data_field
    else:
        rows = []
    if not isinstance(rows, list):
        return []

    out: list[CloudResourceItem] = []
    if kind == "image":
        for v in rows:
            if not isinstance(v, dict):
                continue
            if v.get("status") != 1:
                continue
            group_name = v.get("group_name") or (v.get("group") or {}).get("name") or ""
            label = f"{group_name} / {v.get('name', '')}".strip(" /")
            out.append(
                CloudResourceItem(
                    id=str(v.get("id")),
                    label=label or str(v.get("id")),
                    extra=v.get("filename"),
                )
            )
    else:  # area
        for v in rows:
            if not isinstance(v, dict):
                continue
            out.append(
                CloudResourceItem(
                    id=str(v.get("id")),
                    label=v.get("name") or str(v.get("id")),
                    extra=v.get("country_code"),
                )
            )
    return out


async def list_zjmf_finance_products(db: AsyncSession, cloud_server_id: int) -> dict[str, Any]:
    server = await _require_zjmf_finance_server(db, cloud_server_id)
    client = _zjmf_finance_client(server)
    try:
        data = await client.get_products()
    except ZjmfFinanceError as e:
        raise HTTPException(status_code=502, detail=f"魔方财务上游错误：{e.message}") from e
    return _normalize_zjmf_product_list(data)


async def get_zjmf_finance_product_config(
    db: AsyncSession, cloud_server_id: int, pid: int
) -> dict[str, Any]:
    server = await _require_zjmf_finance_server(db, cloud_server_id)
    client = _zjmf_finance_client(server)
    try:
        data = await client.get_product_config(pid)
    except ZjmfFinanceError as e:
        raise HTTPException(status_code=502, detail=f"魔方财务上游错误：{e.message}") from e
    return _normalize_zjmf_product_config(data)


# ---------- helpers ----------


async def _require_zjmf_finance_server(db: AsyncSession, cloud_server_id: int) -> CloudServer:
    cs_res = await db.execute(select(CloudServer).where(CloudServer.id == cloud_server_id))
    server = cs_res.scalar_one_or_none()
    if not server:
        raise HTTPException(status_code=404, detail="云接口不存在")
    if server.disabled:
        raise HTTPException(status_code=400, detail="该云接口已被禁用")
    if server.server_type != CloudServerType.ZJMF_FINANCE.value:
        raise HTTPException(status_code=400, detail="请选择魔方财务上游节点")
    return server


def _zjmf_finance_client(server: CloudServer) -> ZjmfFinanceClient:
    creds = ZjmfFinanceCredentials(
        base_url=server.base_url,
        username=server.username,
        password=decrypt_secret(server.password_encrypted),
        account_type=server.account_type,
    )
    return ZjmfFinanceClient(creds, server_id=server.id, timeout=15.0)


def _normalize_zjmf_product_list(data: dict[str, Any]) -> dict[str, Any]:
    source_groups = data.get("products")
    if not isinstance(source_groups, list):
        source_groups = data.get("list") or data.get("data") or []
    if not isinstance(source_groups, list):
        source_groups = []

    groups: list[dict[str, Any]] = []
    flat_products: list[dict[str, Any]] = []
    for group in source_groups:
        if not isinstance(group, dict):
            continue
        group_products = group.get("products")
        if isinstance(group_products, list):
            normalized_products = [
                item
                for item in (
                    _normalize_zjmf_product(product, group)
                    for product in group_products
                    if isinstance(product, dict)
                )
                if item is not None
            ]
            groups.append({
                "id": _string_or_none(group.get("id")),
                "name": str(group.get("name") or group.get("group_name") or "未分组"),
                "products": normalized_products,
            })
            flat_products.extend(normalized_products)
        else:
            item = _normalize_zjmf_product(group, None)
            if item is not None:
                flat_products.append(item)

    return {
        "count": _int_or_default(data.get("count"), len(flat_products)),
        "currency": data.get("currency"),
        "groups": groups,
        "products": flat_products,
    }


def _normalize_zjmf_product(product: dict[str, Any], group: dict[str, Any] | None) -> dict[str, Any] | None:
    raw_id = product.get("id") or product.get("pid") or product.get("product_id")
    product_id = _string_or_none(raw_id)
    if not product_id:
        return None
    group_id = _string_or_none(group.get("id")) if group else _string_or_none(product.get("gid") or product.get("group_id"))
    group_name = str(group.get("name") or group.get("group_name") or "") if group else str(product.get("group_name") or "")
    return {
        "id": product_id,
        "name": str(product.get("name") or f"产品 {product_id}"),
        "type": product.get("type"),
        "description": product.get("description"),
        "group_id": group_id,
        "group_name": group_name or None,
    }


def _normalize_zjmf_product_config(data: dict[str, Any]) -> dict[str, Any]:
    if isinstance(data.get("products"), dict):
        product = data["products"]
    elif isinstance(data.get("product"), dict):
        product = data["product"]
    else:
        product = {}
    customfields = data.get("customfields") if isinstance(data.get("customfields"), list) else []
    pricing_rows = data.get("product_pricings") if isinstance(data.get("product_pricings"), list) else []
    config_groups = data.get("config_groups") if isinstance(data.get("config_groups"), list) else []
    summarized_groups = [_summarize_zjmf_config_group(group) for group in config_groups if isinstance(group, dict)]
    first_pricing = pricing_rows[0] if pricing_rows and isinstance(pricing_rows[0], dict) else {}
    option_count = sum(len(group.get("options") or []) for group in summarized_groups)
    sub_option_count = sum(
        sum(int(option.get("sub_count") or 0) for option in group.get("options") or [])
        for group in summarized_groups
    )
    return {
        "product": {
            "id": _string_or_none(product.get("id")),
            "name": product.get("name"),
            "type": product.get("type"),
            "description": product.get("description"),
            "pay_method": product.get("pay_method"),
            "stock_control": product.get("stock_control"),
            "qty": product.get("qty"),
            "hidden": product.get("hidden"),
            "retired": product.get("retired"),
        },
        "currency": first_pricing.get("code"),
        "cycles": _extract_zjmf_pricing_cycles(pricing_rows),
        "customfields": [_summarize_zjmf_customfield(item) for item in customfields if isinstance(item, dict)],
        "customfield_count": len(customfields),
        "config_groups": summarized_groups,
        "config_group_count": len(summarized_groups),
        "config_option_count": option_count,
        "sub_option_count": sub_option_count,
    }


_ZJMF_PRICE_CYCLES = (
    ("monthly", "月付", "monthly", "msetupfee"),
    ("quarterly", "季付", "quarterly", "qsetupfee"),
    ("semi_annually", "半年付", "semiannually", "ssetupfee"),
    ("annually", "年付", "annually", "asetupfee"),
    ("biennially", "两年付", "biennially", "bsetupfee"),
    ("triennially", "三年付", "triennially", "tsetupfee"),
    ("one_time", "一次性", "onetime", "osetupfee"),
)


def _extract_zjmf_pricing_cycles(rows: list[Any]) -> list[dict[str, Any]]:
    out: list[dict[str, Any]] = []
    for row in rows:
        if not isinstance(row, dict):
            continue
        currency = row.get("code")
        for cycle, label, price_key, setup_key in _ZJMF_PRICE_CYCLES:
            price = _money_or_none(row.get(price_key))
            setup_fee = _money_or_none(row.get(setup_key))
            if price is None and setup_fee is None:
                continue
            out.append({
                "cycle": cycle,
                "label": label,
                "price": price,
                "setup_fee": setup_fee,
                "currency": currency,
            })
    return out


def _summarize_zjmf_customfield(item: dict[str, Any]) -> dict[str, Any]:
    return {
        "id": _string_or_none(item.get("id")),
        "fieldname": item.get("fieldname"),
        "fieldtype": item.get("fieldtype"),
        "required": _truthy(item.get("required")),
        "description": item.get("description"),
        "fieldoptions": item.get("fieldoptions"),
    }


def _summarize_zjmf_config_group(group: dict[str, Any]) -> dict[str, Any]:
    options = group.get("options") if isinstance(group.get("options"), list) else []
    return {
        "id": _string_or_none(group.get("id")),
        "name": group.get("name"),
        "description": group.get("description"),
        "options": [_summarize_zjmf_config_option(option) for option in options if isinstance(option, dict)],
    }


def _summarize_zjmf_config_option(option: dict[str, Any]) -> dict[str, Any]:
    subs = option.get("sub") if isinstance(option.get("sub"), list) else []
    return {
        "id": _string_or_none(option.get("id")),
        "option_name": option.get("option_name") or option.get("name"),
        "option_type": option.get("option_type"),
        "sub_count": len(subs),
        "subs": [
            {
                "id": _string_or_none(sub.get("id")),
                "option_name": sub.get("option_name"),
                "qty_minimum": sub.get("qty_minimum"),
                "qty_maximum": sub.get("qty_maximum"),
            }
            for sub in subs[:20]
            if isinstance(sub, dict)
        ],
    }


def _string_or_none(value: Any) -> str | None:
    if value in (None, ""):
        return None
    return str(value)


def _int_or_default(value: Any, default: int) -> int:
    try:
        return int(value)
    except (TypeError, ValueError):
        return default


def _truthy(value: Any) -> bool:
    if isinstance(value, bool):
        return value
    if isinstance(value, (int, float)):
        return value != 0
    if isinstance(value, str):
        return value.strip().lower() in {"1", "true", "yes", "on", "required"}
    return False


def _money_or_none(value: Any) -> float | None:
    if value in (None, ""):
        return None
    try:
        number = float(value)
    except (TypeError, ValueError):
        return None
    if number < 0:
        return None
    return number


async def _ensure_group(db: AsyncSession, group_id: int) -> None:
    res = await db.execute(select(ProductGroup.id).where(ProductGroup.id == group_id))
    if not res.scalar_one_or_none():
        raise HTTPException(status_code=400, detail="产品组不存在")


async def _ensure_cloud_server(db: AsyncSession, cs_id: int, api_type: str | None = None) -> None:
    if api_type == ProductApiType.PLUGIN.value:
        return
    res = await db.execute(select(CloudServer).where(CloudServer.id == cs_id))
    server = res.scalar_one_or_none()
    if not server:
        raise HTTPException(status_code=400, detail="云接口不存在")
    if api_type == ProductApiType.ZJMF_FINANCE.value:
        if server.server_type != CloudServerType.ZJMF_FINANCE.value:
            raise HTTPException(status_code=400, detail="魔方财务产品必须绑定魔方财务上游节点")
        return
    if api_type == ProductApiType.DCIM_CLOUD_IMAGE.value:
        if server.server_type != CloudServerType.DCIMCLOUD.value or server.account_type != CloudAccountType.ADMIN.value:
            raise HTTPException(status_code=400, detail="魔方云镜像开通必须绑定自营魔方云节点")
        return
    if api_type == ProductApiType.DCIM_CLOUD_RID.value:
        if server.server_type != CloudServerType.DCIMCLOUD.value or server.account_type != CloudAccountType.AGENT.value:
            raise HTTPException(status_code=400, detail="魔方云资源包开通必须绑定资源池魔方云节点")


async def _ensure_unique_sku(
    db: AsyncSession, sku: str, exclude_id: int | None = None
) -> None:
    stmt = select(func.count()).select_from(Product).where(Product.sku == sku)
    if exclude_id is not None:
        stmt = stmt.where(Product.id != exclude_id)
    cnt = await db.scalar(stmt)
    if cnt:
        raise HTTPException(status_code=400, detail=f"SKU 已存在：{sku}")


def _validate_api_binding(
    api_type: str, api_identifier: str | None, cloud_server_id: int | None
) -> None:
    if api_type == ProductApiType.NONE.value:
        return
    if api_type == ProductApiType.PLUGIN.value:
        plugin_id = str(api_identifier or "").strip()
        if not plugin_id:
            raise HTTPException(status_code=400, detail="插件开通产品必须选择开通插件")
        provision_plugin_service.ensure_plugin_exists(plugin_id)
        return
    if cloud_server_id is None:
        raise HTTPException(status_code=400, detail="选择上游对接方式时必须选 cloud_server")
    if api_type == ProductApiType.ZJMF_FINANCE.value and not str(api_identifier or "").strip():
        raise HTTPException(status_code=400, detail="魔方财务产品必须填写上游产品 ID")


def _validate_pricings(items: list[ProductPricingItem]) -> None:
    seen: set[str] = set()
    for it in items:
        if it.cycle in seen:
            raise HTTPException(status_code=400, detail=f"周期 {it.cycle} 重复")
        seen.add(it.cycle)


def _pricing_from_item(product_id: int, item: ProductPricingItem) -> ProductPricing:
    return ProductPricing(
        product_id=product_id,
        cycle=BillingCycle(item.cycle).value,
        price=item.price,
        original_price=item.original_price,
        setup_fee=item.setup_fee,
        is_enabled=item.is_enabled,
        sort_order=item.sort_order,
    )


def _issue(level: str, scope: str, message: str, suggestion: str | None = None) -> dict[str, Any]:
    return {
        "level": level,
        "scope": scope,
        "message": message,
        "suggestion": suggestion,
    }


def _issues_status(issues: list[dict[str, Any]]) -> str:
    if any(item["level"] == "error" for item in issues):
        return "error"
    if any(item["level"] == "warning" for item in issues):
        return "warning"
    return "pass"


def _readiness_payload(
    group: ProductGroup,
    rows: list[tuple[Product, list[ProductPricing]]],
    active_server_rows: list[tuple[Product, list[ProductPricing]]],
    group_issues: list[dict[str, Any]],
    product_results: list[dict[str, Any]],
) -> dict[str, Any]:
    error_count = sum(1 for item in group_issues if item["level"] == "error")
    warning_count = sum(1 for item in group_issues if item["level"] == "warning")
    for result in product_results:
        error_count += int(result.get("error_count") or 0)
        warning_count += int(result.get("warning_count") or 0)

    status = "pass"
    if error_count:
        status = "error"
    elif warning_count:
        status = "warning"

    os_options = group.os_options if isinstance(group.os_options, list) else []
    return {
        "group": {
            "id": group.id,
            "name": group.name,
            "slug": group.slug,
            "area": group.area,
            "node": group.node,
            "os_count": len(os_options),
        },
        "status": status,
        "summary": {
            "product_count": len(rows),
            "active_server_count": len(active_server_rows),
            "error_count": error_count,
            "warning_count": warning_count,
        },
        "issues": group_issues,
        "products": product_results,
    }


def _check_product_readiness(
    product: Product,
    pricings: list[ProductPricing],
    group_has_location: bool,
    cloud_server_map: dict[int, CloudServer],
) -> list[dict[str, Any]]:
    issues: list[dict[str, Any]] = []
    is_zjmf_finance = product.api_type == ProductApiType.ZJMF_FINANCE.value
    is_plugin = product.api_type == ProductApiType.PLUGIN.value

    enabled_pricings = [pricing for pricing in pricings if pricing.is_enabled]
    if not enabled_pricings:
        issues.append(_issue(
            "error",
            "pricing",
            f"产品 {product.name} 没有启用的价格周期。",
            "至少启用一个价格周期，否则前台无法选择购买时长。",
        ))
    elif all(float(pricing.price or 0) <= 0 for pricing in enabled_pricings):
        issues.append(_issue(
            "warning",
            "pricing",
            f"产品 {product.name} 的启用价格均为 0。",
            "如果不是免费产品，请检查价格配置。",
        ))

    if product.stock_policy == StockPolicy.TRACKED.value and int(product.stock or 0) <= 0:
        issues.append(_issue(
            "error",
            "stock",
            f"产品 {product.name} 使用库存限制，但当前库存为 0。",
            "补充库存或改为不限库存，否则结账会失败。",
        ))

    if not product.auto_provision:
        issues.append(_issue(
            "warning",
            "provision",
            f"产品 {product.name} 未启用自动开通。",
            "如需自动交付，请开启自动开通并绑定云接口。",
        ))
    else:
        if product.api_type == ProductApiType.NONE.value:
            issues.append(_issue(
                "error",
                "provision",
                f"产品 {product.name} 已启用自动开通，但 API 类型仍为 none。",
                "请选择魔方云对接方式。",
            ))
        if is_plugin:
            plugin_id = str(product.api_identifier or "").strip()
            if not plugin_id:
                issues.append(_issue(
                    "error",
                    "provision",
                    f"产品 {product.name} 已启用插件开通，但未选择开通插件。",
                    "在自动开通里选择 API 类型为 plugin，并填写插件 ID。",
                ))
            else:
                try:
                    provision_plugin_service.ensure_plugin_exists(plugin_id)
                except Exception as exc:
                    issues.append(_issue(
                        "error",
                        "provision",
                        f"产品 {product.name} 绑定的开通插件不存在：{plugin_id}",
                        str(exc),
                    ))
            return issues
        if product.cloud_server_id is None:
            issues.append(_issue(
                "error",
                "provision",
                f"产品 {product.name} 已启用自动开通，但未绑定云接口。",
                "选择一个可用的魔方云节点。",
            ))
        else:
            server = cloud_server_map.get(int(product.cloud_server_id))
            if not server:
                issues.append(_issue(
                    "error",
                    "provision",
                    f"产品 {product.name} 绑定的云接口不存在。",
                    "重新选择云接口。",
                ))
            else:
                if server.disabled:
                    issues.append(_issue(
                        "error",
                        "provision",
                        f"产品 {product.name} 绑定的云接口已禁用。",
                        "启用云接口或重新绑定其他云接口。",
                    ))
                if is_zjmf_finance and server.server_type != CloudServerType.ZJMF_FINANCE.value:
                    issues.append(_issue(
                        "error",
                        "provision",
                        f"产品 {product.name} 是魔方财务产品，但绑定的云接口不是魔方财务。",
                        "重新绑定 server_type=zjmf_finance 的上游节点。",
                    ))
                elif not is_zjmf_finance and server.server_type != CloudServerType.DCIMCLOUD.value:
                    issues.append(_issue(
                        "error",
                        "provision",
                        f"产品 {product.name} 是魔方云产品，但绑定的云接口不是魔方云。",
                        "重新绑定 server_type=dcimcloud 的上游节点。",
                    ))
                if not is_zjmf_finance and server.client_id in (None, "", 0, "0"):
                    issues.append(_issue(
                        "error",
                        "provision",
                        f"产品 {product.name} 绑定的云接口未配置 client_id。",
                        "魔方云创建实例需要 client 参数，请在云节点中配置客户 ID。",
                    ))

    config = sync_provision_config_specs(
        product.provision_config,
        vcpu=product.vcpu,
        memory=product.memory,
    ) or []
    config_keys = {_config_key(field) for field in config if isinstance(field, dict)}
    config_keys.discard("")

    if is_zjmf_finance:
        if not str(product.api_identifier or "").strip():
            issues.append(_issue(
                "error",
                "provision",
                f"产品 {product.name} 未绑定魔方财务上游产品 ID。",
                "在商品对接页绑定或重新创建该产品。",
            ))
        if not _has_config_value(config, "os"):
            issues.append(_issue(
                "error",
                "provision_config",
                f"产品 {product.name} 缺少魔方财务系统选项 os。",
                "在商品对接页重新拉取该产品配置，或在产品配置中补充 os 可选项。",
            ))
        return issues

    for key in _REQUIRED_PROVISION_KEYS:
        if not _has_config_value(config, key):
            issues.append(_issue(
                "error",
                "provision_config",
                f"产品 {product.name} 缺少开通参数 {key}。",
                _required_param_hint(key),
            ))

    for key in _RECOMMENDED_PROVISION_KEYS:
        if key not in config_keys:
            issues.append(_issue(
                "warning",
                "provision_config",
                f"产品 {product.name} 未明确配置 {key}。",
                "如无数据盘也建议配置为 0，避免上游接口默认值不符合预期。",
            ))

    ip_num = _config_number(config, "ip_num")
    if ip_num == 0 and not _has_config_value(config, "nat_acl_limit"):
        issues.append(_issue(
            "error",
            "provision_config",
            f"产品 {product.name} 为 NAT 产品，但缺少 nat_acl_limit。",
            "配置 NAT 转发/建站额度，或把 ip_num 改为独立 IP 数量。",
        ))

    if not group_has_location and not _has_config_location_selector(config):
        issues.append(_issue(
            "error",
            "provision_config",
            f"产品 {product.name} 缺少位置选择器。",
            "产品组填写 area/node，或在产品开通参数中配置 node/area/node_group/ip_group/ip。",
        ))

    return issues


def _config_key(field: dict[str, Any]) -> str:
    return str(field.get("def") or "").split("|", 1)[0].strip()


def _has_config_value(config: list[Any] | None, key: str) -> bool:
    if not isinstance(config, list):
        return False
    for field in config:
        if not isinstance(field, dict) or _config_key(field) != key:
            continue
        field_type = field.get("type", "fixed")
        if field_type == "fixed":
            return field.get("value") not in (None, "")
        if field_type == "slider":
            return field.get("slider_min") is not None
        if field_type == "user_select":
            return bool(field.get("options"))
        if field_type == "input":
            return True
    return False


def _config_number(config: list[Any] | None, key: str) -> int | None:
    if not isinstance(config, list):
        return None
    for field in config:
        if not isinstance(field, dict) or _config_key(field) != key:
            continue
        field_type = field.get("type", "fixed")
        raw = field.get("slider_min") if field_type == "slider" else field.get("value")
        if raw in (None, ""):
            return None
        try:
            return int(float(str(raw).split("|", 1)[0].strip()))
        except (TypeError, ValueError):
            return None
    return None


def _has_config_location_selector(config: list[Any] | None) -> bool:
    if not isinstance(config, list):
        return False
    return any(_has_config_value(config, key) for key in _LOCATION_SELECTOR_KEYS)


def _required_param_hint(key: str) -> str:
    hints = {
        "cpu": "填写产品 vCPU，系统会同步为 cpu 固定参数。",
        "memory": "填写产品内存，例如 2GB，系统会同步为 memory(MB) 固定参数。",
        "system_disk_size": "配置系统盘容量，固定值或滑块均可。",
        "in_bw": "配置上行带宽，固定值或滑块均可。",
        "out_bw": "配置下行带宽，固定值或滑块均可。",
        "ip_num": "配置 IP 数量；NAT/建站型产品通常填 0。",
    }
    return hints.get(key, "补充该开通参数。")
