import argparse
import os
import sys
import shutil
import subprocess
import platform
import traceback
import urllib.request
import urllib.error
import json
import tempfile
from pathlib import Path
import time

from cli_support import Console, init_localization


PROJECT_BASE: Path = Path(__file__).parent.parent.resolve()
MFW_REPO: str = "MaaXYZ/MaaFramework"
MXU_REPO: str = "MistEO/MXU"
MAAEND_REPO: str = "MaaEnd/MaaEnd"


def create_directory_link(src: Path, dst: Path) -> bool:
    """
    在指定位置创建一个指定目录的链接
    - Windows：Junction
    - Unix/macOS：symlink
    """
    if dst.exists() or dst.is_symlink():
        if dst.is_dir() and not dst.is_symlink():
            try:
                dst.rmdir()
            except OSError:
                shutil.rmtree(dst)
        else:
            dst.unlink(missing_ok=True)

    dst.parent.mkdir(parents=True, exist_ok=True)

    if platform.system() == "Windows":
        result = subprocess.run(
            ["cmd", "/c", "mklink", "/J", str(dst), str(src)],
            capture_output=True,
            text=True,
        )
        if result.returncode != 0:
            print(Console.err(t("err_create_junction_failed", stderr=result.stderr)))
            return False
    else:
        dst.symlink_to(src)

    return True

LOCALS_DIR = Path(__file__).parent / "locals" / "setup_workspace"


_local_t = lambda key, **kwargs: key.format(**kwargs) if kwargs else key


def init_local() -> None:
    global _local_t
    t_func, load_error_path = init_localization(LOCALS_DIR)
    _local_t = t_func
    if load_error_path:
        print(Console.err(t("error_load_locale", path=load_error_path)))


def t(key: str, **kwargs) -> str:
    return _local_t(key, **kwargs)


try:
    OS_KEYWORD: str = {
        "windows": "win",
        "linux": "linux",
        "darwin": "macos",
    }[platform.system().lower()]
except KeyError as e:
    raise RuntimeError(
        f"Unrecognized operating system: {platform.system().lower()}"
    ) from e

try:
    ARCH_KEYWORD: str = {
        "amd64": "x86_64",
        "x86_64": "x86_64",
        "aarch64": "aarch64",
        "arm64": "aarch64",
    }[platform.machine().lower()]
except KeyError as e:
    raise RuntimeError(
        f"Unrecognized architecture: {platform.machine().lower()}"
    ) from e

try:
    MFW_DIST_NAME: str = {
        "win": "MaaFramework.dll",
        "linux": "libMaaFramework.so",
        "macos": "libMaaFramework.dylib",
    }[OS_KEYWORD]
except KeyError as e:
    raise RuntimeError(f"Unsupported OS for MaaFramework: {OS_KEYWORD}") from e

MXU_DIST_NAME: str = "mxu.exe" if OS_KEYWORD == "win" else "mxu"
CPP_ALGO_DIST_NAME: str = "cpp-algo.exe" if OS_KEYWORD == "win" else "cpp-algo"
ARCH_VARIANT_HINTS: dict[str, tuple[str, ...]] = {
    "x86_64": ("x86_64", "amd64", "x64"),
    "aarch64": ("aarch64", "arm64"),
}
TIMEOUT: int = 30
CACHE_DIR: Path = PROJECT_BASE / ".cache"
VERSION_FILE_NAME: str = "version.json"


def configure_token() -> None:
    """配置 GitHub Token，输出检测结果"""
    token = os.environ.get("GITHUB_TOKEN") or os.environ.get("GH_TOKEN")
    if token:
        print(Console.ok(t("inf_github_token_configured")))
    else:
        print(Console.warn(t("wrn_github_token_not_configured")))
        print(Console.info(t("inf_github_token_hint")))
    print("-" * 40)


def run_command(
    cmd: list[str] | str, cwd: Path | str | None = None, shell: bool = False
) -> bool:
    """执行命令并输出日志，返回是否成功"""
    cmd_str = " ".join(cmd) if isinstance(cmd, list) else str(cmd)
    print(f"{Console.info(t('cmd_prefix'))} {cmd_str}")
    try:
        subprocess.check_call(cmd, cwd=cwd or PROJECT_BASE, shell=shell)
        print(Console.ok(t("inf_command_success", cmd=cmd_str)))
        return True
    except subprocess.CalledProcessError as e:
        print(Console.err(t("err_command_failed", cmd=cmd_str, error=e)))
        return False


