# -*- coding: utf-8 -*-
from contextlib import contextmanager
from datetime import datetime
import json
import logging
import os
from pathlib import Path
import time
import uuid
from typing import Any, Dict, Iterator, List, Optional, Sequence, Set, Tuple

import volcenginesdkcore
import volcenginesdkvpc
from volcenginesdkcore.rest import ApiException

try:
    from volcengine import config
except ModuleNotFoundError:
    from . import config

from common.log_utils import credential_scope_label, safe_log_name


VOLC_EIP_DAILY_PURCHASE_LIMIT = 40
_EIP_DAILY_ACTIVE_STATUSES = {"pending", "success"}


class EipDailyPurchaseLimitError(RuntimeError):
    def __init__(self, region: str, count: int) -> None:
        self.region = str(region or "").strip()
        self.count = max(0, int(count or 0))
        self.limit = VOLC_EIP_DAILY_PURCHASE_LIMIT
        super().__init__(
            f"当前账号在 {self.region or '该地区'} 今日公网IP已购买 "
            f"{self.count}/{self.limit} 次，已停止继续购买；请明日再试。"
        )


def _eip_daily_purchase_root() -> Path:
    # 每日限额属于云账号而不是某个安装包，固定保存到用户文档目录供总包和分包共用。
    return Path.home() / "Documents" / "多云管理" / "Volcengine" / "eip_daily_purchase_counts"


def _eip_daily_purchase_account_scope(ak: str, sk: str) -> str:
    return credential_scope_label(ak, sk)


def _eip_daily_purchase_path(ak: str, sk: str) -> Path:
    filename = safe_log_name(_eip_daily_purchase_account_scope(ak, sk)) + ".json"
    return _eip_daily_purchase_root() / filename


@contextmanager
def _locked_eip_daily_purchase_file(path: Path) -> Iterator[None]:
    path.parent.mkdir(parents=True, exist_ok=True)
    lock_path = path.with_suffix(path.suffix + ".lock")
    deadline = time.monotonic() + 8.0
    lock_fd: Optional[int] = None
    while lock_fd is None:
        try:
            lock_fd = os.open(str(lock_path), os.O_CREAT | os.O_EXCL | os.O_WRONLY)
        except FileExistsError:
            try:
                if time.time() - lock_path.stat().st_mtime > 30.0:
                    lock_path.unlink()
                    continue
            except FileNotFoundError:
                continue
            if time.monotonic() >= deadline:
                raise RuntimeError("公网IP每日购买记录正被占用，请稍后重试。")
            time.sleep(0.05)
    try:
        os.write(lock_fd, str(os.getpid()).encode("ascii", errors="ignore"))
        yield
    finally:
        try:
            os.close(lock_fd)
        finally:
            try:
                lock_path.unlink()
            except FileNotFoundError:
                pass


def _new_eip_daily_purchase_data(account_scope: str) -> Dict[str, Any]:
    return {"version": 1, "account": account_scope, "days": {}}


def _read_eip_daily_purchase_data(path: Path, account_scope: str) -> Dict[str, Any]:
    if not path.exists():
        return _new_eip_daily_purchase_data(account_scope)
    try:
        data = json.loads(path.read_text(encoding="utf-8"))
    except Exception as exc:
        raise RuntimeError(f"公网IP每日购买记录读取失败，为避免超过限额已停止购买：{exc}") from exc
    if not isinstance(data, dict) or not isinstance(data.get("days"), dict):
        raise RuntimeError("公网IP每日购买记录格式错误，为避免超过限额已停止购买。")
    data.setdefault("version", 1)
    data.setdefault("account", account_scope)
    return data


def _write_eip_daily_purchase_data(path: Path, data: Dict[str, Any]) -> None:
    temp_path = path.with_name(f".{path.name}.{os.getpid()}.{uuid.uuid4().hex}.tmp")
    try:
        temp_path.write_text(json.dumps(data, ensure_ascii=False, indent=2), encoding="utf-8")
        os.replace(str(temp_path), str(path))
    finally:
        try:
            temp_path.unlink()
        except FileNotFoundError:
            pass


