from __future__ import annotations

import json
import os
import re
import threading
import time
import urllib.error
import urllib.request
from dataclasses import dataclass
from pathlib import Path
from typing import Callable, List, Sequence, Tuple

from common.models import RegionPlan
from common.runtime_paths import PROJECT_ROOT, cloud_data_dir, portable_root_dir, resource_path


ROS_CHR_FILENAME = "chr-6.48.6.img.zip"
ROS_CHR_DOWNLOAD_URL = f"https://www.wawaapi.com/static/scripts/Huawei/{ROS_CHR_FILENAME}"
ROS_LICENSE_URL = "https://www.wawaapi.com/api/ros/license"
ROS_LICENSE_TIMEOUT_SECONDS = 6
ROS_CHR_MIN_BYTES = 10 * 1024 * 1024
ROS_DEFAULT_L2TP_USERNAME = "user"
ROS_DEFAULT_PPP_PASSWORD = "wawacloud"
ROS_DEFAULT_LICENSE_ACCOUNT = "mowdeo4c1o@oqqaj.com"
ROS_DEFAULT_LICENSE_PASSWORD = "LIWW2TIaJ7"
ROS_L2TP_PSK = "wawacloud"
ROS_ADMIN_USER = "admin"
ROS_ADMIN_PASSWORD = "admin"
ROS_WINBOX_PORT = 6555
ROS_SSH_PORT = 22
ROS_READY_TIMEOUT_SECONDS = 240
ROS_READY_INTERVAL_SECONDS = 5
ROS_FIVE_REGION_TEMPLATE = "hw5L2.rsc"
ROS_TEN_REGION_TEMPLATE = "hw10L2.rsc"
ROS_TEN_REGION_KEYS = {"east2", "qingdao"}
ROS_SETTINGS_FILE = cloud_data_dir("HUAWEI") / "ros_settings.json"

# app.py 会在热更新后 reload 本模块；保留同一个条件变量和下载状态，
# 避免 reload 期间正在执行的预下载失去单飞保护。
if "_CHR_DOWNLOAD_CONDITION" not in globals():
    _CHR_DOWNLOAD_CONDITION = threading.Condition()
if "_CHR_DOWNLOAD_IN_PROGRESS" not in globals():
    _CHR_DOWNLOAD_IN_PROGRESS = False
if "_CHR_DOWNLOAD_ERROR" not in globals():
    _CHR_DOWNLOAD_ERROR: BaseException | None = None


@dataclass(frozen=True)
class RosBuildConfig:
    ppp_password: str
    license_account: str
    license_password: str
    l2tp_username: str = ROS_DEFAULT_L2TP_USERNAME
    l2tp_psk: str = ""


def default_ros_config() -> RosBuildConfig:
    return RosBuildConfig(
        ppp_password=ROS_DEFAULT_PPP_PASSWORD,
        license_account=ROS_DEFAULT_LICENSE_ACCOUNT,
        license_password=ROS_DEFAULT_LICENSE_PASSWORD,
        l2tp_username=ROS_DEFAULT_L2TP_USERNAME,
        l2tp_psk=ROS_DEFAULT_PPP_PASSWORD,
    )


def load_ros_config() -> RosBuildConfig:
    defaults = default_ros_config()
    try:
        data = json.loads(ROS_SETTINGS_FILE.read_text(encoding="utf-8"))
    except Exception:
        return defaults
    return RosBuildConfig(
        ppp_password=str(data.get("ppp_password") or defaults.ppp_password),
        license_account=str(data.get("license_account") or defaults.license_account),
        license_password=str(data.get("license_password") or defaults.license_password),
        l2tp_username=str(data.get("l2tp_username") or defaults.l2tp_username),
        l2tp_psk=str(data.get("l2tp_psk") or data.get("ppp_password") or defaults.l2tp_psk),
    )


def fetch_server_ros_license(timeout: int = ROS_LICENSE_TIMEOUT_SECONDS) -> Tuple[str, str]:
    request = urllib.request.Request(
        ROS_LICENSE_URL,
        headers={"User-Agent": "WaWaCloud-ROS-License/1.0", "Cache-Control": "no-cache"},
    )
    with urllib.request.urlopen(request, timeout=timeout) as response:
        payload = response.read(64 * 1024)
    data = json.loads(payload.decode("utf-8-sig"))
    if not isinstance(data, dict) or not data.get("ok", True):
        raise RuntimeError(str(data.get("message") if isinstance(data, dict) else "") or "服务器ROS许可证不可用")
    account = str(data.get("license_account") or data.get("username") or "").strip()
    password = str(data.get("license_password") or data.get("password") or "").strip()
    if not account or not password:
        raise RuntimeError("服务器ROS许可证配置不完整")
    return account, password


