# @pattern Strategy, Guarded Command
"""khy OS — `khy os` 启动前的 QEMU 前置依赖交互闸(Python 启动器侧)。

QEMU 是 ``khy os`` 启动虚机的唯一宿主前置依赖(Node 侧
``platform/packages/shared/src/runtime/khyos/KhyOsRunner.js`` 用
``qemu-system-x86_64`` spawn 虚机);普通 ``khy`` REPL/chat 不需要它。今天 manifest
里的便携 QEMU pin 是空占位符 → 没有任何自动下载发生,Linux/macOS 必须宿主自带
QEMU,缺失时只会在 KhyOsRunner spawn 失败处报错,用户没有「是否现在安装?」的入口。

本模块在 ``khy os`` 启动路径(把终端交给 TUI 之前)补这个交互入口:检测 QEMU,
缺失则询问「是否现在安装?」,答「是」→ **运行系统包管理器**(apt/dnf/pacman/zypper/
brew/winget/choco),终端透传以便 sudo/UAC 自行弹密码;答「否」→ 打印手动提示并继续
(由既有 KhyOsRunner 错误路径如实兜底)。

诚实边界(与 ``node_provisioner`` 的「绝不自动提权」策略相比的刻意放宽):答「是」
确实会运行包管理器,但这**不是静默提权**——双重同意:① 我方 ``[y/N]`` 提示,
② OS 自身的 sudo/UAC 密码提示(khy 绝不替用户输入密码)。``run`` 失败/无受支持包
管理器/装后仍缺 → 一律软化为打印手动提示,绝不假装成功。

硬契约:``maybe_prompt_install_qemu`` **全程失败软化**——任何异常都吞掉,绝不让这个
前置助手阻断 ``khy os``。纯决策函数(``prompt_enabled`` / ``resolve_qemu_exe`` /
``select_install_command`` / ``install_hint_lines``)零 IO、可离线单测;IO(探测/询问/
执行)收在 ``maybe_prompt_install_qemu`` 且依赖注入(``which``/``run``/``ask``/
``isatty``),便于单测传假。

门控:``KHY_QEMU_INSTALL_PROMPT``(默认开,``0/false/no/off`` 关)。关 → 什么都不做,
``khy os`` 行为与今日逐字节一致。复用既有 ``KHY_QEMU``:指向已装可执行文件时直接命中。

仅用 ``stdlib``(``os``/``sys``/``shutil``/``subprocess``)。
"""

from __future__ import annotations

import os
import shutil
import subprocess
import sys
from typing import Optional


DEFAULT_QEMU_EXE = "qemu-system-x86_64"

# 受支持的包管理器探测顺序(逐 OS)。pure ``select_install_command`` 据此选第一个
# 在 ``available`` 中命中的。
_LINUX_MANAGER_ORDER = ("apt-get", "dnf", "pacman", "zypper")
_DARWIN_MANAGER_ORDER = ("brew",)
_WIN_MANAGER_ORDER = ("winget", "choco")

# 探测用的全集(thin shell 用它逐个 which)。
PROBE_MANAGERS = (
    "apt-get", "dnf", "pacman", "zypper", "brew", "winget", "choco",
)


# ── 纯决策(零 IO,可单测) ──────────────────────────────────────────────────


def prompt_enabled(env=None) -> bool:
    """是否启用「缺 QEMU 时询问安装」(``KHY_QEMU_INSTALL_PROMPT != 0/false/no/off``)。

    空串/未设 → 开(与 ``node_provisioner._auto_install_enabled`` 同惯例)。
    """
    e = os.environ if env is None else env
    val = str(e.get("KHY_QEMU_INSTALL_PROMPT", "")).strip().lower()
    return val not in {"0", "false", "no", "off"}


def resolve_qemu_exe(env=None) -> str:
    """解析待探测的 QEMU 可执行名(``KHY_QEMU`` 覆盖,默认 qemu-system-x86_64)。

    与 ``khy os doctor`` / ``KhyOsRunner`` 同口径。
    """
    e = os.environ if env is None else env
    return str(e.get("KHY_QEMU") or "").strip() or DEFAULT_QEMU_EXE