def update_submodules(skip_if_exist: bool = True) -> bool:
    print(Console.hdr(t("inf_check_submodules")))

    # 兼容旧版本：model 可能是普通文件夹而非子模块，需要删除以确保子模块正常 clone
    model_path = PROJECT_BASE / "assets" / "resource" / "model"
    if model_path.is_dir() and not (model_path / "LICENSE").exists():
        print(Console.warn(t("wrn_model_not_submodule", path=model_path)))
        shutil.rmtree(model_path)
        print(Console.ok(t("inf_model_dir_removed", path=model_path)))

    if (
        not skip_if_exist
        or not (model_path / "LICENSE").exists()
        or not (PROJECT_BASE / "agent" / "cpp-algo" / "MaaUtils" / "MaaUtils.cmake").exists()
    ):
        print(Console.info(t("inf_updating_submodules")))
        return run_command(["git", "submodule", "update", "--init", "--recursive"])
    print(Console.ok(t("inf_submodules_exist")))
    return True


def bootstrap_maadeps(skip_if_exist: bool = True) -> bool:
    """下载 MaaDeps 预编译依赖"""
    maadeps_dir = (
        PROJECT_BASE / "agent" / "cpp-algo" / "MaaUtils" / "MaaDeps" / "vcpkg" / "installed"
    )
    if skip_if_exist and maadeps_dir.exists() and any(maadeps_dir.iterdir()):
        print(Console.ok(t("inf_maadeps_exist")))
        return True

    print(Console.info(t("inf_bootstrap_maadeps")))
    script_path = PROJECT_BASE / "tools" / "maadeps-download.py"
    return run_command([sys.executable, str(script_path)])


def run_build_script() -> bool:
    print(Console.hdr(t("inf_run_build_script")))
    script_path = PROJECT_BASE / "tools" / "build_and_install.py"
    return run_command([sys.executable, str(script_path)])


def get_latest_release_url(
    repo: str, keywords: list[str], prerelease: bool = True
) -> tuple[str | None, str | None, str | None]:
    """
    获取指定 GitHub 仓库 Release 中首个符合是否预发布要求，且匹配所有关键字的资源下载链接和文件名。

    https://docs.github.com/en/rest/releases/releases?apiVersion=2022-11-28#list-releases
    """
    api_url = f"https://api.github.com/repos/{repo}/releases"
    token = os.environ.get("GITHUB_TOKEN") or os.environ.get("GH_TOKEN")

    try:
        print(Console.info(t("inf_get_latest_release", repo=repo)))

        req = urllib.request.Request(api_url)
        if token:
            req.add_header("Authorization", f"Bearer {token}")
        req.add_header("Accept", "application/vnd.github+json")
        req.add_header("User-Agent", "MaaEnd-setup")
        req.add_header("X-GitHub-Api-Version", "2022-11-28")

        with urllib.request.urlopen(req, timeout=TIMEOUT) as res:
            tags = json.loads(res.read().decode())
            assert isinstance(tags, list)
            if not tags:
                raise ValueError("No releases found (GitHub API)")

        for tag in tags:
            assert isinstance(tag, dict)
            if (
                not prerelease
                and tag.get("prerelease", False)
                or tag.get("draft", False)
            ):
                continue
            assets = tag.get("assets", [])
            assert isinstance(assets, list)

            for asset in assets:
                assert isinstance(asset, dict)
                name = asset["name"].lower()
                if all(k.lower() in name for k in keywords):
                    print(Console.ok(t("inf_matched_asset", name=asset["name"])))
                    tag_name = tag.get("tag_name") or tag.get("name")
                    return asset["browser_download_url"], asset["name"], tag_name

        raise ValueError("No matching asset found in the latest release (GitHub API)")
    except Exception as e:
        print(Console.err(t("err_get_release_failed", error_type=type(e).__name__, error=e)))

    return None, None, None


def read_versions_file(path: Path) -> dict[str, str]:
    if not path.exists():
        return {}
    try:
        with open(path, "r", encoding="utf-8") as f:
            data = json.load(f)
        versions = data.get("versions", {})
        if isinstance(versions, dict):
            return {str(k): str(v) for k, v in versions.items()}
    except Exception as e:
        print(Console.warn(t("wrn_read_version_failed", error=e)))
    return {}


def write_versions_file(path: Path, versions: dict[str, str]) -> None:
    try:
        path.parent.mkdir(parents=True, exist_ok=True)
        with open(path, "w", encoding="utf-8") as f:
            json.dump({"versions": versions}, f, ensure_ascii=False, indent=4)
        print(Console.ok(t("inf_write_version_file", path=path)))
        print(Console.info(t("inf_current_versions", versions=versions)))
    except Exception as e:
        print(Console.warn(t("wrn_write_version_failed", error=e)))