def _eip_daily_purchase_bucket(data: Dict[str, Any], date_text: str, region: str) -> Dict[str, Any]:
    days = data.setdefault("days", {})
    day = days.setdefault(date_text, {})
    bucket = day.setdefault(region, {})
    bucket.setdefault("baseline", 0)
    bucket.setdefault("events", [])
    return bucket


def _eip_daily_purchase_count(bucket: Dict[str, Any]) -> int:
    try:
        baseline = max(0, int(bucket.get("baseline") or 0))
    except (TypeError, ValueError):
        baseline = 0
    events = bucket.get("events") if isinstance(bucket.get("events"), list) else []
    return baseline + sum(
        1
        for event in events
        if isinstance(event, dict) and str(event.get("status") or "").strip().lower() in _EIP_DAILY_ACTIVE_STATUSES
    )


def _eip_daily_purchase_status(path: Path, date_text: str, region: str, bucket: Dict[str, Any]) -> Dict[str, Any]:
    count = _eip_daily_purchase_count(bucket)
    return {
        "date": date_text,
        "region": region,
        "count": count,
        "limit": VOLC_EIP_DAILY_PURCHASE_LIMIT,
        "remaining": max(0, VOLC_EIP_DAILY_PURCHASE_LIMIT - count),
        "path": str(path),
    }


def get_eip_daily_purchase_status(ak: str, sk: str, region: str) -> Dict[str, Any]:
    ak_value, sk_value = _resolve_credentials(ak, sk)
    region_value = _resolve_region(region)
    if not ak_value or not sk_value or not region_value:
        raise ValueError("缺少AK/SK或地区，无法读取公网IP每日购买记录。")
    account_scope = _eip_daily_purchase_account_scope(ak_value, sk_value)
    path = _eip_daily_purchase_path(ak_value, sk_value)
    date_text = datetime.now().astimezone().date().isoformat()
    with _locked_eip_daily_purchase_file(path):
        data = _read_eip_daily_purchase_data(path, account_scope)
        bucket = _eip_daily_purchase_bucket(data, date_text, region_value)
        return _eip_daily_purchase_status(path, date_text, region_value, bucket)


def reserve_eip_daily_purchase(ak: str, sk: str, region: str, *, name: str = "") -> Dict[str, Any]:
    ak_value, sk_value = _resolve_credentials(ak, sk)
    region_value = _resolve_region(region)
    if not ak_value or not sk_value or not region_value:
        raise ValueError("缺少AK/SK或地区，无法记录公网IP每日购买次数。")
    account_scope = _eip_daily_purchase_account_scope(ak_value, sk_value)
    path = _eip_daily_purchase_path(ak_value, sk_value)
    now = datetime.now().astimezone()
    date_text = now.date().isoformat()
    with _locked_eip_daily_purchase_file(path):
        data = _read_eip_daily_purchase_data(path, account_scope)
        bucket = _eip_daily_purchase_bucket(data, date_text, region_value)
        count = _eip_daily_purchase_count(bucket)
        if count >= VOLC_EIP_DAILY_PURCHASE_LIMIT:
            raise EipDailyPurchaseLimitError(region_value, count)
        token = uuid.uuid4().hex
        bucket["events"].append(
            {
                "token": token,
                "time": now.isoformat(timespec="seconds"),
                "status": "pending",
                "name": str(name or "").strip(),
                "ip": "",
            }
        )
        _write_eip_daily_purchase_data(path, data)
        status = _eip_daily_purchase_status(path, date_text, region_value, bucket)
        status["token"] = token
        return status


