import time  # 导入时间模块用于重试等待。 
import socket  # 导入 socket 模块用于 DNS 解析测试。 
from urllib.parse import urlparse  # 导入 URL 解析工具用于提取域名。 
from huaweicloudsdkcore.auth.credentials import BasicCredentials  # 导入华为云 SDK 的基础鉴权对象。 
from huaweicloudsdkcore.region.region import Region  # 导入区域对象用于设置服务端点。 
from huaweicloudsdkcore.exceptions.exceptions import ClientRequestException, ConnectionException  # 导入 SDK 异常用于重试判断。 
from huaweicloudsdkeip.v2 import EipClient, CreatePublicipRequest, UpdatePublicipRequest  # 导入 EIP 客户端与请求模型。 
from huaweicloudsdkeip.v2 import ListPublicipsRequest  # 导入 EIP 列表查询请求模型。 
from huaweicloudsdkecs.v2 import EcsClient, ShowServerRequest  # 导入 ECS 客户端与查询服务器请求模型。 
from config import PROJECT_ID, get_runtime_ak_sk  # 导入鉴权参数用于构造客户端。 
from iam_service import get_project_id  # 导入自动获取项目 ID 的函数。 
from data import EIP_ENDPOINT_TEMPLATES  # 导入 EIP 服务端点模板列表。 
from data import EIP_TYPE, STATIC_BGP_EIP_TYPE, STATIC_BGP_REGION_IDS, EIP_IP_VERSION  # 导入 EIP 类型与 IP 版本配置。 
from data import EIP_BANDWIDTH_SIZE, EIP_BANDWIDTH_SHARE_TYPE, EIP_BANDWIDTH_CHARGE_MODE  # 导入带宽配置。 
from data import EIP_BANDWIDTH_NAME  # 导入带宽名称配置。 
from data import EIP_ENTERPRISE_PROJECT_ID  # 导入 EIP 企业项目配置。 
from data import EIP_AUTO_REUSE  # 导入 EIP 复用配置。
from data import ECS_ENDPOINT_TEMPLATE  # 导入 ECS 服务端点模板用于查询网卡。
from retry_utils import is_transient_cloud_error, retry_cloud_call  # 导入华为云临时网络异常重试工具。
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 _iter_eip_endpoints(region_id):  # 生成可用 EIP 端点列表的函数。 
    templates = EIP_ENDPOINT_TEMPLATES  # 默认使用通用端点模板列表。 
    if region_id == "cn-south-1":  # 广州区域优先使用 VPC 端点避免连接重置。 
        templates = ["https://vpc.{region_id}.myhuaweicloud.com"]  # 仅使用 VPC 端点模板。 
    for template in templates:  # 按优先级遍历端点模板。 
        endpoint = template.format(region_id=region_id)  # 使用区域拼接完整端点。 
        host = urlparse(endpoint).hostname  # 解析端点域名用于 DNS 测试。 
        if not host:  # 若无法解析域名则跳过该端点。 
            continue  # 继续尝试下一个端点模板。 
        try:  # 捕获 DNS 解析错误以便降级。 
            socket.gethostbyname(host)  # 尝试解析域名判断是否可用。 
            yield endpoint  # 解析成功则产出该端点。 
        except OSError:  # DNS 解析失败时继续尝试下一个端点。 
            continue  # 跳过当前端点模板。 
def _build_eip_client(region_id, endpoint):  # 创建 EIP 客户端的函数。 
    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 构造鉴权对象。 
    region = Region(region_id, endpoint)  # 创建区域对象并绑定端点。 
    client = EipClient.new_builder().with_credentials(creds).with_region(region).build()  # 构建 EIP 客户端实例。 
    return client  # 返回构建好的客户端对象。 
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 _eip_type_for_region(region_id):  # 根据主程序传入的地区列表决定是否使用静态 BGP。
    return STATIC_BGP_EIP_TYPE if region_id in STATIC_BGP_REGION_IDS else EIP_TYPE


def _build_publicip_body(region_id=None):  # 组装 EIP 创建请求体的函数。 
    publicip_body = {  # 创建 EIP 的 publicip 字段。 
        "type": _eip_type_for_region(region_id),  # EIP 类型字段。 
        "ip_version": EIP_IP_VERSION,  # EIP IP 版本字段。 
    }  # publicip 字段构建结束。 
    bandwidth_body = {  # 创建带宽配置字段。 
        "size": EIP_BANDWIDTH_SIZE,  # 带宽大小字段。 
        "share_type": EIP_BANDWIDTH_SHARE_TYPE,  # 带宽共享类型字段。 
        "charge_mode": EIP_BANDWIDTH_CHARGE_MODE,  # 带宽计费方式字段。 
        "name": EIP_BANDWIDTH_NAME,  # 带宽名称字段。 
    }  # 带宽配置构建结束。 
    request_body = {  # 创建 EIP 请求体字典。 
        "publicip": publicip_body,  # 设置 publicip 字段。 
        "bandwidth": bandwidth_body,  # 设置 bandwidth 字段。 
        "enterprise_project_id": EIP_ENTERPRISE_PROJECT_ID,  # 设置企业项目字段。 
    }  # 请求体构建结束。 
    return request_body  # 返回 EIP 请求体。 
