import os  # 读取按需调试环境变量。
import re  # 导入正则模块用于从云 API 错误里提取被占用的固定私网 IP。
import time  # 导入时间模块用于轮询 Job 状态。 
import uuid  # 导入 UUID 模块用于生成幂等 Token。 
from huaweicloudsdkcore.auth.credentials import BasicCredentials  # 导入华为云 SDK 的基础鉴权对象。 
from huaweicloudsdkcore.exceptions.exceptions import ClientRequestException  # 导入 SDK 请求异常用于重试判断。 
from huaweicloudsdkcore.region.region import Region  # 导入区域对象用于设置服务端点。 
from huaweicloudsdkecs.v2 import EcsClient, CreateServersRequest, ShowJobRequest, ShowServerRequest  # 导入 ECS 客户端与请求模型。 
from huaweicloudsdkecs.v2 import NovaListAvailabilityZonesRequest  # 导入 ECS 可用区查询请求模型。
from huaweicloudsdkecs.v2 import ListServersDetailsRequest  # 导入 ECS 查询云服务器列表请求模型。 
from huaweicloudsdkims.v2 import ImsClient, ListImagesRequest  # 导入 IMS 客户端与镜像列表请求模型。 
from huaweicloudsdkvpc.v2 import VpcClient, ShowSubnetRequest, NeutronListPortsRequest  # 导入 VPC 客户端、子网与端口查询模型。 
from config import PROJECT_ID, get_runtime_ak_sk  # 导入鉴权参数用于构造客户端。 
from iam_service import get_project_id  # 导入自动获取项目 ID 的函数。 
from data import ECS_ENDPOINT_TEMPLATE, IMS_ENDPOINT_TEMPLATE, VPC_ENDPOINT_TEMPLATE  # 导入各服务端点模板。 
from data import ECS_NAME, ECS_COUNT, ECS_DRY_RUN, ECS_DESCRIPTION  # 导入 ECS 基本参数。 
from data import ECS_CHARGING_MODE, ECS_PERIOD_TYPE, ECS_PERIOD_NUM  # 导入包周期计费参数。 
from data import ECS_AUTO_RENEW, ECS_AUTO_PAY  # 导入自动续费与自动支付参数。 
from data import ECS_KEY_NAME, ECS_ADMIN_PASS, ECS_USER_DATA  # 导入 ECS 登录鉴权与用户数据参数。 
from data import ECS_IMAGE_ID, ECS_IMAGE_NAME, ECS_IMAGE_VISIBILITY  # 导入镜像选择参数。 
from data import ECS_CLIENT_TOKEN, ECS_WAIT_JOB, ECS_JOB_POLL_INTERVAL, ECS_JOB_TIMEOUT  # 导入 Job 轮询参数。 
from data import ECS_AVAILABILITY_ZONE, ECS_BATCH_CREATE_IN_MULTI_AZ  # 导入可用区与多 AZ 参数。 
from data import REGION_AVAILABILITY_ZONE_PREFERENCES  # 导入地区可用区优先级策略。
from data import ECS_SERVER_TAGS, ECS_METADATA  # 导入 ECS 标签与元数据参数。 
from data import ECS_DISABLE_SOURCE_CHECK, ECS_ALLOWED_ADDRESS_PAIR_IP  # 导入源目检查关闭配置。 
from data import ECS_AUTO_REUSE  # 导入 ECS 复用配置。
from data import runtime_resource_name  # 多套资源购买时生成独立 ECS 名称。
from data import LOG_ENABLED  # 导入日志开关用于输出进度。
from data import HUAWEI_DISK_UPGRADE_ORDER, HUAWEI_DISK_TYPE_LABELS  # 导入云硬盘售罄升档配置。
from retry_utils import retry_cloud_call  # 导入华为云临时网络异常重试工具。

try:
    from common.debug_billing import is_on_demand_debug_enabled
except Exception:
    def is_on_demand_debug_enabled(_cloud):  # type: ignore
        value = os.getenv("WAWA_ON_DEMAND_DEBUG_HUAWEI") or os.getenv("WAWA_DEBUG_BILLING_HUAWEI") or ""
        return value.strip().lower() in {"1", "true", "yes", "on", "enabled"}

_AZ_CACHE = {}  # 缓存本进程已查询过的可用区，避免同地区多次创建 ECS 时重复请求。


def _ecs_on_demand_debug_enabled():  # 判断华为云 ECS 是否启用按需计费调试模式。
    return bool(is_on_demand_debug_enabled("huawei"))