def select_install_command(sys_platform: str, available, *, is_root: bool = False) -> Optional[dict]:
    """对给定平台与「可用包管理器集合」选出 QEMU 安装命令。

    返回 ``{'argv': [...], 'display': str, 'manager': str}`` 或 ``None``(无受支持
    包管理器/未知平台)。``-y``/``--noconfirm`` 自动确认包管理器本身(用户已在我方
    提示答「是」);Linux 非 root 时前置 ``sudo``(密码由终端透传),root 省略 sudo。

    纯函数:不探测、不执行,仅按表选择。
    """
    avail = set(available or ())
    plat = (sys_platform or "").lower()

    def _pick(order):
        for mgr in order:
            if mgr in avail:
                return mgr
        return None

    if plat.startswith("linux"):
        mgr = _pick(_LINUX_MANAGER_ORDER)
        if mgr is None:
            return None
        base = {
            "apt-get": ["apt-get", "install", "-y", "qemu-system-x86"],
            "dnf": ["dnf", "install", "-y", "qemu-system-x86"],
            "pacman": ["pacman", "-S", "--noconfirm", "qemu-full"],
            "zypper": ["zypper", "install", "-y", "qemu"],
        }[mgr]
        argv = base if is_root else (["sudo"] + base)
        return {"argv": argv, "display": " ".join(argv), "manager": mgr}

    if plat == "darwin":
        mgr = _pick(_DARWIN_MANAGER_ORDER)
        if mgr is None:
            return None
        # Homebrew 绝不可在 root/sudo 下运行 — 不加 sudo。
        argv = ["brew", "install", "qemu"]
        return {"argv": argv, "display": " ".join(argv), "manager": mgr}

    if plat in {"win32", "cygwin"} or plat.startswith("win"):
        mgr = _pick(_WIN_MANAGER_ORDER)
        if mgr is None:
            return None
        if mgr == "winget":
            argv = ["winget", "install", "--id", "SoftwareFreedomConservancy.QEMU",
                    "-e", "--source", "winget"]
        else:  # choco
            argv = ["choco", "install", "qemu", "-y"]
        return {"argv": argv, "display": " ".join(argv), "manager": mgr}

    return None


def install_hint_lines(sys_platform: str) -> list:
    """答否/无包管理器/装后仍缺时的手动安装提示(与 khyos.js doctor 同款文案)。"""
    plat = (sys_platform or "").lower()
    lines = [
        "QEMU 是运行 `khy os` 的前置依赖,请手动安装后重试:",
        "  macOS:  brew install qemu",
        "  Ubuntu: sudo apt-get install qemu-system-x86",
        "  Fedora: sudo dnf install qemu-system-x86",
        "  Arch:   sudo pacman -S qemu-full",
    ]
    if plat.startswith("win") or plat == "cygwin":
        lines.append("  Windows: 安装 QEMU(https://qemu.weilnetz.de/w64/)并加入 PATH,"
                     "或自备 qemu-system-x86_64.exe 并设环境变量 KHY_QEMU 指向它")
    else:
        lines.append("  或设环境变量 KHY_QEMU 指向已装的 qemu-system-x86_64 可执行文件")
    return lines


def _is_affirmative(answer: str) -> bool:
    """用户回复是否为肯定(中英)。空回车=否(默认 N)。"""
    a = (answer or "").strip().lower()
    return a in {"y", "yes", "是", "好", "好的", "ok"}


# winget 便携/包目录前缀:%LOCALAPPDATA%\Microsoft\WinGet\Packages\<id>\...
WINGET_QEMU_PREFIX = "SoftwareFreedomConservancy.QEMU"


def autolocate_enabled(env=None) -> bool:
    """是否启用「PATH 缺失时自动定位已装 QEMU」(``KHY_QEMU_AUTOLOCATE != 0/false/no/off``)。

    默认开(空串/未设 → 开)。关 → 跳过定位,行为与今日「只探 PATH」逐字节一致。
    """
    e = os.environ if env is None else env
    val = str(e.get("KHY_QEMU_AUTOLOCATE", "")).strip().lower()
    return val not in {"0", "false", "no", "off"}


def qemu_exe_name(sys_platform: str) -> str:
    """待查可执行名(Windows 带 .exe)。"""
    plat = (sys_platform or "").lower()
    if plat.startswith("win") or plat == "cygwin":
        return DEFAULT_QEMU_EXE + ".exe"
    return DEFAULT_QEMU_EXE