def _list_unbound_eips(region_id):  # 查询未绑定 EIP 的函数。 
    last_exc = None  # 初始化最后一次异常用于兜底抛出。 
    for endpoint in _iter_eip_endpoints(region_id):  # 依次尝试可用端点。 
        client = _build_eip_client(region_id, endpoint)  # 构建 EIP 客户端用于查询。 
        request = ListPublicipsRequest()  # 创建查询 EIP 列表的请求对象。 
        try:  # 捕获连接异常以便切换端点。 
            response = retry_cloud_call("查询未绑定EIP", lambda: client.list_publicips(request))  # 调用 SDK 查询 EIP 列表。
        except Exception as exc:  # 连接失败时尝试下一个端点。
            if not is_transient_cloud_error(exc):
                raise
            last_exc = exc  # 记录异常用于最终抛出。
            continue  # 继续尝试下一个端点。
        free_ids = []  # 初始化未绑定 EIP ID 列表。 
        for item in response.publicips or []:  # 遍历 EIP 列表筛选未绑定项。 
            if not item.port_id:  # 若未绑定端口则视为可复用。 
                free_ids.append(item.id)  # 记录未绑定的 EIP ID。 
        return free_ids  # 返回可复用的 EIP ID 列表。 
    if last_exc:  # 若所有端点都失败则抛出最后异常。 
        raise last_exc  # 抛出连接异常提示端点不可用。 
    return []  # 端点为空时返回空列表。 
def allocate_eip(region_id, count):  # 批量申请 EIP 的函数实现。 
    if count <= 0:  # 若数量非法则直接返回空列表。 
        return []  # 返回空列表表示未申请。 
    eip_ids = []  # 初始化 EIP ID 列表。 
    if EIP_AUTO_REUSE:  # 若开启复用则优先使用未绑定 EIP。 
        free_ids = _list_unbound_eips(region_id)  # 查询未绑定 EIP 列表。 
        take = free_ids[:count]  # 取前 count 个未绑定 EIP。 
        eip_ids.extend(take)  # 将可复用 EIP 加入列表。 
    remaining = count - len(eip_ids)  # 计算仍需创建的数量。 
    for _ in range(remaining):  # 按剩余数量循环创建 EIP。 
        created = False  # 标记本次是否创建成功。 
        last_exc = None  # 初始化最后一次异常用于兜底抛出。 
        for endpoint in _iter_eip_endpoints(region_id):  # 依次尝试可用端点。 
            client = _build_eip_client(region_id, endpoint)  # 构建 EIP 客户端以调用创建接口。 
            request = CreatePublicipRequest()  # 创建 EIP 请求对象。 
            request.body = _build_publicip_body(region_id)  # 填充请求体。 
            try:  # 捕获连接异常以便切换端点。 
                response = retry_cloud_call("创建EIP", lambda: client.create_publicip(request))  # 调用 SDK 创建 EIP。
            except Exception as exc:  # 连接失败时尝试下一个端点。
                if not is_transient_cloud_error(exc):
                    raise
                last_exc = exc  # 记录异常用于最终抛出。
                continue  # 继续尝试下一个端点。
            eip_ids.append(response.publicip.id)  # 记录创建的 EIP ID。 
            created = True  # 标记创建成功。 
            break  # 成功后退出端点循环。 
        if not created and last_exc:  # 若未创建成功则抛出最后异常。 
            raise last_exc  # 抛出连接异常提示端点不可用。 
    return eip_ids  # 返回 EIP ID 列表。 