def _build_ecs_extendparam(region_id):  # 组装 ECS 计费扩展参数。
    if _ecs_on_demand_debug_enabled():
        return {
            "chargingMode": "postPaid",
            "regionID": region_id,
        }
    return {
        "chargingMode": ECS_CHARGING_MODE,
        "regionID": region_id,
        "periodType": ECS_PERIOD_TYPE,
        "periodNum": ECS_PERIOD_NUM,
        "isAutoRenew": ECS_AUTO_RENEW,
        "isAutoPay": ECS_AUTO_PAY,
    }


def _resolve_project_id(region_id):  # 根据配置或 IAM 自动解析项目 ID。 
    if PROJECT_ID and PROJECT_ID != "YOUR_PROJECT_ID":  # 若已手工配置项目 ID 则直接使用。 
        return PROJECT_ID  # 返回手工配置的项目 ID。 
    return get_project_id(region_id)  # 否则调用 IAM 接口自动获取项目 ID。 
def _build_ecs_client(region_id):  # 创建 ECS 客户端的函数。 
    project_id = _resolve_project_id(region_id)  # 获取当前区域对应的项目 ID。 
    ak, sk = get_runtime_ak_sk()  # 读取当前线程生效的 AK/SK。 
    creds = BasicCredentials(ak, sk).with_project_id(project_id)  # 使用 AK/SK 与项目 ID 构造鉴权对象。 
    endpoint = ECS_ENDPOINT_TEMPLATE.format(region_id=region_id)  # 根据区域拼接 ECS 服务端点。 
    region = Region(region_id, endpoint)  # 创建区域对象并绑定端点。 
    client = EcsClient.new_builder().with_credentials(creds).with_region(region).build()  # 构建 ECS 客户端实例。 
    return client  # 返回构建好的客户端对象。 
def _build_vpc_client(region_id):  # 创建 VPC 客户端的函数。 
    project_id = _resolve_project_id(region_id)  # 获取当前区域对应的项目 ID。 
    ak, sk = get_runtime_ak_sk()  # 读取当前线程生效的 AK/SK。 
    creds = BasicCredentials(ak, sk).with_project_id(project_id)  # 使用 AK/SK 与项目 ID 构造鉴权对象。 
    endpoint = VPC_ENDPOINT_TEMPLATE.format(region_id=region_id)  # 根据区域拼接 VPC 服务端点。 
    region = Region(region_id, endpoint)  # 创建区域对象并绑定端点。 
    client = VpcClient.new_builder().with_credentials(creds).with_region(region).build()  # 构建 VPC 客户端实例。 
    return client  # 返回构建好的客户端对象。 
def _build_ims_client(region_id):  # 创建 IMS 客户端的函数。 
    project_id = _resolve_project_id(region_id)  # 获取当前区域对应的项目 ID。 
    ak, sk = get_runtime_ak_sk()  # 读取当前线程生效的 AK/SK。 
    creds = BasicCredentials(ak, sk).with_project_id(project_id)  # 使用 AK/SK 与项目 ID 构造鉴权对象。 
    endpoint = IMS_ENDPOINT_TEMPLATE.format(region_id=region_id)  # 根据区域拼接 IMS 服务端点。 
    region = Region(region_id, endpoint)  # 创建区域对象并绑定端点。 
    client = ImsClient.new_builder().with_credentials(creds).with_region(region).build()  # 构建 IMS 客户端实例。 
    return client  # 返回构建好的客户端对象。 
def _resolve_image_id(region_id):  # 自动解析镜像 ID 的函数。 
    if ECS_IMAGE_ID:  # 若已配置镜像 ID 则直接使用。 
        return ECS_IMAGE_ID  # 返回手工配置的镜像 ID。 
    if not ECS_IMAGE_NAME:  # 若未配置镜像名称则无法自动查找。 
        raise ValueError("未配置 ECS_IMAGE_ID 或 ECS_IMAGE_NAME，无法选择镜像")  # 抛出异常提示配置。 
    client = _build_ims_client(region_id)  # 构建 IMS 客户端用于查询镜像。 
    request = ListImagesRequest(name=ECS_IMAGE_NAME, visibility=ECS_IMAGE_VISIBILITY)  # 使用名称与可见性过滤镜像列表。 
    response = retry_cloud_call("查询IMS镜像", lambda: client.list_images(request))  # 调用 IMS 查询镜像列表。
    for image in response.images or []:  # 优先匹配名称完全相同的镜像。 
        if image.name == ECS_IMAGE_NAME:  # 若名称完全匹配则直接返回。 
            return image.id  # 返回匹配到的镜像 ID。 
    if response.images:  # 若存在候选镜像则返回第一条。 
        return response.images[0].id  # 返回首个镜像 ID 作为兜底选择。 
    raise RuntimeError("未找到匹配的镜像，请检查 ECS_IMAGE_NAME 或镜像可见性")  # 未找到则抛出异常。 