def finish_eip_daily_purchase(
    ak: str,
    sk: str,
    region: str,
    token: str,
    *,
    success: bool,
    ip: str = "",
    error: str = "",
) -> Dict[str, Any]:
    ak_value, sk_value = _resolve_credentials(ak, sk)
    region_value = _resolve_region(region)
    account_scope = _eip_daily_purchase_account_scope(ak_value, sk_value)
    path = _eip_daily_purchase_path(ak_value, sk_value)
    now = datetime.now().astimezone()
    date_text = now.date().isoformat()
    with _locked_eip_daily_purchase_file(path):
        data = _read_eip_daily_purchase_data(path, account_scope)
        bucket = _eip_daily_purchase_bucket(data, date_text, region_value)
        events = bucket.get("events") if isinstance(bucket.get("events"), list) else []
        for event in events:
            if isinstance(event, dict) and str(event.get("token") or "") == str(token or ""):
                event["status"] = "success" if success else "failed"
                event["finished_at"] = now.isoformat(timespec="seconds")
                event["ip"] = str(ip or "").strip() if success else ""
                event["error"] = "" if success else str(error or "").strip()[:500]
                break
        _write_eip_daily_purchase_data(path, data)
        return _eip_daily_purchase_status(path, date_text, region_value, bucket)


def _normalize(value: Optional[str]) -> str:
    return (value or "").strip()


def _resolve_credentials(ak: Optional[str], sk: Optional[str]) -> Tuple[str, str]:
    ak_value = _normalize(ak) or _normalize(os.getenv("VOLC_AK")) or _normalize(getattr(config, "DEFAULT_ACCESS_KEY", ""))
    sk_value = _normalize(sk) or _normalize(os.getenv("VOLC_SK")) or _normalize(getattr(config, "DEFAULT_SECRET_KEY", ""))
    return ak_value, sk_value


def _resolve_region(region: Optional[str]) -> str:
    return _normalize(region) or _normalize(getattr(config, "DEFAULT_REGION", ""))


def _build_configuration(ak: str, sk: str, region: str) -> volcenginesdkcore.Configuration:
    configuration = volcenginesdkcore.Configuration()
    configuration.ak = ak
    configuration.sk = sk
    configuration.region = region
    return configuration


def _silence_sdk_warnings() -> None:
    for name in ("volcenginesdkcore", "volcenginesdkvpc", "volcengine"):
        logging.getLogger(name).setLevel(logging.ERROR)


def _to_dict(obj) -> Dict[str, Any]:
    if obj is None:
        return {}
    if isinstance(obj, dict):
        return obj
    to_dict = getattr(obj, "to_dict", None)
    return to_dict() if callable(to_dict) else {}


def _request(cls, **kwargs):
    try:
        return cls(**kwargs)
    except TypeError:
        request = cls()
        for key, value in kwargs.items():
            try:
                setattr(request, key, value)
            except Exception:
                pass
        return request


def _extract_list(data: Dict[str, Any], *keys: str) -> List[Dict[str, Any]]:
    preferred = {key.lower() for key in keys}

    def walk(value: Any) -> List[Dict[str, Any]]:
        if isinstance(value, dict):
            for key, item in value.items():
                if key.lower() in preferred and isinstance(item, list):
                    return [entry for entry in item if isinstance(entry, dict)]
            for item in value.values():
                found = walk(item)
                if found:
                    return found
        elif isinstance(value, list):
            dict_items = [entry for entry in value if isinstance(entry, dict)]
            if dict_items and any(
                entry.get("AllocationId") or entry.get("allocation_id") or entry.get("EipAddress") or entry.get("eip_address")
                for entry in dict_items
            ):
                return dict_items
            for item in value:
                found = walk(item)
                if found:
                    return found
        return []

    return walk(data)


def _result(data: Dict[str, Any]) -> Dict[str, Any]:
    result = data.get("Result") or data.get("result")
    return result if isinstance(result, dict) else data


def _field(data: Dict[str, Any], *keys: str) -> str:
    for key in keys:
        value = data.get(key)
        if value not in (None, ""):
            return str(value).strip()
    raw = data.get("_raw")
    if isinstance(raw, dict):
        for key in keys:
            value = raw.get(key)
            if value not in (None, ""):
                return str(value).strip()
    return ""