def parse_semver(version: str) -> tuple[list[int], list[str]]:
    """Parse a semver string into (core_numbers, prerelease_identifiers).

    Implements SemVer 2.0.0 precedence essentials used by compare_semver:
    - Ignore leading 'v'/'V'
    - Ignore build metadata (+...)
    - Compare core version as numeric dot-separated identifiers
    - Handle prerelease precedence (alpha/beta/rc, numeric identifiers, etc.)
    """
    if not version:
        return [], []

    v = version.strip()
    if v.startswith(("v", "V")):
        v = v[1:]

    # Drop build metadata for precedence comparison.
    if "+" in v:
        v = v.split("+", 1)[0]

    # Split core and prerelease.
    core_part, pre_part = (v.split("-", 1) + [""])[:2] if "-" in v else (v, "")

    def parse_core_number(part: str) -> int:
        num = ""
        for ch in part:
            if ch.isdigit():
                num += ch
            else:
                break
        return int(num) if num else 0

    core_numbers = [parse_core_number(p) for p in core_part.split(".") if p != ""]
    prerelease = [p for p in pre_part.split(".") if p != ""] if pre_part else []
    return core_numbers, prerelease


def compare_semver(a: str | None, b: str | None) -> int:
    if not a and not b:
        return 0
    if a and not b:
        return 1
    if b and not a:
        return -1

    left_core, left_pre = parse_semver(a or "")
    right_core, right_pre = parse_semver(b or "")

    # Compare major.minor.patch (or longer) numerically.
    max_len = max(len(left_core), len(right_core))
    left_core += [0] * (max_len - len(left_core))
    right_core += [0] * (max_len - len(right_core))
    for l, r in zip(left_core, right_core):
        if l > r:
            return 1
        if l < r:
            return -1

    # Core equal: version without prerelease has higher precedence.
    if not left_pre and not right_pre:
        return 0
    if not left_pre and right_pre:
        return 1
    if left_pre and not right_pre:
        return -1

    # Both prerelease: compare dot-separated identifiers.
    def is_numeric_identifier(s: str) -> bool:
        return s.isdigit()

    for l, r in zip(left_pre, right_pre):
        l_num = is_numeric_identifier(l)
        r_num = is_numeric_identifier(r)

        if l_num and r_num:
            li, ri = int(l), int(r)
            if li > ri:
                return 1
            if li < ri:
                return -1
            continue

        if l_num and not r_num:
            return -1  # numeric < non-numeric
        if not l_num and r_num:
            return 1

        # both non-numeric: ASCII lexical compare
        if l > r:
            return 1
        if l < r:
            return -1

    # All shared identifiers equal: shorter prerelease has lower precedence.
    if len(left_pre) > len(right_pre):
        return 1
    if len(left_pre) < len(right_pre):
        return -1
    return 0


def ensure_cache_dir() -> Path:
    CACHE_DIR.mkdir(parents=True, exist_ok=True)
    return CACHE_DIR


def cleanup_cache_file(path: Path) -> None:
    try:
        if path.exists():
            path.unlink()
            print(Console.ok(t("inf_cache_cleaned", path=path)))
        meta = Path(str(path) + ".url")
        if meta.exists():
            meta.unlink()
    except OSError as e:
        print(Console.warn(t("wrn_cache_clean_failed", path=path, error=e)))


def clean_cache() -> None:
    if not CACHE_DIR.exists():
        print(Console.info(t("inf_cache_empty")))
        return
    total_size = 0
    count = 0
    for f in CACHE_DIR.iterdir():
        if f.is_file():
            total_size += f.stat().st_size
            count += 1
    if count == 0:
        print(Console.info(t("inf_cache_empty")))
        return
    size_mb = total_size / (1024 * 1024)
    print(Console.info(t("inf_cache_summary", count=count, size=f"{size_mb:.1f} MB")))
    try:
        shutil.rmtree(CACHE_DIR)
        print(Console.ok(t("inf_cache_purged")))
    except OSError as e:
        print(Console.warn(t("wrn_cache_clean_failed", path=CACHE_DIR, error=e)))