def _resolve_vpc_id_from_subnet(region_id, subnet_id):  # 通过子网查询 VPC ID 的函数。 
    if not subnet_id:  # 若未提供子网 ID 则无法查询。 
        raise ValueError("subnet_id 不能为空")  # 抛出异常提示调用方补齐参数。 
    client = _build_vpc_client(region_id)  # 构建 VPC 客户端用于查询子网。 
    request = ShowSubnetRequest(subnet_id=subnet_id)  # 构造子网查询请求对象。 
    response = retry_cloud_call("查询子网详情", lambda: client.show_subnet(request))  # 调用 SDK 查询子网详情。
    return response.subnet.vpc_id  # 返回子网所属的 VPC ID。 
def _build_nics(subnet_id, spec):  # 组装网卡参数列表的函数。 
    nics = []  # 初始化网卡列表。 
    main_nic = {  # 构建主网卡参数。 
        "subnet_id": subnet_id,  # 主网卡所属子网 ID。 
        "ip_address": spec.get("main_nic_ip"),  # 主网卡固定 IP 地址。 
    }  # 主网卡参数构建结束。 
    if ECS_DISABLE_SOURCE_CHECK:  # 若需要关闭源目的地址检查。 
        main_nic["allowed_address_pairs"] = [{"ip_address": ECS_ALLOWED_ADDRESS_PAIR_IP}]  # 使用允许地址对关闭源目检查。 
    nics.append(main_nic)  # 将主网卡加入网卡列表。 
    if spec.get("extra_nic_count", 0) > 0:  # 若规格要求额外网卡。 
        secondary_nic = {  # 构建副网卡参数。 
            "subnet_id": subnet_id,  # 副网卡所属子网 ID。 
            "ip_address": spec.get("secondary_nic_ip"),  # 副网卡固定 IP 地址。 
        }  # 副网卡参数构建结束。 
        nics.append(secondary_nic)  # 将副网卡加入网卡列表。 
    return nics  # 返回网卡参数列表。 
def _zone_is_available(zone_state):  # 兼容 SDK 对象和 dict，判断可用区是否可用于中心资源创建。
    if zone_state is None:
        return True
    if isinstance(zone_state, dict):
        return zone_state.get("available") is not False
    return getattr(zone_state, "available", True) is not False


def _parse_availability_zone_items(raw_zones):  # 从 ECS 可用区接口响应里提取可用区名称。
    zones = []
    for item in raw_zones or []:
        if isinstance(item, dict):
            zone_name = item.get("zone_name") or item.get("zoneName") or item.get("availability_zone_id") or item.get("name")
            zone_state = item.get("zone_state") or item.get("zoneState")
            zone_type = str(item.get("type") or "").lower()
        else:
            zone_name = getattr(item, "zone_name", "") or getattr(item, "zoneName", "") or getattr(item, "name", "")
            zone_state = getattr(item, "zone_state", None) or getattr(item, "zoneState", None)
            zone_type = str(getattr(item, "type", "") or "").lower()
        if not zone_name or not _zone_is_available(zone_state):
            continue
        if zone_type and zone_type != "center":
            continue
        zones.append(str(zone_name))
    return list(dict.fromkeys(zones))


def _list_availability_zones(region_id):  # 实时查询地区可用区，供 ECS 创建失败时切换 AZ 使用。
    if region_id in _AZ_CACHE:
        return list(_AZ_CACHE[region_id])
    try:
        client = _build_ecs_client(region_id)
        request = NovaListAvailabilityZonesRequest()
        response = retry_cloud_call("查询ECS可用区", lambda: client.nova_list_availability_zones(request), attempts=1)
        raw_zones = getattr(response, "availability_zone_info", None)
        if raw_zones is None and hasattr(response, "to_dict"):
            raw_zones = (response.to_dict() or {}).get("availability_zone_info")
        zones = _parse_availability_zone_items(raw_zones)
    except Exception as exc:
        zones = []
        if LOG_ENABLED:
            print(f"步骤: ECS可用区查询失败，将使用自动AZ兜底: {exc}")
    _AZ_CACHE[region_id] = list(zones)
    return list(zones)


def _availability_zone_candidates(region_id):  # 生成 ECS 创建失败后的显式可用区候选。
    candidates = []
    if ECS_AVAILABILITY_ZONE:
        candidates.append(str(ECS_AVAILABILITY_ZONE).strip())
    # 中文注释：首轮购买不走这里，仍旧不传 availability_zone；只有首轮失败后才查询并切换 AZ，避免提前固定到某个可用区。
    candidates.extend(REGION_AVAILABILITY_ZONE_PREFERENCES.get(region_id, []))
    candidates.extend(_list_availability_zones(region_id))
    normalized = []
    for value in candidates:
        item = str(value).strip()
        if item:
            normalized.append(item)
    return list(dict.fromkeys(normalized))