def ros_l2tp_psk(config: RosBuildConfig) -> str:
    psk = str(getattr(config, "l2tp_psk", "") or "").strip()
    if psk:
        return psk
    ppp_password = str(getattr(config, "ppp_password", "") or "").strip()
    return ppp_password or ROS_L2TP_PSK


def ros_l2tp_username(config: RosBuildConfig) -> str:
    username = str(getattr(config, "l2tp_username", "") or "").strip()
    return username or ROS_DEFAULT_L2TP_USERNAME


def _desktop_path(filename: str) -> Path:
    return Path.home() / "Desktop" / filename


def find_ros_asset(filename: str) -> Path:
    candidates = [
        Path(resource_path(filename)),
        # 中文注释：便携包为了减小体积，CHR 大文件只保留在外部 source 目录，exe 运行时也要优先从这里读取。
        portable_root_dir() / "source" / "HUAWEI" / "assets" / filename,
        PROJECT_ROOT / "HUAWEI" / "assets" / filename,
        _desktop_path(filename),
        PROJECT_ROOT / "HUAWEI" / "scripts" / filename,
        PROJECT_ROOT / "assets" / filename,
        PROJECT_ROOT / filename,
    ]
    for path in candidates:
        if path.exists():
            return path
    return _desktop_path(filename)


def chr_image_path() -> Path:
    return find_ros_asset(ROS_CHR_FILENAME)


def _chr_cache_path() -> Path:
    return cloud_data_dir("HUAWEI") / "cache" / ROS_CHR_FILENAME


def _valid_file(path: Path) -> bool:
    try:
        return path.exists() and path.stat().st_size >= ROS_CHR_MIN_BYTES
    except OSError:
        return False


def _available_chr_image() -> Path | None:
    cache_path = _chr_cache_path()
    if _valid_file(cache_path):
        return cache_path
    local_path = chr_image_path()
    if _valid_file(local_path):
        return local_path
    return None


def chr_image_available() -> bool:
    return _available_chr_image() is not None


def _download_chr_image(cache_path: Path, emit: Callable[[str], None] | None = None) -> Path:
    cache_path.parent.mkdir(parents=True, exist_ok=True)
    temp_path = cache_path.with_name(
        f"{cache_path.name}.{os.getpid()}.{threading.get_ident()}.tmp"
    )
    last_error = ""
    for attempt in range(1, 4):
        try:
            if emit:
                emit(f"步骤: 首次使用 ROS，正在下载 CHR 镜像({attempt}/3)，请稍等。")
            request = urllib.request.Request(
                ROS_CHR_DOWNLOAD_URL,
                headers={"User-Agent": "WaWaCloud-ROS-CHR-Downloader/1.0"},
            )
            with urllib.request.urlopen(request, timeout=30) as response, temp_path.open("wb") as target:
                while True:
                    chunk = response.read(1024 * 1024)
                    if not chunk:
                        break
                    target.write(chunk)
            if not _valid_file(temp_path):
                raise RuntimeError("下载到的 CHR 镜像不完整")

            # 另一个软件进程可能已经完成同一镜像，直接复用，避免覆盖其正在上传的文件。
            if _valid_file(cache_path):
                temp_path.unlink(missing_ok=True)
                return cache_path
            try:
                temp_path.replace(cache_path)
            except OSError:
                if _valid_file(cache_path):
                    temp_path.unlink(missing_ok=True)
                    return cache_path
                raise
            if emit:
                emit(f"步骤: CHR 镜像下载完成，已缓存到 {cache_path}")
            return cache_path
        except (OSError, urllib.error.URLError, TimeoutError, RuntimeError) as exc:
            last_error = str(exc)
            try:
                temp_path.unlink(missing_ok=True)
            except OSError:
                pass
            if attempt < 3:
                time.sleep(3)

    raise FileNotFoundError(
        "ROS CHR 镜像下载失败，请检查本机网络或稍后重试；"
        f"下载地址：{ROS_CHR_DOWNLOAD_URL}；失败原因：{last_error}"
    )