def download_file(url: str, dest_path: Path, resume: bool = False) -> bool:
    """下载文件到指定路径。"""

    def to_percentage(current: float, total: float) -> str:
        return f"{(current / total) * 100:.1f}%" if total > 0 else ""

    def to_file_size(size: int | None) -> str:
        if size is None or size < 0:
            return "--"
        s = float(size)
        for unit in ["B", "KB", "MB", "GB", "TB"]:
            if s < 1024.0 or unit == "TB":
                return f"{s:.1f} {unit}"
            s /= 1024.0
        return "--"

    def to_speed(bps: float) -> str:
        if bps is None or bps <= 0:
            return "--/s"
        s = float(bps)
        for unit in ["B/s", "KB/s", "MB/s", "GB/s"]:
            if s < 1024.0 or unit == "GB/s":
                return f"{s:.1f} {unit}"
            s /= 1024.0
        return "--/s"

    def seconds_to_hms(sec: float | None) -> str:
        if sec is None or sec < 0:
            return "--:--:--"
        sec = int(sec)
        h = sec // 3600
        m = (sec % 3600) // 60
        s = sec % 60
        return f"{h:02d}:{m:02d}:{s:02d}"

    _retried_416 = False

    try:
        print(Console.info(t("inf_start_download", url=url)))

        url_meta = Path(str(dest_path) + ".url")

        if resume and dest_path.exists() and dest_path.stat().st_size > 0:
            if url_meta.exists():
                try:
                    cached_url = url_meta.read_text(encoding="utf-8").strip()
                except OSError:
                    cached_url = ""
                if cached_url and cached_url != url:
                    print(Console.warn(t("wrn_cache_url_mismatch")))
                    cleanup_cache_file(dest_path)
                    if dest_path.exists():
                        resume = False

        while True:
            existing_size = 0
            if resume and not _retried_416 and dest_path.exists():
                existing_size = dest_path.stat().st_size
                if existing_size > 0:
                    print(Console.info(t("inf_resume_detected", size=to_file_size(existing_size))))

            req = urllib.request.Request(url)
            req.add_header("User-Agent", "MaaEnd-setup")
            if existing_size > 0:
                req.add_header("Range", f"bytes={existing_size}-")

            print(Console.info(t("inf_connecting")), end="", flush=True)
            try:
                res = urllib.request.urlopen(req, timeout=TIMEOUT)
            except urllib.error.HTTPError as he:
                if he.code == 416 and existing_size > 0 and not _retried_416:
                    print()
                    _retried_416 = True
                    cleanup_cache_file(dest_path)
                    continue
                raise

            break

        with res:
            status_code = res.getcode()
            if status_code == 206:
                content_range = res.headers.get("Content-Range", "")
                size_total = 0
                if "/" in content_range:
                    total_str = content_range.rsplit("/", 1)[-1].strip()
                    if total_str != "*":
                        try:
                            size_total = int(total_str)
                        except (ValueError, TypeError):
                            size_total = 0
                file_mode = "ab"
                size_received = existing_size
                print(Console.info(
                    t("inf_resuming_download",
                      downloaded=to_file_size(existing_size),
                      total=to_file_size(size_total))
                ))
            else:
                size_total = int(res.headers.get("Content-Length", 0) or 0)
                file_mode = "wb"
                size_received = 0
                if existing_size > 0:
                    print(Console.warn(t("wrn_resume_not_supported")))

            session_received = 0
            cached_progress_str = ""
            start_ts = time.time()

            with open(dest_path, file_mode) as out_file:
                while True:
                    chunk = res.read(8192)
                    if not chunk:
                        break
                    out_file.write(chunk)
                    size_received += len(chunk)
                    session_received += len(chunk)

                    elapsed = max(1e-6, time.time() - start_ts)
                    speed = session_received / elapsed
                    eta = None
                    if size_total > 0 and speed > 0:
                        eta = (size_total - size_received) / speed

                    progress_str = (
                        f"{to_file_size(size_received)}/{to_file_size(size_total)} "
                        f"({to_percentage(size_received, size_total)}) | "
                        f"{to_speed(speed)} | ETA {seconds_to_hms(eta)}"
                    )

                    if progress_str != cached_progress_str:
                        print(
                            f"\r{Console.info(t('inf_downloading', progress=progress_str))}",
                            end="",
                            flush=True,
                        )
                        cached_progress_str = progress_str
        print()
        print(Console.ok(t("inf_download_complete", path=dest_path)))
        try:
            url_meta.write_text(url, encoding="utf-8")
        except OSError:
            pass
        return True
    except urllib.error.URLError as e:
        print(Console.err(t("err_network_error", reason=e.reason)))
    except Exception as e:
        print(Console.err(t("err_download_failed", error_type=type(e).__name__, error=e)))
    return False