def _zone_label(availability_zone):  # 统一可用区展示文本。
    return availability_zone or "自动AZ"


def _exception_text(exc):  # 汇总 SDK 异常内常见字段，便于匹配华为云业务错误文案。
    parts = [str(exc or "")]
    for name in ("error_msg", "error_code", "body", "request_id"):
        value = getattr(exc, name, None)
        if value:
            parts.append(str(value))
    return " ".join(parts).lower()


def _is_availability_zone_retry_error(exc):  # 判断是否适合换可用区重试创建 ECS。
    text = _exception_text(exc)
    if "ecs.0010" in text:
        return False
    # 中文注释：只在 ECS 创建阶段识别容量、售罄、可用区、主机 RAM/内存页不足等业务错误；网络断连交给 retry_cloud_call，不在这里换 AZ，避免重复下单。
    retry_tokens = (
        "availability zone",
        "available zone",
        "可用区",
        "az",
        "capacity",
        "insufficient",
        "not enough",
        "resource not enough",
        "resources are not enough",
        "资源不足",
        "容量不足",
        "主机",
        "内存页",
        "ram",
        "memory",
        "sold out",
        "sell out",
        "not available",
        "not supported",
        "abandoned",
        "ecs.0008",
        "ecs.0013",
        "ecs.0019",
    )
    return any(token in text for token in retry_tokens)


def _build_server_body(region_id, subnet_id, security_group_id, spec, availability_zone=None):  # 组装云服务器请求体的函数。 
    image_id = _resolve_image_id(region_id)  # 解析镜像 ID。 
    vpc_id = _resolve_vpc_id_from_subnet(region_id, subnet_id)  # 通过子网解析 VPC ID。 
    server_body = {  # 创建云服务器请求体字典。 
        "name": runtime_resource_name(ECS_NAME),  # 云服务器名称字段。 
        "imageRef": image_id,  # 镜像 ID 字段。 
        "flavorRef": spec.get("flavor"),  # 规格 ID 字段。 
        "vpcid": vpc_id,  # VPC ID 字段。 
        "nics": _build_nics(subnet_id, spec),  # 网卡参数字段。 
        "security_groups": [{"id": security_group_id}],  # 安全组字段。 
        "root_volume": {  # 系统盘配置字段。 
            "volumetype": spec.get("disk_type"),  # 系统盘类型字段。 
            "size": spec.get("disk_size_gb"),  # 系统盘大小字段。 
        },  # 系统盘配置结束。 
        "count": ECS_COUNT,  # 创建数量字段。 
        "isAutoRename": False,  # 多台创建时是否使用同名。 
        "availability_zone": availability_zone or ECS_AVAILABILITY_ZONE,  # 可用区字段，可选。 
        "batch_create_in_multi_az": ECS_BATCH_CREATE_IN_MULTI_AZ,  # 多 AZ 随机部署开关。 
        "description": ECS_DESCRIPTION,  # 描述字段。 
        "extendparam": _build_ecs_extendparam(region_id),  # 计费扩展参数，按需调试时不传包周期字段。 
        "server_tags": ECS_SERVER_TAGS,  # 标签字段。 
        "metadata": ECS_METADATA,  # 元数据字段。 
    }  # 云服务器请求体字典构建结束。 
    if ECS_KEY_NAME:  # 若配置了密钥对则优先使用密钥登录。 
        server_body["key_name"] = ECS_KEY_NAME  # 设置密钥名称字段。 
    elif ECS_USER_DATA:  # 若配置了 user_data 则优先注入用户数据。 
        server_body["user_data"] = ECS_USER_DATA  # 设置用户数据字段。 
    else:  # 未配置密钥与 user_data 时使用密码登录。 
        server_body["adminPass"] = ECS_ADMIN_PASS  # 设置管理员密码字段。 
    filtered_body = {k: v for k, v in server_body.items() if v not in (None, "", [], {})}  # 过滤空值字段避免接口报错。 
    return filtered_body  # 返回过滤后的请求体字典。 