def ensure_chr_image(emit: Callable[[str], None] | None = None) -> Path:
    """确保 ROS CHR 镜像可用；同一时刻只下载一次，其余搭建等待并复用缓存。"""
    global _CHR_DOWNLOAD_ERROR, _CHR_DOWNLOAD_IN_PROGRESS

    available = _available_chr_image()
    if available is not None:
        return available

    with _CHR_DOWNLOAD_CONDITION:
        available = _available_chr_image()
        if available is not None:
            return available
        if _CHR_DOWNLOAD_IN_PROGRESS:
            if emit:
                emit("步骤: ROS CHR 镜像正在后台下载，等待完成后继续搭建。")
            while _CHR_DOWNLOAD_IN_PROGRESS:
                _CHR_DOWNLOAD_CONDITION.wait()
            available = _available_chr_image()
            if available is not None:
                return available
            error = _CHR_DOWNLOAD_ERROR
            raise FileNotFoundError(str(error) or "ROS CHR 镜像后台下载失败") from error
        _CHR_DOWNLOAD_IN_PROGRESS = True
        _CHR_DOWNLOAD_ERROR = None

    try:
        result = _download_chr_image(_chr_cache_path(), emit)
    except BaseException as exc:
        with _CHR_DOWNLOAD_CONDITION:
            _CHR_DOWNLOAD_ERROR = exc
            _CHR_DOWNLOAD_IN_PROGRESS = False
            _CHR_DOWNLOAD_CONDITION.notify_all()
        raise
    with _CHR_DOWNLOAD_CONDITION:
        _CHR_DOWNLOAD_ERROR = None
        _CHR_DOWNLOAD_IN_PROGRESS = False
        _CHR_DOWNLOAD_CONDITION.notify_all()
    return result


def template_name_for_plan(plan: RegionPlan) -> str:
    return ROS_TEN_REGION_TEMPLATE if plan.key in ROS_TEN_REGION_KEYS else ROS_FIVE_REGION_TEMPLATE


def parse_ip_txt(ip_txt: str) -> List[Tuple[str, str, str]]:
    rows: List[Tuple[str, str, str]] = []
    for raw_line in ip_txt.splitlines():
        line = raw_line.strip()
        if not line or line.startswith("#"):
            continue
        parts = line.split()
        if len(parts) >= 4 and not parts[0].isdigit():
            vlan_id, mac, private_ip = parts[1], parts[2], parts[3]
        elif len(parts) >= 3:
            vlan_id, mac, private_ip = parts[0], parts[1], parts[2]
        else:
            continue
        if vlan_id.isdigit() and re.fullmatch(r"[0-9a-fA-F:]{17}", mac):
            rows.append((vlan_id, mac.lower(), private_ip))
    return rows


def _replace_line(text: str, prefix: str, replacement: str) -> str:
    pattern = re.compile(rf"^{re.escape(prefix)}.*$", flags=re.MULTILINE)
    if pattern.search(text):
        return pattern.sub(replacement, text, count=1)
    return text.rstrip() + "\n" + replacement + "\n"


def _move_ip_service_block_to_end(text: str) -> str:
    pattern = re.compile(r"(?ms)^/ip service\s*\n(?:^set .*\n)+^/\s*\n")
    match = pattern.search(text)
    service_block = match.group(0).strip() if match else ""
    body = (text[: match.start()] + text[match.end() :] if match else text).rstrip()
    remove_admin = bool(re.search(r"(?m)^/user remove admin\s*$", body))
    body = re.sub(r"(?m)^/user remove admin\s*\n?", "", body).rstrip()
    if not service_block and not remove_admin:
        return text
    parts: List[str] = []
    if service_block:
        parts.append("# 自动导入时先保留管理通道，全部配置完成后再关闭服务。")
        parts.append(service_block)
    if remove_admin:
        parts.append("# 自动导入收尾时再删除默认admin，避免提前中断终端导入。")
        parts.append("/user remove admin")
    insert = "\n" + "\n".join(parts) + "\n"
    last_brace = body.rfind("}")
    if last_brace >= 0:
        return body[:last_brace].rstrip() + insert + body[last_brace:]
    return body + insert