def install_maafw(
    install_root: Path,
    skip_if_exist: bool = True,
    update_mode: bool = False,
    local_version: str | None = None,
) -> tuple[bool, str | None, bool]:
    """安装 MaaFramework，若遇占用则提示用户手动处理"""
    real_install_root = install_root.resolve()
    maafw_dest = real_install_root / "maafw"
    maafw_deps = PROJECT_BASE / "deps"
    maafw_installed = maafw_deps.exists() and any(maafw_deps.iterdir())

    if skip_if_exist and maafw_installed:
        print(Console.ok(t("inf_maafw_installed_skip")))
        return True, local_version, False

    url, filename, remote_version = get_latest_release_url(
        MFW_REPO, ["maa", OS_KEYWORD, ARCH_KEYWORD]
    )
    if not url or not filename:
        print(Console.err(t("err_maafw_url_not_found")))
        return False, local_version, False

    if (
        update_mode
        and maafw_installed
        and local_version
        and remote_version
        and compare_semver(local_version, remote_version) >= 0
    ):
        print(Console.ok(t("inf_maafw_latest_version", version=local_version)))
        return True, local_version, False

    cache_dir = ensure_cache_dir()
    download_path = cache_dir / filename
    if not download_file(url, download_path, resume=True):
        return False, local_version, False

    with tempfile.TemporaryDirectory() as tmp_dir:
        tmp_path = Path(tmp_dir)

        maafw_dest_is_link = maafw_dest.is_symlink()
        if hasattr(maafw_dest, 'is_junction'):
            maafw_dest_is_link = maafw_dest_is_link or maafw_dest.is_junction()

        if maafw_dest_is_link:
            print(Console.ok(t("inf_link_already_exists", path=maafw_dest)))
        elif maafw_dest.exists():
            if maafw_dest.is_dir():
                while True:
                    try:
                        print(Console.info(t("inf_delete_old_dir", path=maafw_dest)))
                        shutil.rmtree(maafw_dest)
                        break
                    except PermissionError as e:
                        print(Console.err(t("err_permission_denied", error=e)))
                        print(Console.err(t("err_cannot_delete_maafw", path=maafw_dest)))
                        cmd = input(t("prompt_retry_or_quit")).strip().lower()
                        if cmd == "q":
                            return False, local_version, False
                    except Exception as e:
                        print(Console.err(t("err_unknown_error_delete", error=e)))
                        return False, local_version, False
            else:
                maafw_dest.unlink(missing_ok=True)

        print(Console.info(t("inf_extract_maafw")))
        try:
            extract_root = tmp_path / "extracted"
            extract_root.mkdir(parents=True, exist_ok=True)

            # 使用 shutil.unpack_archive 自动识别格式进行解压
            shutil.unpack_archive(str(download_path), extract_root)

            # 找到包含 bin 目录的 SDK 根目录
            sdk_root = None
            for root, dirs, _ in os.walk(extract_root):
                if "bin" in dirs:
                    sdk_root = Path(root)
                    break

            if not sdk_root:
                print(Console.err(t("err_bin_not_found")))
                return False, local_version, False

            # 先将完整 SDK 复制到项目根目录 deps/
            maafw_deps = PROJECT_BASE / "deps"
            print(Console.info(t("inf_copying_sdk", dest=maafw_deps)))
            if maafw_deps.exists():
                shutil.rmtree(maafw_deps)
            shutil.copytree(sdk_root, maafw_deps)
            print(Console.ok(t("inf_sdk_copied", dest=maafw_deps)))

            if not maafw_dest_is_link:
                # 创建 install/maafw -> deps/bin 的目录链接
                bin_path = maafw_deps / "bin"
                print(Console.info(t("inf_creating_link", link=maafw_dest, target=bin_path)))
                if not create_directory_link(bin_path, maafw_dest):
                    print(Console.err(t("err_create_link_failed")))
                    return False, local_version, False

            print(Console.ok(t("inf_maafw_install_complete")))
            cleanup_cache_file(download_path)
            return True, remote_version or local_version, True
        except Exception as e:
            print(Console.err(t("err_maafw_install_failed", error=e)))
            return False, local_version, False