def _wait_job_success(region_id, job_id):  # 轮询 Job 状态直到成功或失败。 
    client = _build_ecs_client(region_id)  # 构建 ECS 客户端用于查询 Job。 
    start_time = time.time()  # 记录轮询开始时间。 
    while True:  # 持续轮询直到超时或结束。 
        response = retry_cloud_call("查询ECS任务", lambda: client.show_job(ShowJobRequest(job_id=job_id)))  # 查询 Job 状态。
        if LOG_ENABLED:  # 若开启日志输出则提示当前 Job 状态。 
            print(f"步骤: 任务 {job_id} 状态={response.status}")  # 输出 Job 状态日志。 
        if response.status == "SUCCESS":  # 若 Job 成功则返回 server_id。 
            if response.entities and response.entities.server_id:  # 若主实体包含 server_id 则直接返回。 
                return response.entities.server_id  # 返回 Job 关联的 server_id。 
            sub_jobs = response.entities.sub_jobs if response.entities else []  # 获取子任务列表并处理空值。 
            for sub_job in sub_jobs or []:  # 轮询子任务列表尝试获取 server_id。 
                if sub_job.entities and sub_job.entities.server_id:  # 若子任务实体包含 server_id。 
                    return sub_job.entities.server_id  # 返回子任务中的 server_id。 
            if LOG_ENABLED:  # 若开启日志输出则提示未获取到 server_id。 
                print("步骤: 任务成功但无server_id")  # 输出 Job 成功但无 server_id 的提示。 
            return None  # 若未获取到 server_id 则返回空值。 
        if response.status in ("FAIL", "FAILURE", "ERROR"):  # 若 Job 失败则抛出异常。 
            raise RuntimeError(f"创建云服务器失败: {response.error_code} {response.message}")  # 失败时输出错误信息。 
        if time.time() - start_time > ECS_JOB_TIMEOUT:  # 若超时则终止轮询。 
            raise TimeoutError("创建云服务器超时未完成")  # 抛出超时异常提示。 
        time.sleep(ECS_JOB_POLL_INTERVAL)  # 等待指定间隔后继续轮询。 
def _find_server_by_ip(region_id, ip_address):  # 按固定 IP 查找云服务器的函数。 
    if not ip_address:  # 若未提供 IP 则无法查找。 
        return None  # 返回空值表示未找到。 
    client = _build_ecs_client(region_id)  # 构建 ECS 客户端用于查询。 
    request = ListServersDetailsRequest(ip_eq=ip_address)  # 使用精确 IP 过滤查询云服务器。 
    response = retry_cloud_call("按IP查询ECS", lambda: client.list_servers_details(request))  # 调用 SDK 查询云服务器列表。
    for server in response.servers or []:  # 遍历服务器列表尝试匹配。 
        return server.id  # 返回首个匹配的服务器 ID。 
    return None  # 未找到匹配项则返回空值。 


def _private_ips_from_in_use_error(exc):  # 从 Ecs.0010 错误文本中提取被占用的固定私网 IP。
    text = str(exc or "")
    found = re.findall(r"privateIp\s*\[([0-9]{1,3}(?:\.[0-9]{1,3}){3})\]", text, flags=re.I)
    if not found:
        found = re.findall(r"\b192\.168\.0\.[0-9]{1,3}\b", text)
    return list(dict.fromkeys(found))


def _port_id(port):  # 兼容 SDK 端口对象读取端口 ID。
    return str(getattr(port, "id", "") or "")


def _port_owner(port):  # 兼容 SDK 端口对象读取设备归属。
    return str(getattr(port, "device_owner", "") or "")


def _port_device_id(port):  # 兼容 SDK 端口对象读取设备 ID；ECS 标准网卡通常就是 server_id。
    return str(getattr(port, "device_id", "") or "")


def _port_fixed_ips(port):  # 兼容 SDK 端口对象读取固定私网 IP 列表。
    values = []
    for item in getattr(port, "fixed_ips", None) or []:
        ip_address = str(getattr(item, "ip_address", "") or "")
        if ip_address:
            values.append(ip_address)
    return values


def _resolve_subnet_network_id(region_id, subnet_id):  # 通过子网查询 neutron network_id，端口反查必须使用 network_id。
    if not subnet_id:
        return ""
    client = _build_vpc_client(region_id)
    response = retry_cloud_call("查询子网网络ID", lambda: client.show_subnet(ShowSubnetRequest(subnet_id=subnet_id)))
    subnet = response.subnet
    return (
        getattr(subnet, "neutron_network_id", "")
        or getattr(subnet, "network_id", "")
        or subnet_id
    )


def _show_reusable_server_for_occupied_ip(region_id, server_id, ip_address):  # 校验占用固定 IP 的设备确实是可复用 ECS。
    if not server_id:
        return ""
    client = _build_ecs_client(region_id)
    response = retry_cloud_call("校验占用IP归属ECS", lambda: client.show_server(ShowServerRequest(server_id=server_id)))
    server = response.server
    name = str(getattr(server, "name", "") or "")
    status = str(getattr(server, "status", "") or "")
    # 中文注释：只把本工具命名的 ECS 当作自动复用对象；其它业务 ECS 即使占用了固定 IP，也不能擅自拿来搭建。
    if name and not any(token in name.lower() for token in ("wawacloud", "wtdiy")):
        if LOG_ENABLED:
            print(f"地区:{region_id} 固定IP {ip_address} 归属ECS {server_id}({name})，名称非本工具资源，不自动复用。")
        return ""
    if status and status.upper() not in {"ACTIVE", "RUNNING"}:
        if LOG_ENABLED:
            print(f"地区:{region_id} 固定IP {ip_address} 归属ECS {server_id}({name or '未命名'})，状态={status}，暂不复用。")
        return ""
    if LOG_ENABLED:
        print(f"地区:{region_id} 固定IP {ip_address} 归属服务器ID:{server_id} name={name or '未命名'} status={status or '未知'}，按旧资源复用。")
    return server_id