def bind_eip(region_id, eip_id, port_id):  # 将 EIP 绑定到指定端口的函数实现。 
    if not eip_id or not port_id:  # 校验必要参数是否存在。 
        raise ValueError("eip_id 或 port_id 不能为空")  # 抛出异常提示参数缺失。 
    last_exc = None  # 初始化最后一次异常用于兜底抛出。 
    for endpoint in _iter_eip_endpoints(region_id):  # 依次尝试可用端点。 
        client = _build_eip_client(region_id, endpoint)  # 构建 EIP 客户端以调用绑定接口。 
        request = UpdatePublicipRequest(publicip_id=eip_id)  # 创建 EIP 更新请求对象。 
        request.body = {"publicip": {"port_id": port_id}}  # 设置绑定端口字段。 
        try:  # 捕获连接异常以便切换端点。 
            retry_cloud_call("绑定EIP", lambda: client.update_publicip(request))  # 调用 SDK 绑定 EIP。
        except Exception as exc:  # 连接失败时尝试下一个端点。
            if not is_transient_cloud_error(exc):
                raise
            last_exc = exc  # 记录异常用于最终抛出。
            continue  # 继续尝试下一个端点。
        return  # 绑定成功则直接返回。 
    if last_exc:  # 若所有端点都失败则抛出最后异常。 
        raise last_exc  # 抛出连接异常提示端点不可用。 
def get_server_ports(region_id, server_id, spec, timeout=120, interval=3):  # 获取主/副网卡端口 ID 的函数。 
    client = _build_ecs_client(region_id)  # 构建 ECS 客户端用于查询服务器详情。 
    start_time = time.time()  # 记录查询开始时间。 
    main_port_id = None  # 初始化主网卡端口 ID。 
    secondary_port_id = None  # 初始化副网卡端口 ID。 
    secondary_ip = spec.get("secondary_nic_ip")  # 获取副网卡 IP 用于匹配端口。 
    while True:  # 循环查询直到超时或拿到端口信息。 
        try:  # 捕获查询实例详情时的临时错误。 
            response = retry_cloud_call("查询ECS端口", lambda: client.show_server(ShowServerRequest(server_id=server_id)))  # 查询云服务器详情。
        except ClientRequestException as exc:  # 捕获请求异常用于重试处理。 
            if "Ecs.0114" in str(exc) or "404" in str(exc):  # 实例未就绪时继续等待。 
                if time.time() - start_time > timeout:  # 超过超时时间仍未获取到实例。 
                    raise TimeoutError("等待云服务器就绪超时")  # 抛出超时异常提示。 
                time.sleep(interval)  # 等待指定间隔后继续轮询。 
                continue  # 跳过本次循环进入下一轮查询。 
            raise  # 非预期错误则直接抛出。 
        interfaces = response.server.network_interfaces or []  # 获取网卡列表并处理空值。 
        for interface in interfaces:  # 遍历网卡列表寻找主/副网卡。 
            if interface.primary:  # 若标记为主网卡。 
                main_port_id = interface.port_id  # 记录主网卡端口 ID。 
            if secondary_ip and secondary_ip in (interface.ip_addresses or []):  # 若匹配到副网卡 IP。 
                secondary_port_id = interface.port_id  # 记录副网卡端口 ID。 
        need_secondary = bool(secondary_ip or int(spec.get("extra_nic_count", 0) or 0) > 0)  # 10IP/20规格需要等到 192.168.0.11 标准弹性网卡端口可见。
        if main_port_id and (not need_secondary or secondary_port_id):  # 若已找到所需标准网卡端口则退出循环。
            break  # 退出轮询循环。
        if time.time() - start_time > timeout:  # 若超时仍未获取到主网卡。 
            raise TimeoutError("获取主网卡端口 ID 超时")  # 抛出超时异常提示。 
        time.sleep(interval)  # 等待指定间隔后继续轮询。 
    return main_port_id, secondary_port_id  # 返回主/副网卡端口 ID。 
def get_public_ips_by_port(region_id, port_id):  # 按端口查询绑定的公网 IP 列表。 
    if not port_id:  # 若端口 ID 为空则直接返回空列表。 
        return []  # 返回空列表表示未查询到 EIP。 
    last_exc = None  # 初始化最后一次异常用于兜底抛出。 
    for endpoint in _iter_eip_endpoints(region_id):  # 依次尝试可用端点。 
        client = _build_eip_client(region_id, endpoint)  # 构建 EIP 客户端用于查询。 
        request = ListPublicipsRequest(port_id=[port_id])  # 按端口 ID 过滤查询 EIP 列表。 
        try:  # 捕获连接异常以便切换端点。 
            response = retry_cloud_call("按端口查询EIP", lambda: client.list_publicips(request))  # 调用 SDK 查询 EIP 列表。
        except Exception as exc:  # 连接失败时尝试下一个端点。
            if not is_transient_cloud_error(exc):
                raise
            last_exc = exc  # 记录异常用于最终抛出。
            continue  # 继续尝试下一个端点。
        return [item.public_ip_address for item in response.publicips or [] if item.public_ip_address]  # 返回公网 IP 列表。 
    if last_exc:  # 若所有端点都失败则抛出最后异常。 
        raise last_exc  # 抛出连接异常提示端点不可用。 
    return []  # 端点为空时返回空列表。 