def install_mxu(
    install_root: Path,
    skip_if_exist: bool = True,
    update_mode: bool = False,
    local_version: str | None = None,
) -> tuple[bool, str | None, bool]:
    """安装 MXU，若遇占用则提示用户手动处理"""
    real_install_root = install_root.resolve()
    mxu_path = real_install_root / MXU_DIST_NAME
    mxu_installed = mxu_path.exists()

    if skip_if_exist and mxu_installed:
        print(Console.ok(t("inf_mxu_installed_skip")))
        return True, local_version, False

    url, filename, remote_version = get_latest_release_url(
        MXU_REPO, ["mxu", OS_KEYWORD, ARCH_KEYWORD]
    )
    if not url or not filename:
        print(Console.err(t("err_mxu_url_not_found")))
        return False, local_version, False

    if (
        update_mode
        and mxu_installed
        and local_version
        and remote_version
        and compare_semver(local_version, remote_version) >= 0
    ):
        print(Console.ok(t("inf_mxu_latest_version", version=local_version)))
        return True, local_version, False

    cache_dir = ensure_cache_dir()
    download_path = cache_dir / filename
    if not download_file(url, download_path, resume=True):
        return False, local_version, False

    with tempfile.TemporaryDirectory() as tmp_dir:
        tmp_path = Path(tmp_dir)

        if mxu_path.exists():
            while True:
                try:
                    print(Console.info(t("inf_delete_old_file", path=mxu_path)))
                    mxu_path.unlink()
                    break
                except PermissionError as e:
                    print(Console.err(t("err_permission_denied", error=e)))
                    print(Console.err(t("err_cannot_delete_mxu", name=MXU_DIST_NAME)))
                    cmd = input(t("prompt_retry_or_quit")).strip().lower()
                    if cmd == "q":
                        return False, local_version, False
                except Exception as e:
                    print(Console.err(t("err_unknown_error_delete_file", error=e)))
                    return False, local_version, False

        print(Console.info(t("inf_extract_install_mxu")))
        try:
            extract_root = tmp_path / "extracted"
            extract_root.mkdir(parents=True, exist_ok=True)

            # 使用 shutil.unpack_archive 自动识别格式进行解压
            shutil.unpack_archive(str(download_path), extract_root)

            real_install_root.mkdir(parents=True, exist_ok=True)
            target_files = [MXU_DIST_NAME]
            if OS_KEYWORD == "win":
                target_files.append("mxu.pdb")

            copied = False
            for item in extract_root.iterdir():
                if item.name.lower() in [f.lower() for f in target_files]:
                    dest = real_install_root / item.name
                    shutil.copy2(item, dest)
                    print(Console.ok(t("inf_updated_file", name=item.name)))
                    if item.name.lower() == MXU_DIST_NAME.lower():
                        copied = True

            if not copied:
                print(Console.err(t("err_mxu_not_found", name=MXU_DIST_NAME)))
                return False, local_version, False
            print(Console.ok(t("inf_mxu_install_complete")))
            cleanup_cache_file(download_path)
            return True, remote_version or local_version, True
        except Exception as e:
            print(Console.err(t("err_mxu_install_failed", error=e)))
            return False, local_version, False


def find_cpp_algo_binary(search_root: Path) -> Path | None:
    preferred_names = (
        ["cpp-algo.exe", "cpp-algo"] if OS_KEYWORD == "win" else ["cpp-algo", "cpp-algo.exe"]
    )
    candidates: list[Path] = []
    for name in preferred_names:
        candidates.extend(path for path in search_root.rglob(name) if path.is_file())

    if not candidates:
        return None

    def _arch_rank(path_parts: list[str]) -> int:
        joined_path = "/".join(path_parts)
        preferred_hints = set(ARCH_VARIANT_HINTS.get(ARCH_KEYWORD, ()))
        all_hints = {hint for hints in ARCH_VARIANT_HINTS.values() for hint in hints}
        has_preferred_arch = any(hint in joined_path for hint in preferred_hints)
        has_other_arch = any(hint in joined_path for hint in (all_hints - preferred_hints))
        if has_preferred_arch:
            return 0
        if has_other_arch:
            return 2
        return 1

    def _score(path: Path) -> tuple[int, int, int, int]:
        path_parts = [part.lower() for part in path.parts]
        in_agent_dir = "agent" in path_parts
        agent_dir_rank = 0 if in_agent_dir else 1
        preferred_name_rank = 0 if path.name.lower() == preferred_names[0] else 1
        return agent_dir_rank, preferred_name_rank, _arch_rank(path_parts), len(path_parts)

    candidates.sort(key=_score)
    return candidates[0]


def _replace_file_with_retry(src_path: Path, target_path: Path) -> None:
    tmp_target = target_path.with_name(f".{target_path.name}.tmp")
    while True:
        try:
            tmp_target.unlink(missing_ok=True)
            shutil.copy2(src_path, tmp_target)
            os.replace(tmp_target, target_path)
            return
        except PermissionError as e:
            tmp_target.unlink(missing_ok=True)
            print(Console.err(t("err_permission_denied", error=e)))
            cmd = input(t("prompt_retry_or_quit")).strip().lower()
            if cmd == "q":
                raise
        except Exception:
            tmp_target.unlink(missing_ok=True)
            raise