def _find_server_by_occupied_private_ip(region_id, subnet_id, ip_address):  # 固定 IP 被占用时反查端口归属 ECS。
    if not ip_address:
        return None
    try:
        network_id = _resolve_subnet_network_id(region_id, subnet_id)
        client = _build_vpc_client(region_id)
        request = NeutronListPortsRequest(network_id=network_id or None, limit=2000)
        response = retry_cloud_call("按固定IP反查端口", lambda: client.neutron_list_ports(request))
    except Exception as exc:
        if LOG_ENABLED:
            print(f"地区:{region_id} 固定IP {ip_address} 占用归属反查失败: {exc}")
        return None
    for port in response.ports or []:
        if ip_address not in _port_fixed_ips(port):
            continue
        port_id = _port_id(port)
        owner = _port_owner(port)
        device_id = _port_device_id(port)
        if LOG_ENABLED:
            print(f"地区:{region_id} 固定IP {ip_address} 已被端口 {port_id or '未知'} 占用 owner={owner or '未知'} device_id={device_id or '空'}")
        if not device_id:
            return None
        try:
            reusable_id = _show_reusable_server_for_occupied_ip(region_id, device_id, ip_address)
        except Exception as exc:
            if LOG_ENABLED:
                print(f"地区:{region_id} 固定IP {ip_address} 端口 {port_id or '未知'} 的 device_id={device_id} 不是可查询ECS: {exc}")
            return None
        if reusable_id:
            return reusable_id
    return None
def _is_volume_sell_out_error(exc):  # 判断是否为云硬盘库存售罄错误。 
    text = str(exc).lower()  # 标准化异常文本便于匹配。 
    return "ecs.0044" in text or "volume is sell out" in text or "volume sold out" in text  # 匹配华为云售罄错误。 
def _disk_type_label(disk_type):  # 获取云硬盘类型展示名称。 
    key = (disk_type or "").strip().upper()  # 标准化云硬盘类型。 
    label = HUAWEI_DISK_TYPE_LABELS.get(key, key or "未知盘型")  # 查找中文名称。 
    return f"{label}({key})" if key else label  # 返回带 API 值的展示名称。 
def _disk_upgrade_candidates(disk_type):  # 根据当前盘型生成升档候选列表。 
    current = (disk_type or "").strip().upper()  # 标准化当前盘型。 
    order = [item.strip().upper() for item in HUAWEI_DISK_UPGRADE_ORDER if item]  # 标准化升档顺序。 
    if current in order:  # 若当前盘型在升档链内。 
        return order[order.index(current):]  # 从当前盘型开始依次尝试更高级盘型。 
    if current:  # 若是未知盘型则不擅自替换。 
        return [current]  # 仅尝试当前盘型。 
    return order  # 未指定盘型时从最低级开始尝试。 
def _flavor_candidates(spec):  # 生成 ECS 规格候选；20 模式当前固定 s7.small.1，保留列表结构便于旧模式复用。
    candidates = spec.get("flavor_candidates") or [spec.get("flavor")]  # 默认沿用原规格，避免影响旧模式。
    return [item for item in dict.fromkeys(str(value).strip() for value in candidates) if item]  # 去重并清理空值。
def _is_flavor_unavailable_error(exc):  # 判断是否为规格售罄/不可用；只有配置了多个候选时才会切到下一项。
    text = str(exc).lower()  # 标准化异常文本便于匹配。
    return any(token in text for token in ("flavor", "ecs.0008", "ecs.0013", "sold out", "sell out", "not available", "not supported"))  # 匹配常见规格不可用文案。