def windows_qemu_search_dirs(environ=None) -> list:
    """Windows 上 QEMU 的常见安装目录(从环境变量派生;纯函数,不做 IO)。

    覆盖官方安装器/winget 默认(Program Files\\qemu)、32 位、便携常见位置。
    坏输入 fail-soft 返回 []。
    """
    try:
        e = os.environ if environ is None else environ
        dirs = []

        def push(base, *sub):
            if base:
                dirs.append(os.path.join(str(base), *sub))

        # ProgramW6432 在 32 位进程里也指向真正的 64 位 Program Files。
        push(e.get("ProgramW6432"), "qemu")
        push(e.get("ProgramFiles"), "qemu")
        push(e.get("ProgramFiles(x86)"), "qemu")
        push(e.get("LOCALAPPDATA"), "Programs", "qemu")
        sys_drive = str(e.get("SystemDrive") or "C:")
        drive_root = sys_drive if sys_drive.endswith("\\") else sys_drive + "\\"
        dirs.append(os.path.join(drive_root, "qemu"))
        dirs.append(os.path.join(drive_root, "Program Files", "qemu"))

        # 去重(保序)。
        seen = set()
        out = []
        for d in dirs:
            if d and d not in seen:
                seen.add(d)
                out.append(d)
        return out
    except Exception:  # noqa: BLE001 — 纯叶子 fail-soft
        return []


def unix_qemu_search_dirs() -> list:
    """非 Windows 的兜底目录(PATH 未覆盖时的常见安装位置)。"""
    return ["/opt/homebrew/bin", "/usr/local/bin", "/usr/bin", "/usr/local/qemu/bin"]


# ── 注入式 IO 壳(默认实参=真实现,单测传假) ─────────────────────────────────


def _winget_qemu_dirs(environ, listdir) -> list:
    """枚举 %LOCALAPPDATA%\\Microsoft\\WinGet\\Packages 下匹配 QEMU 前缀的子目录。

    注入 ``listdir``;fail-soft:无 LOCALAPPDATA / listdir 抛错 / 非序列 → []。
    """
    try:
        e = environ or {}
        local = e.get("LOCALAPPDATA")
        if not local or not callable(listdir):
            return []
        base = os.path.join(str(local), "Microsoft", "WinGet", "Packages")
        try:
            names = listdir(base)
        except Exception:  # noqa: BLE001 — 目录不存在等
            return []
        if not isinstance(names, (list, tuple)):
            return []
        return [
            os.path.join(base, n)
            for n in names
            if isinstance(n, str) and n.startswith(WINGET_QEMU_PREFIX)
        ]
    except Exception:  # noqa: BLE001
        return []


def locate_qemu(
    environ=None,
    *,
    sys_platform=None,
    exists=os.path.isfile,
    listdir=os.listdir,
) -> Optional[str]:
    """在 PATH 之外的常见位置定位已装的 QEMU 可执行文件。

    返回命中的绝对路径,或 ``None``(门控关 / 未命中 / 坏输入)。所有 IO(存在性
    检查、winget 目录枚举)依赖注入,便于单测。全程 fail-soft,绝不抛。
    """
    try:
        e = os.environ if environ is None else environ
        if not autolocate_enabled(e):
            return None
        plat = sys_platform if sys_platform is not None else sys.platform
        exe = qemu_exe_name(plat)
        p = (plat or "").lower()
        if p.startswith("win") or p == "cygwin":
            dirs = windows_qemu_search_dirs(e) + _winget_qemu_dirs(e, listdir)
        else:
            dirs = unix_qemu_search_dirs()
        for d in dirs:
            if not d:
                continue
            cand = os.path.join(d, exe)
            try:
                if exists(cand):
                    return cand
            except Exception:  # noqa: BLE001 — 单个候选检查抛错不影响其余
                continue
        return None
    except Exception:  # noqa: BLE001
        return None


def _apply_located_qemu(env, found) -> None:
    """把定位到的 QEMU 注入 ``env`` dict:设 ``KHY_QEMU`` + 把其目录前置进 ``PATH``。

    ``env`` 是 ``cli.py`` 传入并随后交给 Node 子进程的可变字典 —— 注入后 Node 侧
    ``KhyOsRunner`` 读 ``process.env.KHY_QEMU`` 即命中,整条 pip → Node 链一并修好。
    best-effort,绝不抛。
    """
    try:
        if env is None or not found:
            return
        env["KHY_QEMU"] = found
        d = os.path.dirname(found)
        if d:
            cur = env.get("PATH") or ""
            parts = cur.split(os.pathsep) if cur else []
            if d not in parts:
                env["PATH"] = d + (os.pathsep + cur if cur else "")
    except Exception:  # noqa: BLE001
        pass