def _is_supported_archive(path: Path) -> bool:
    lower_name = path.name.lower()
    for _, extensions, _ in shutil.get_unpack_formats():
        if any(lower_name.endswith(ext.lower()) for ext in extensions):
            return True
    return False


def copy_cpp_algo_binary(src_path: Path, install_root: Path) -> None:
    agent_dir = install_root / "agent"
    agent_dir.mkdir(parents=True, exist_ok=True)

    target_path = agent_dir / CPP_ALGO_DIST_NAME
    _replace_file_with_retry(src_path, target_path)
    print(Console.ok(t("inf_updated_file", name=target_path.name)))

    if OS_KEYWORD != "win":
        target_path.chmod(target_path.stat().st_mode | 0o111)

    pdb_src = src_path.with_suffix(".pdb")
    if pdb_src.exists():
        pdb_target = agent_dir / f"{Path(CPP_ALGO_DIST_NAME).stem}.pdb"
        _replace_file_with_retry(pdb_src, pdb_target)
        print(Console.ok(t("inf_updated_file", name=pdb_target.name)))


def install_cpp_algo(
    install_root: Path,
    skip_if_exist: bool = True,
    update_mode: bool = False,
    local_version: str | None = None,
) -> tuple[bool, str | None, bool]:
    real_install_root = install_root.resolve()
    cpp_algo_path = real_install_root / "agent" / CPP_ALGO_DIST_NAME
    cpp_algo_installed = cpp_algo_path.exists()

    if skip_if_exist and cpp_algo_installed:
        print(Console.ok(t("inf_cpp_algo_installed_skip")))
        return True, local_version, False

    url, filename, remote_version = get_latest_release_url(
        MAAEND_REPO, ["maaend", OS_KEYWORD, ARCH_KEYWORD]
    )
    if not url or not filename:
        print(Console.err(t("err_cpp_algo_url_not_found")))
        return False, local_version, False

    if (
        update_mode
        and cpp_algo_installed
        and local_version
        and remote_version
        and compare_semver(local_version, remote_version) >= 0
    ):
        print(Console.ok(t("inf_cpp_algo_latest_version", version=local_version)))
        return True, local_version, False

    cache_dir = ensure_cache_dir()
    download_path = cache_dir / filename
    if not download_file(url, download_path, resume=True):
        return False, local_version, False

    with tempfile.TemporaryDirectory() as tmp_dir:
        tmp_path = Path(tmp_dir)

        print(Console.info(t("inf_extract_install_cpp_algo")))
        try:
            lower_filename = filename.lower()
            if lower_filename.endswith(".dmg"):
                if platform.system() != "Darwin":
                    print(Console.err(t("err_cpp_algo_dmg_unsupported")))
                    return False, local_version, False

                mount_dir = tmp_path / "mounted"
                mount_dir.mkdir(parents=True, exist_ok=True)
                attach_result = subprocess.run(
                    [
                        "hdiutil",
                        "attach",
                        str(download_path),
                        "-nobrowse",
                        "-readonly",
                        "-mountpoint",
                        str(mount_dir),
                    ],
                    capture_output=True,
                    text=True,
                )
                if attach_result.returncode != 0:
                    error_message = (
                        attach_result.stderr.strip()
                        or attach_result.stdout.strip()
                        or str(attach_result.returncode)
                    )
                    print(Console.err(t("err_cpp_algo_dmg_attach_failed", error=error_message)))
                    return False, local_version, False

                try:
                    cpp_algo_src = find_cpp_algo_binary(mount_dir)
                    if not cpp_algo_src:
                        print(Console.err(t("err_cpp_algo_not_found", name=CPP_ALGO_DIST_NAME)))
                        return False, local_version, False
                    copy_cpp_algo_binary(cpp_algo_src, real_install_root)
                finally:
                    detach_result = subprocess.run(
                        ["hdiutil", "detach", str(mount_dir), "-force"],
                        capture_output=True,
                        text=True,
                    )
                    if detach_result.returncode != 0:
                        error_message = (
                            detach_result.stderr.strip()
                            or detach_result.stdout.strip()
                            or str(detach_result.returncode)
                        )
                        print(Console.warn(t("wrn_cpp_algo_dmg_detach_failed", error=error_message)))
            else:
                cpp_algo_src: Path | None = None
                if _is_supported_archive(download_path):
                    extract_root = tmp_path / "extracted"
                    extract_root.mkdir(parents=True, exist_ok=True)
                    shutil.unpack_archive(str(download_path), extract_root)
                    cpp_algo_src = find_cpp_algo_binary(extract_root)
                elif download_path.is_file():
                    cpp_algo_src = download_path

                if not cpp_algo_src:
                    print(Console.err(t("err_cpp_algo_not_found", name=CPP_ALGO_DIST_NAME)))
                    return False, local_version, False
                copy_cpp_algo_binary(cpp_algo_src, real_install_root)

            print(Console.ok(t("inf_cpp_algo_install_complete")))
            cleanup_cache_file(download_path)
            return True, remote_version or local_version, True
        except Exception as e:
            traceback.print_exc()
            error_with_type = f"{type(e).__name__}: {e}"
            print(Console.err(t("err_cpp_algo_install_failed", error=error_with_type)))
            return False, local_version, False