def _ensure_ros_l2tp_ipsec(text: str, l2tp_psk: str) -> str:
    line = f"/int l2tp-server server set use-ipsec=yes ipsec-secret={l2tp_psk or ROS_L2TP_PSK}"
    ipsec_pattern = re.compile(r"(?m)^/int(?:erface)?\s+l2tp-server\s+server\s+set\s+.*\buse-ipsec=.*$")
    if ipsec_pattern.search(text):
        return ipsec_pattern.sub(line, text, count=1)
    enabled_pattern = re.compile(r"(?m)^/int(?:erface)?\s+l2tp-server\s+server\s+set\s+.*\benabled=yes.*$")
    if enabled_pattern.search(text):
        return enabled_pattern.sub(lambda match: match.group(0).rstrip() + "\n" + line, text, count=1)
    return text.rstrip() + "\n" + line + "\n"


def _append_ros_l2tp_self_check(text: str, expected_users: int) -> str:
    # 中文注释：自动导入后输出RouterOS内部状态，程序据此判断L2TP配置是否真正落地。
    check_block = "\n".join(
        [
            "# 自动导入自检：供实时日志和程序判定L2TP是否配置成功。",
            f':put "ROS_CHECK_EXPECTED_USERS={expected_users}"',
            ':put ("ROS_CHECK_L2TP_ENABLED=" . [/interface l2tp-server server get enabled])',
            ':put ("ROS_CHECK_L2TP_USE_IPSEC=" . [/interface l2tp-server server get use-ipsec])',
            ':put ("ROS_CHECK_PPP_COUNT=" . [:len [/ppp secret find]])',
            ':put ("ROS_CHECK_SRCNAT_COUNT=" . [:len [/ip firewall nat find where chain=srcnat]])',
        ]
    )
    marker = "ROS_CHECK_EXPECTED_USERS="
    if marker in text:
        return text
    last_brace = text.rfind("}")
    if last_brace >= 0:
        return text[:last_brace].rstrip() + "\n" + check_block + "\n" + text[last_brace:]
    return text.rstrip() + "\n" + check_block + "\n"


def render_ros_rsc(plan: RegionPlan, ip_txt: str, config: RosBuildConfig) -> Tuple[str, str]:
    template_name = template_name_for_plan(plan)
    template_path = find_ros_asset(template_name)
    if not template_path.exists():
        raise FileNotFoundError(f"未找到 ROS RSC 模板: {template_path}")
    rows = parse_ip_txt(ip_txt)
    expected = 8 if template_name == ROS_TEN_REGION_TEMPLATE else 4
    if len(rows) < expected:
        raise ValueError(f"{plan.label} ROS模板需要 {expected} 条 VLAN/MAC，当前 ip.txt 只有 {len(rows)} 条。")
    rows = rows[:expected]
    text = template_path.read_text(encoding="utf-8-sig")
    vid = ";".join(vlan_id for vlan_id, _mac, _ip in rows)
    vmac = ";".join(f'"{mac}"' for _vlan_id, mac, _ip in rows)
    text = _replace_line(text, ":local vid ", f":local vid {{{vid}}}")
    text = _replace_line(text, ":local vmac ", f":local vmac {{{vmac}}}")
    # 中文注释：PPP连接密码只改本次生成的rsc，不写入持久设置。
    text = re.sub(
        r'(/ppp secret add name=\("user"\.\$i\) password=)[^\s]+(\s+remote-address=)',
        rf"\g<1>{config.ppp_password}\g<2>",
        text,
    )
    l2tp_username = ros_l2tp_username(config)
    text = re.sub(r'\("user"\.\$i\)', f'("{l2tp_username}".$i)', text)
    # 中文注释：许可证账号和密码由客户端任务启动时从服务器配置动态获取。
    text = re.sub(
        r"/sys license renew account=\S+ password=\S+ level=p10",
        f"/sys license renew account={config.license_account} password={config.license_password} level=p10",
        text,
    )
    # 中文注释：ROS随机连接账密时，L2TP预共享密钥必须与本次PPP密码统一，结果框和实际RouterOS配置才一致。
    text = _ensure_ros_l2tp_ipsec(text, ros_l2tp_psk(config))
    text = _append_ros_l2tp_self_check(text, 10 if template_name == ROS_TEN_REGION_TEMPLATE else 5)
    text = _move_ip_service_block_to_end(text)
    return template_name, text