def purchase_server(region_id, subnet_id, security_group_id, spec):  # 创建云服务器的函数实现。 
    if ECS_AUTO_REUSE:  # 若开启复用则优先按固定 IP 查找。 
        existed_id = _find_server_by_occupied_private_ip(region_id, subnet_id, spec.get("main_nic_ip"))  # 仅在本资源组子网内查找已有服务器。 
        if existed_id:  # 若找到则直接返回已有实例 ID。 
            return existed_id  # 复用已有实例避免重复创建失败。 
    client = _build_ecs_client(region_id)  # 构建 ECS 客户端以调用创建接口。 
    disk_candidates = _disk_upgrade_candidates(spec.get("disk_type"))  # 计算云硬盘售罄时的升档候选。 
    flavor_candidates = _flavor_candidates(spec)  # 按当前模式读取候选规格，20 模式只有 s7.small.1。 
    az_candidates = [""]  # 首次创建保持旧逻辑：不传 availability_zone，由华为云自动选择。
    fallback_az_loaded = False  # 首轮失败后才查询显式可用区候选，避免提前固定到某个 AZ。
    response = None  # 初始化响应对象，成功创建后赋值。 
    last_create_error = None  # 记录最后一次创建失败，所有候选都失败时抛出。 
    if LOG_ENABLED and _ecs_on_demand_debug_enabled():
        print("步骤: 华为云按需计费调试模式已生效，ECS使用postPaid按需计费。")
    for flavor_index, flavor_id in enumerate(flavor_candidates):  # 按候选规格顺序尝试；20 模式不会自动切到 2c2g。 
        switch_flavor = False  # 当前规格在所有可用区均不可用时才切换规格候选。 
        for az_index, availability_zone in enumerate(az_candidates):  # 同一规格优先切换可用区，避免单 AZ 容量不足导致整地区失败。
            switch_zone = False  # 当前可用区资源不足时切换到下一个可用区。
            for index, disk_type in enumerate(disk_candidates):  # 每个可用区下继续沿用系统盘售罄升档逻辑。 
                attempt_spec = dict(spec)  # 复制规格，避免重试时污染调用方配置。 
                attempt_spec["flavor"] = flavor_id  # 设置本轮尝试的 ECS 规格。 
                if disk_type:  # 若存在候选盘型则覆盖当前尝试盘型。 
                    attempt_spec["disk_type"] = disk_type  # 设置本次尝试的系统盘类型。 
                if LOG_ENABLED:  # 若开启日志则输出本次尝试规格、可用区和盘型。 
                    print(f"步骤: 尝试可用区 {_zone_label(availability_zone)} ECS规格 {flavor_id} 系统盘 {_disk_type_label(attempt_spec.get('disk_type'))} {attempt_spec.get('disk_size_gb')}GB")  # 输出可用区、规格、盘型与大小。 
                server_body = _build_server_body(region_id, subnet_id, security_group_id, attempt_spec, availability_zone)  # 构建云服务器请求体。 
                request = CreateServersRequest()  # 创建服务器请求对象。 
                request.body = {"server": server_body, "dry_run": ECS_DRY_RUN}  # 填充请求体与预检开关。 
                request.x_client_token = ECS_CLIENT_TOKEN or str(uuid.uuid4())  # 不同可用区/规格/盘型必须使用新 Token，避免幂等命中旧请求体。 
                try:  # 捕获创建云服务器时的可恢复错误。 
                    response = retry_cloud_call("创建ECS", lambda: client.create_servers(request))  # 调用 SDK 创建云服务器。
                    if availability_zone and LOG_ENABLED:  # 若传入可用区则输出最终选中的 AZ，方便和控制台核对。 
                        print(f"步骤: ECS可用区选定 {_zone_label(availability_zone)}")  # 输出最终可用区。 
                    if flavor_id != spec.get("flavor") and LOG_ENABLED:  # 若发生了规格降级/切换。 
                        print(f"步骤: ECS规格自动切换为 {flavor_id}")  # 输出最终规格结果。 
                    if disk_type != (spec.get("disk_type") or "").strip().upper() and LOG_ENABLED:  # 若发生了自动升档。 
                        print(f"步骤: 系统盘自动升档为 {_disk_type_label(disk_type)}")  # 输出最终升档结果。 
                    break  # 创建请求成功后跳出盘型循环。 
                except ClientRequestException as exc:  # 捕获请求异常用于复用、可用区切换、规格切换和云硬盘升档处理。 
                    last_create_error = exc  # 保存失败原因供兜底抛出。 
                    if "Ecs.0010" in str(exc):  # 私网 IP 已被占用时尝试复用。 
                        # 中文注释：10IP/20规格常见冲突点是第二张标准网卡 192.168.0.11；先反查占用 IP 的端口归属，
                        # 如果确认属于本工具旧 ECS，就把 server_id 交给后续网卡/EIP 补齐流程，避免重复购买和循环失败。
                        lookup_ips = _private_ips_from_in_use_error(exc)
                        for expected_ip in (spec.get("main_nic_ip"), spec.get("secondary_nic_ip")):
                            if expected_ip and expected_ip not in lookup_ips:
                                lookup_ips.append(expected_ip)
                        for occupied_ip in lookup_ips:
                            existed_id = _find_server_by_occupied_private_ip(region_id, subnet_id, occupied_ip)
                            if existed_id:
                                return existed_id
                        existed_id = _find_server_by_ip(region_id, spec.get("main_nic_ip"))  # 按主网卡 IP 查找已有服务器。 
                        if existed_id:  # 若找到则返回已有实例 ID。 
                            return existed_id  # 复用已有实例避免创建失败。 
                    if _is_volume_sell_out_error(exc) and index < len(disk_candidates) - 1:  # 云硬盘售罄且还有更高级盘型。 
                        next_disk_type = disk_candidates[index + 1]  # 获取下一档盘型。 
                        if LOG_ENABLED:  # 若开启日志则输出升档原因。 
                            print(f"步骤: 系统盘 {_disk_type_label(disk_type)} 库存售罄，自动升档到 {_disk_type_label(next_disk_type)}")  # 输出升档日志。 
                        continue  # 尝试下一档盘型。 
                    if _is_availability_zone_retry_error(exc):  # 当前自动AZ或显式可用区创建失败时，尝试切换其它 AZ。
                        if az_index >= len(az_candidates) - 1 and not fallback_az_loaded:
                            fallback_az_loaded = True
                            fallback_zones = _availability_zone_candidates(region_id)
                            for zone in fallback_zones:
                                if zone not in az_candidates:
                                    az_candidates.append(zone)
                            if LOG_ENABLED and fallback_zones:
                                print(f"步骤: 首轮自动AZ创建失败，改用可用区候选 {', '.join(_zone_label(item) for item in fallback_zones)}")
                        if az_index < len(az_candidates) - 1:
                            next_zone = az_candidates[az_index + 1]  # 获取下一可用区候选。 
                            if LOG_ENABLED:  # 若开启日志则输出可用区切换原因。 
                                print(f"步骤: 可用区 {_zone_label(availability_zone)} 创建失败，切换到 {_zone_label(next_zone)}")  # 输出可用区切换日志。 
                            switch_zone = True  # 标记切换可用区。 
                            break  # 跳出盘型循环，进入下一个可用区。 
                    if _is_flavor_unavailable_error(exc) and flavor_index < len(flavor_candidates) - 1:  # 当前 ECS 规格不可用且还有候选。 
                        if LOG_ENABLED:  # 若开启日志则输出切换原因。 
                            print(f"步骤: ECS规格 {flavor_id} 不可用，切换到 {flavor_candidates[flavor_index + 1]}")  # 输出规格切换日志。 
                        switch_flavor = True  # 标记切到下一个规格候选。 
                        break  # 跳出盘型循环。 
                    raise  # 非预期错误或已无候选则直接抛出。 
            if response is not None:  # 已成功创建则不再尝试其他可用区。 
                break  # 跳出可用区循环。 
            if switch_flavor:  # 当前规格需要切换候选规格。 
                break  # 跳出可用区循环，进入下一个规格。 
            if switch_zone:  # 当前可用区资源不足，继续下一个可用区。 
                continue  # 继续可用区循环。 
        if response is not None:  # 已成功创建则不再尝试其他规格。 
            break  # 跳出规格循环。 
    if response is None and last_create_error:  # 理论兜底：所有候选都失败。 
        raise last_create_error  # 抛出最后一次创建错误。 
    if response.server_ids:  # 若响应中包含 server_ids 则返回首个。 
        if LOG_ENABLED:  # 若开启日志输出则提示返回 server_ids。 
            print(f"步骤: 服务器ID列表={response.server_ids}")  # 输出 server_ids 日志。 
        return response.server_ids[0]  # 返回首个 server_id 用于后续流程。 
    if LOG_ENABLED:  # 若开启日志输出则提示 job_id/order_id。 
        print(f"步骤: 任务ID={response.job_id} 订单ID={response.order_id}")  # 输出 job_id 与 order_id 日志。 
    if ECS_WAIT_JOB and response.job_id:  # 若需要等待 Job 则优先轮询获取 server_id。 
        job_server_id = _wait_job_success(region_id, response.job_id)  # 获取 Job 完成后的 server_id。 
        if job_server_id:  # 若成功获取到 server_id。 
            return job_server_id  # 返回 Job 成功后的 server_id。 
    existed_id = _find_server_by_ip(region_id, spec.get("main_nic_ip"))  # 兜底按 IP 查找服务器。 
    if existed_id:  # 若兜底找到服务器则返回。 
        return existed_id  # 返回兜底获取的 server_id。 
    raise RuntimeError("未获取到 server_id，请检查响应或开启 ECS_WAIT_JOB")  # 若无 server_id 则抛出异常。 