def _is_cn_locale() -> bool:
    """检测当前系统语言是否为简体中文"""
    import locale as _locale

    loc = _locale.getlocale()
    lang = (loc[0] or "").lower()
    return lang in ("zh_cn", "chinese (simplified)_china")


def main() -> None:
    init_local()

    if _is_cn_locale():
        print(
            Console.warn(
                "[提示] 本脚本需要访问 GitHub，若出现下载超时或连接失败，可尝试配置系统代理"
            )
        )
        print("-" * 60)

    parser = argparse.ArgumentParser(description=t("description"))
    parser.add_argument("--update", action="store_true", help=t("arg_update"))
    parser.add_argument("--ci", action="store_true", help=t("arg_ci"))
    parser.add_argument("--clean-cache", action="store_true", help=t("arg_clean_cache"))
    args = parser.parse_args()

    if args.clean_cache:
        clean_cache()
        return

    install_dir = PROJECT_BASE / "install"
    version_file = install_dir / VERSION_FILE_NAME
    local_versions = read_versions_file(version_file)
    print(Console.hdr(t("header_workspace_init")))
    configure_token()
    if not update_submodules(skip_if_exist=not args.update):
        print(Console.err(t("fatal_submodule_failed")))
        sys.exit(1)
    print(Console.hdr(t("header_bootstrap_maadeps")))
    if not bootstrap_maadeps(skip_if_exist=True):   # 这玩意太慢了，也不常更新，没必要每次下载
        print(Console.err(t("fatal_maadeps_failed")))
        sys.exit(1)
    print(Console.hdr(t("header_build_go")))
    if not run_build_script():
        print(Console.err(t("fatal_build_failed")))
        sys.exit(1)
    print(Console.hdr(t("header_download_deps")))
    versions: dict[str, str] = dict(local_versions)
    any_downloaded = False
    maafw_ok, maafw_version, maafw_downloaded = install_maafw(
        install_dir,
        skip_if_exist=not args.update,
        update_mode=args.update,
        local_version=local_versions.get("maafw"),
    )
    if not maafw_ok:
        print(Console.err(t("fatal_maafw_failed")))
        sys.exit(1)
    if maafw_version:
        versions["maafw"] = maafw_version
    any_downloaded = any_downloaded or maafw_downloaded

    mxu_ok, mxu_version, mxu_downloaded = install_mxu(
        install_dir,
        skip_if_exist=not args.update,
        update_mode=args.update,
        local_version=local_versions.get("mxu"),
    )
    if not mxu_ok:
        print(Console.err(t("fatal_mxu_failed")))
        sys.exit(1)
    if mxu_version:
        versions["mxu"] = mxu_version
    any_downloaded = any_downloaded or mxu_downloaded

    cpp_algo_ok, cpp_algo_version, cpp_algo_downloaded = install_cpp_algo(
        install_dir,
        skip_if_exist=not args.update,
        update_mode=args.update,
        local_version=local_versions.get("cpp_algo"),
    )
    if not cpp_algo_ok:
        print(Console.err(t("fatal_cpp_algo_failed")))
        sys.exit(1)
    if cpp_algo_version:
        versions["cpp_algo"] = cpp_algo_version
    any_downloaded = any_downloaded or cpp_algo_downloaded

    if not args.ci and any_downloaded:
        write_versions_file(version_file, versions)
    print(Console.ok(t("header_setup_complete")))
    print(Console.info(t("inf_workspace_ready", mxu_path=install_dir / MXU_DIST_NAME)))
    print(Console.info(t("inf_install_dir_hint", install_dir=install_dir)))

    dev_doc = (
        PROJECT_BASE / "docs/zh_cn/developers/README.md"
        if _is_cn_locale()
        else PROJECT_BASE / "docs/en_us/developers/README.md"
    )
    print(Console.info(t("inf_read_dev_doc", doc_path=dev_doc)))


if __name__ == "__main__":
    main()