def describe_eip_addresses(
    region: Optional[str],
    ak: Optional[str] = None,
    sk: Optional[str] = None,
) -> Tuple[bool, str, List[Dict[str, Any]]]:
    ak_value, sk_value = _resolve_credentials(ak, sk)
    region_value = _resolve_region(region)
    if not ak_value or not sk_value:
        return False, "AK/SK 为空，无法查询公网 IP", []
    if not region_value:
        return False, "地域为空，无法查询公网 IP", []
    volcenginesdkcore.Configuration.set_default(_build_configuration(ak_value, sk_value, region_value))
    _silence_sdk_warnings()
    api_instance = volcenginesdkvpc.VPCApi()

    all_items: List[Dict[str, Any]] = []
    seen: set = set()
    for page in range(1, 21):
        request = _request(volcenginesdkvpc.DescribeEipAddressesRequest, page_number=page, page_size=100)
        try:
            response = api_instance.describe_eip_addresses(request)
        except ApiException as exc:
            return False, f"查询公网 IP 失败: {exc}", []
        data_dict = _to_dict(response)
        items = _extract_list(data_dict, "EipAddresses", "eip_addresses", "EipAddressSet", "eip_address_set")
        for item in items:
            allocation_id = _field(item, "AllocationId", "allocation_id", "EipId", "eip_id", "id")
            key = allocation_id or repr(sorted(item.items()))
            if key in seen:
                continue
            seen.add(key)
            all_items.append(item)
        total = _result(data_dict).get("TotalCount") or _result(data_dict).get("total_count") or _result(data_dict).get("Total")
        try:
            total_int = int(total)
        except (TypeError, ValueError):
            total_int = 0
        if not items or (total_int and len(all_items) >= total_int):
            break
    return True, "查询公网 IP 成功", all_items


def allocate_eip_address(
    region: Optional[str],
    bandwidth: Optional[int],
    billing_type: Optional[int],
    isp: Optional[str],
    project_name: Optional[str] = None,
    name: Optional[str] = None,
    description: Optional[str] = None,
    client_token: Optional[str] = None,
    ak: Optional[str] = None,
    sk: Optional[str] = None,
) -> Tuple[bool, str, Dict[str, Any]]:
    ak_value, sk_value = _resolve_credentials(ak, sk)
    region_value = _resolve_region(region)
    if not ak_value or not sk_value:
        return False, "AK/SK 为空，无法申请公网 IP", {}
    if not region_value:
        return False, "地域为空，无法申请公网 IP", {}
    volcenginesdkcore.Configuration.set_default(_build_configuration(ak_value, sk_value, region_value))
    _silence_sdk_warnings()
    api_instance = volcenginesdkvpc.VPCApi()
    params: Dict[str, Any] = {"client_token": client_token or str(uuid.uuid4())}
    if bandwidth is not None:
        params["bandwidth"] = int(bandwidth)
    if billing_type is not None:
        params["billing_type"] = int(billing_type)
    if _normalize(isp):
        params["isp"] = _normalize(isp)
    if _normalize(project_name):
        params["project_name"] = _normalize(project_name)
    if _normalize(name):
        params["name"] = _normalize(name)
    if _normalize(description):
        params["description"] = _normalize(description)
    try:
        daily_reservation = reserve_eip_daily_purchase(
            ak_value,
            sk_value,
            region_value,
            name=_normalize(name),
        )
    except EipDailyPurchaseLimitError as exc:
        return False, str(exc), {}
    try:
        response = api_instance.allocate_eip_address(volcenginesdkvpc.AllocateEipAddressRequest(**params))
    except ApiException as exc:
        finish_eip_daily_purchase(
            ak_value,
            sk_value,
            region_value,
            str(daily_reservation.get("token") or ""),
            success=False,
            error=str(exc),
        )
        return False, f"申请公网 IP 失败: {exc}", {}
    except Exception as exc:
        finish_eip_daily_purchase(
            ak_value,
            sk_value,
            region_value,
            str(daily_reservation.get("token") or ""),
            success=False,
            error=str(exc),
        )
        raise
    response_dict = _to_dict(response)
    result = _result(response_dict)
    eip_address = _field(result, "EipAddress", "eip_address", "IpAddress", "ip_address")
    try:
        daily_status = finish_eip_daily_purchase(
            ak_value,
            sk_value,
            region_value,
            str(daily_reservation.get("token") or ""),
            success=True,
            ip=eip_address,
        )
    except Exception:
        # 预占记录仍会计入当日次数，购买成功后不能因记录状态更新失败而丢失EIP结果。
        daily_status = daily_reservation
    response_dict["_daily_purchase_count"] = daily_status["count"]
    response_dict["_daily_purchase_remaining"] = daily_status["remaining"]
    return True, "申请公网 IP 成功", response_dict