def maybe_prompt_install_qemu(
    env=None,
    *,
    which=shutil.which,
    run=subprocess.run,
    ask=input,
    isatty=None,
    machine_is_root=None,
    out=print,
    exists=os.path.isfile,
    listdir=os.listdir,
) -> str:
    """`khy os` 启动前:检测 QEMU,缺失则询问并(经用户同意后)运行包管理器安装。

    返回 ``'present' | 'located' | 'installed' | 'declined' | 'skipped' | 'failed'``。
    全程失败软化:任何异常 → 打印一行提示并返回 ``'failed'``,绝不抛出。
    """
    try:
        e = os.environ if env is None else env
        exe = resolve_qemu_exe(e)

        # 1. 已在 PATH(或 KHY_QEMU 指向)→ 不打扰。
        if which(exe):
            return "present"

        # 1b. PATH 上没有,但系统可能装在 PATH 之外(Windows 安装器/winget →
        #     C:\\Program Files\\qemu)。自动定位并注入 env(设 KHY_QEMU + 把其目录
        #     前置进 PATH),使已装的 QEMU 无需手动配 PATH 即可用;env 随后传给 Node
        #     子进程,面板报错随之消失。门控 KHY_QEMU_AUTOLOCATE(默认开)。
        if autolocate_enabled(e):
            found = locate_qemu(e, sys_platform=sys.platform, exists=exists, listdir=listdir)
            if found:
                _apply_located_qemu(e, found)
                out(f"✓ 已在 {found} 定位到 QEMU 并自动配置(本次启动)")
                return "located"

        # 2. 门控关 → 逐字节回退:什么都不做。
        if not prompt_enabled(e):
            return "skipped"

        # 3. 非交互(无 TTY)→ 不弹窗阻塞自动化,只留一行诚实提示。
        _isatty = isatty
        if _isatty is None:
            try:
                _isatty = sys.stdin.isatty() and sys.stdout.isatty()
            except Exception:
                _isatty = False
        if not _isatty:
            out("未检测到 QEMU(运行 `khy os` 的前置依赖)。"
                "可设 KHY_QEMU 指向已装可执行文件,或参见 `khy os doctor`。")
            return "skipped"

        # 4. 询问。EOF/Ctrl-C → 当作否(抄 cli.py 模型选择守卫)。
        try:
            answer = ask("未检测到 QEMU(运行 `khy os` 的前置依赖)。是否现在安装? [y/N] ")
        except (EOFError, KeyboardInterrupt):
            out("")
            answer = ""
        if not _is_affirmative(answer):
            for line in install_hint_lines(sys.platform):
                out(line)
            return "declined"

        # 5. 答是 → 探测包管理器并选命令。
        available = [m for m in PROBE_MANAGERS if which(m)]
        is_root = machine_is_root
        if is_root is None:
            try:
                is_root = hasattr(os, "geteuid") and os.geteuid() == 0
            except Exception:
                is_root = False
        cmd = select_install_command(sys.platform, available, is_root=bool(is_root))
        if cmd is None:
            out("未找到受支持的包管理器(apt-get/dnf/pacman/zypper/brew/winget/choco)。")
            for line in install_hint_lines(sys.platform):
                out(line)
            return "failed"

        out(f"正在安装 QEMU: {cmd['display']}")
        out("(若提示输入密码,这是系统 sudo/UAC 在向你索权,khy 不会代为输入)")
        try:
            # 不捕获输出 → 终端透传,sudo/UAC 与包管理器进度可正常交互。
            run(cmd["argv"])
        except Exception as exc:  # noqa: BLE001 — 包管理器缺失/被拒等
            out(f"安装命令执行失败: {exc}")
            for line in install_hint_lines(sys.platform):
                out(line)
            return "failed"

        # 6. 复检:装后 PATH 是否已见 QEMU。
        if which(exe):
            out(f"✓ QEMU 已安装,可运行: khy os")
            return "installed"
        # 6b. winget/安装器常把 QEMU 装到 PATH 之外(C:\\Program Files\\qemu)。装后
        #     PATH 仍没有 → 再自动定位并注入 env,免去重开终端刷新 PATH / 手动配置。
        if autolocate_enabled(e):
            found = locate_qemu(e, sys_platform=sys.platform, exists=exists, listdir=listdir)
            if found:
                _apply_located_qemu(e, found)
                out(f"✓ QEMU 已安装并定位到: {found}(本次启动已自动配置)")
                return "installed"
        out("安装命令已结束,但 PATH 中仍未检测到 QEMU。可能需重开终端(刷新 PATH)或手动安装。")
        for line in install_hint_lines(sys.platform):
            out(line)
        return "failed"
    except Exception:  # noqa: BLE001 — 硬契约:绝不让前置助手阻断 khy os
        try:
            out("QEMU 前置检查跳过(内部错误,不影响 khy os 继续)。")
        except Exception:
            pass
        return "failed"