def release_eip_address(
    region: Optional[str],
    allocation_id: str,
    *,
    ak: Optional[str] = None,
    sk: Optional[str] = None,
) -> Tuple[bool, str, Dict[str, Any]]:
    ak_value, sk_value = _resolve_credentials(ak, sk)
    region_value = _resolve_region(region)
    allocation_value = _normalize(allocation_id)
    if not ak_value or not sk_value:
        return False, "AK/SK 为空，无法释放公网 IP", {}
    if not region_value:
        return False, "地域为空，无法释放公网 IP", {}
    if not allocation_value:
        return False, "AllocationId 为空，无法释放公网 IP", {}
    volcenginesdkcore.Configuration.set_default(_build_configuration(ak_value, sk_value, region_value))
    _silence_sdk_warnings()
    api_instance = volcenginesdkvpc.VPCApi()
    params = {"allocation_id": allocation_value, "client_token": str(uuid.uuid4())}
    try:
        response = api_instance.release_eip_address(volcenginesdkvpc.ReleaseEipAddressRequest(**params))
    except ApiException as exc:
        lowered = str(exc).lower()
        if "notfound" in lowered or "not found" in lowered or "invalidallocationid.notfound" in lowered:
            return True, "公网 IP 已不存在，按释放完成处理", {}
        return False, f"释放公网 IP 失败: {exc}", {}
    return True, "释放公网 IP 成功", _to_dict(response)


def allocate_eip_batch(
    region: Optional[str],
    count: int,
    bandwidth: Optional[int],
    billing_type: Optional[int],
    isp: Optional[str],
    project_name: Optional[str] = None,
    name_prefix: Optional[str] = None,
    ak: Optional[str] = None,
    sk: Optional[str] = None,
    excluded_ips: Optional[Sequence[str]] = None,
    duplicate_hit_limit: int = 3,
) -> Tuple[bool, str, List[Dict[str, Any]]]:
    if count <= 0:
        return True, "无需申请公网 IP", []
    ak_value, sk_value = _resolve_credentials(ak, sk)
    region_value = _resolve_region(region)
    try:
        daily_status = get_eip_daily_purchase_status(ak_value, sk_value, region_value)
    except Exception as exc:
        return False, f"读取公网IP每日购买记录失败，为避免超过限额已停止购买: {exc}", []
    daily_remaining = int(daily_status.get("remaining") or 0)
    if daily_remaining < count:
        return (
            False,
            f"当前账号在 {region_value} 今日公网IP已购买 {daily_status['count']}/{VOLC_EIP_DAILY_PURCHASE_LIMIT} 次，"
            f"仅剩 {daily_remaining} 次，本批需要 {count} 次；为避免只购买部分资源已停止，请明日再试。",
            [],
        )
    excluded: Set[str] = {_normalize(ip) for ip in (excluded_ips or []) if _normalize(ip)}
    filter_enabled = bool(excluded)
    duplicate_hit_limit = max(1, int(duplicate_hit_limit or 3))
    duplicate_hit_streak = 0
    attempt = 0
    eips: List[Dict[str, Any]] = []

    def cleanup_accepted_eips() -> None:
        for item in eips:
            allocation_id = _normalize(item.get("allocation_id"))
            if allocation_id:
                release_eip_address(region_value, allocation_id, ak=ak_value, sk=sk_value)

    while len(eips) < count:
        attempt += 1
        accepted_index = len(eips) + 1
        name_value = f"{name_prefix}{accepted_index}-try{attempt}" if name_prefix and filter_enabled else (f"{name_prefix}{accepted_index}" if name_prefix else None)
        ok, message, data_dict = allocate_eip_address(
            region=region,
            bandwidth=bandwidth,
            billing_type=billing_type,
            isp=isp,
            project_name=project_name,
            name=name_value,
            ak=ak,
            sk=sk,
        )
        if not ok:
            if filter_enabled:
                cleanup_accepted_eips()
            return False, message, []
        result = _result(data_dict)
        allocation_id = _field(result, "AllocationId", "allocation_id", "EipId", "eip_id", "id")
        eip_address = _field(result, "EipAddress", "eip_address", "IpAddress", "ip_address")
        if not allocation_id:
            if filter_enabled:
                cleanup_accepted_eips()
            return False, f"申请公网 IP 失败: 未返回 AllocationId，响应={data_dict}", []
        if filter_enabled and eip_address and eip_address in excluded:
            duplicate_hit_streak += 1
            release_ok, release_message, _ = release_eip_address(
                region_value,
                allocation_id,
                ak=ak_value,
                sk=sk_value,
            )
            print(
                f"[{region_value}] 随机新公网IP命中旧IP库 {eip_address}，"
                f"已释放后重试 {duplicate_hit_streak}/{duplicate_hit_limit}。",
                flush=True,
            )
            if not release_ok:
                cleanup_accepted_eips()
                return False, f"新公网IP命中旧IP库，但{release_message}", []
            if duplicate_hit_streak >= duplicate_hit_limit:
                cleanup_accepted_eips()
                return (
                    False,
                    f"随机新公网IP连续 {duplicate_hit_streak} 次命中旧IP库，已停止本地区购买新IP；"
                    "请超过2小时再试，可大大降低买到旧IP概率。",
                    [],
                )
            time.sleep(1.0)
            continue
        duplicate_hit_streak = 0
        if filter_enabled and eip_address:
            excluded.add(eip_address)
        eips.append(
            {
                "allocation_id": allocation_id,
                "eip_address": eip_address,
                "name": name_value or "",
                "_raw": data_dict,
            }
        )
    return True, "申请公网 IP 成功", eips


def associate_eip_address(
    region: Optional[str],
    allocation_id: str,
    instance_id: str,
    private_ip_address: Optional[str],
    instance_type: str = "EcsInstance",
    client_token: Optional[str] = None,
    ak: Optional[str] = None,
    sk: Optional[str] = None,
    retries: int = 6,
    interval_seconds: int = 5,
) -> Tuple[bool, str, Dict[str, Any]]:
    ak_value, sk_value = _resolve_credentials(ak, sk)
    region_value = _resolve_region(region)
    if not ak_value or not sk_value:
        return False, "AK/SK 为空，无法绑定公网 IP", {}
    if not region_value:
        return False, "地域为空，无法绑定公网 IP", {}
    if not allocation_id or not instance_id:
        return False, "AllocationId 或 InstanceId 为空，无法绑定公网 IP", {}
    volcenginesdkcore.Configuration.set_default(_build_configuration(ak_value, sk_value, region_value))
    api_instance = volcenginesdkvpc.VPCApi()
    params: Dict[str, Any] = {
        "allocation_id": allocation_id,
        "instance_id": instance_id,
        "instance_type": instance_type,
        "client_token": client_token or str(uuid.uuid4()),
    }
    if _normalize(private_ip_address):
        params["private_ip_address"] = _normalize(private_ip_address)
    for attempt in range(1, retries + 1):
        try:
            response = api_instance.associate_eip_address(volcenginesdkvpc.AssociateEipAddressRequest(**params))
            return True, "公网 IP 绑定成功", _to_dict(response)
        except ApiException as exc:
            error_text = str(exc)
            lowered = error_text.lower()
            if "already associated" in lowered or "invalidassociation" in lowered:
                return True, "公网 IP 已绑定，跳过重复绑定", {}
            if (
                "InvalidInstance.InvalidStatus" in error_text
                or "InvalidEip.InvalidStatus" in error_text
                or "InvalidOperation.Conflict" in error_text
            ) and attempt < retries:
                time.sleep(interval_seconds)
                continue
            return False, f"公网 IP 绑定失败: {exc}", {}
    return False, "公网 IP 绑定失败: 超过最大重试次数", {}
