"""MuraveyChina — Full auto-installer. Installs Python, VS Build Tools, all dependencies, models."""

import os
import sys
import time
import shutil
import hashlib
import platform
import subprocess
import urllib.request

# ─── Language ────────────────────────────────────────────────

LANG = "en"

S = {
    "en": {
        "choose_lang": "Choose language / Выберите язык:",
        "checking": "Checking system...",
        "ram": "RAM",
        "gpu": "GPU",
        "gpu_none": "Not detected (CPU mode)",
        "speed": "Speed",
        "fast": "Fast",
        "medium": "Medium",
        "slower": "Slower",
        "slowest": "Slowest",
        "press_enter": "Press Enter to start installation...",
        "installing_python": "Installing Python 3.11...",
        "python_ok": "Python 3.11 OK",
        "python_installing": "Downloading and installing Python 3.11...",
        "python_restart": "Python 3.11 installed! Please CLOSE this window and run INSTALL.bat again.",
        "installing_vs": "Checking Visual Studio Build Tools...",
        "vs_ok": "Visual Studio Build Tools found",
        "vs_installing": "Downloading Visual Studio Build Tools...",
        "vs_manual": "VS Build Tools download started. Install 'Desktop development with C++', then run INSTALL.bat again.",
        "installing_deps": "Installing dependencies...",
        "deps_ok": "Dependencies installed",
        "installing_ai": "Installing AI engine...",
        "ai_ok": "AI engine installed",
        "ai_fail": "AI engine failed — need VS Build Tools",
        "downloading_models": "Downloading AI models (~6.6 GB)...",
        "models_ok": "Models ready",
        "models_fail": "Some models failed to download",
        "creating_shortcut": "Creating desktop shortcut...",
        "shortcut_ok": "Shortcut created",
        "complete": "Installation Complete!",
        "machine_id": "Your Machine ID:",
        "activate_1": "1. Double-click start.pyw to launch",
        "activate_2": "2. Free until April 21, 2026",
        "activate_3": "3. Press Ctrl+` to get answers",
        "launch": "Buy: whop.com/muravey-ai",
        "id_copied": "Machine ID copied to clipboard!",
        "low_ram": "WARNING: Less than 8GB RAM. May be slow.",
        "no_gpu_warn": "No NVIDIA GPU detected.",
        "cpu_note1": "Answers will be slower and use compact prompts.",
        "cpu_note2": "Accuracy may be slightly lower than with GPU.",
        "with_gpu": "With NVIDIA GPU it would be faster and more accurate.",
        "attempt": "Attempt",
        "corrupted": "corrupted, re-downloading...",
        "failed_download": "FAILED: download failed after 3 attempts",
        "ok_size": "OK",
        "incomplete": "incomplete, deleting and re-downloading...",
    },
    "ru": {
        "choose_lang": "Choose language / Выберите язык:",
        "checking": "Проверка системы...",
        "ram": "ОЗУ",
        "gpu": "Видеокарта",
        "gpu_none": "Не обнаружена (режим CPU)",
        "speed": "Скорость",
        "fast": "Быстро",
        "medium": "Средне",
        "slower": "Медленнее",
        "slowest": "Самый медленный",
        "press_enter": "Нажмите Enter для начала установки...",
        "installing_python": "Установка Python 3.11...",
        "python_ok": "Python 3.11 OK",
        "python_installing": "Скачивание и установка Python 3.11...",
        "python_restart": "Python 3.11 установлен! ЗАКРОЙТЕ это окно и запустите INSTALL.bat заново.",
        "installing_vs": "Проверка Visual Studio Build Tools...",
        "vs_ok": "Visual Studio Build Tools найден",
        "vs_installing": "Скачивание Visual Studio Build Tools...",
        "vs_manual": "Загрузка VS Build Tools начата. Установите 'Desktop development with C++', затем запустите INSTALL.bat заново.",
        "installing_deps": "Установка зависимостей...",
        "deps_ok": "Зависимости установлены",
        "installing_ai": "Установка AI движка...",
        "ai_ok": "AI движок установлен",
        "ai_fail": "AI движок не установлен — нужен VS Build Tools",
        "downloading_models": "Скачивание AI моделей (~6.6 ГБ)...",
        "models_ok": "Модели готовы",
        "models_fail": "Некоторые модели не скачались",
        "creating_shortcut": "Создание ярлыка на рабочем столе...",
        "shortcut_ok": "Ярлык создан",
        "complete": "Установка завершена!",
        "machine_id": "Ваш Machine ID:",
        "activate_1": "1. Дважды кликните start.pyw для запуска",
        "activate_2": "2. Бесплатно до 21 апреля 2026",
        "activate_3": "3. Нажмите Ctrl+` для ответов",
        "launch": "Купить: whop.com/muravey-ai",
        "id_copied": "Machine ID скопирован в буфер обмена!",
        "low_ram": "ВНИМАНИЕ: Менее 8 ГБ ОЗУ. Может быть медленно.",
        "no_gpu_warn": "NVIDIA GPU не обнаружена.",
        "cpu_note1": "Ответы будут медленнее и используют короткие промпты.",
        "cpu_note2": "Точность может быть немного ниже чем с GPU.",
        "with_gpu": "С NVIDIA GPU будет быстрее и точнее.",
        "attempt": "Попытка",
        "corrupted": "повреждён, скачиваю заново...",
        "failed_download": "ОШИБКА: не удалось скачать после 3 попыток",
        "ok_size": "OK",
        "incomplete": "неполный, удаляю и скачиваю заново...",
    },
}


def t(key):
    return S.get(LANG, S["en"]).get(key, key)


def choose_language():
    global LANG
    print()
    print("  Choose language / Выберите язык:")
    print("  1. English")
    print("  2. Русский")
    print()
    choice = input("  > ").strip()
    LANG = "ru" if choice == "2" else "en"


# ─── Config ──────────────────────────────────────────────────

APP_DIR = os.path.dirname(os.path.abspath(__file__))
MODELS_DIR = os.path.expanduser("~/.muravey-china/models")
QWEN_VL_DIR = os.path.join(MODELS_DIR, "qwen25vl")

PYTHON_URL = "https://www.python.org/ftp/python/3.11.9/python-3.11.9-amd64.exe"
PYTHON_INSTALLER = os.path.join(APP_DIR, "python-3.11.9-amd64.exe")
VS_BUILDTOOLS_URL = "https://aka.ms/vs/17/release/vs_BuildTools.exe"
VS_INSTALLER = os.path.join(APP_DIR, "vs_BuildTools.exe")

# China mirror: hf-mirror.com (fast inside China, fallback to HuggingFace)
HF_MIRROR = "https://hf-mirror.com"
HF_ORIGIN = "https://huggingface.co"

MODELS = [
    {
        "name": "Qwen2.5-VL-3B (vision)",
        "filename": "Qwen2.5-VL-3B-Instruct-Q4_K_M.gguf",
        "dir": QWEN_VL_DIR,
        "urls": [
            f"{HF_MIRROR}/ggml-org/Qwen2.5-VL-3B-Instruct-GGUF/resolve/main/Qwen2.5-VL-3B-Instruct-Q4_K_M.gguf",
            f"{HF_ORIGIN}/ggml-org/Qwen2.5-VL-3B-Instruct-GGUF/resolve/main/Qwen2.5-VL-3B-Instruct-Q4_K_M.gguf",
        ],
        "size_mb": 1929,
    },
    {
        "name": "Qwen2.5-VL mmproj (vision encoder)",
        "filename": "mmproj-Qwen2.5-VL-3B-Instruct-f16.gguf",
        "dir": QWEN_VL_DIR,
        "urls": [
            f"{HF_MIRROR}/ggml-org/Qwen2.5-VL-3B-Instruct-GGUF/resolve/main/mmproj-Qwen2.5-VL-3B-Instruct-f16.gguf",
            f"{HF_ORIGIN}/ggml-org/Qwen2.5-VL-3B-Instruct-GGUF/resolve/main/mmproj-Qwen2.5-VL-3B-Instruct-f16.gguf",
        ],
        "size_mb": 1338,
    },
]

# Hybrid solver models: Phi-4-mini-Reasoning for math, Qwen3.5-4B for Chinese
SOLVER_PHI4 = {
    "name": "Phi-4-mini-Reasoning (math/physics/chemistry — MATH 94.6%)",
    "filename": "Phi-4-mini-reasoning-Q4_K_M.gguf",
    "dir": MODELS_DIR,
    "urls": [
        f"{HF_MIRROR}/unsloth/Phi-4-mini-reasoning-GGUF/resolve/main/Phi-4-mini-reasoning-Q4_K_M.gguf",
        f"{HF_ORIGIN}/unsloth/Phi-4-mini-reasoning-GGUF/resolve/main/Phi-4-mini-reasoning-Q4_K_M.gguf",
    ],
    "size_mb": 2490,
}

SOLVER_QWEN35 = {
    "name": "Qwen3.5-4B (Chinese — best CJK on 4GB VRAM)",
    "filename": "Qwen3.5-4B-Q4_K_M.gguf",
    "dir": MODELS_DIR,
    "urls": [
        f"{HF_MIRROR}/unsloth/Qwen3.5-4B-GGUF/resolve/main/Qwen3.5-4B-Q4_K_M.gguf",
        f"{HF_ORIGIN}/unsloth/Qwen3.5-4B-GGUF/resolve/main/Qwen3.5-4B-Q4_K_M.gguf",
    ],
    "size_mb": 2740,
}

# Legacy fallback solvers (kept for users who installed earlier)
SOLVER_QWEN3 = {
    "name": "Qwen3-8B (legacy fallback)",
    "filename": "Qwen3-8B-Q4_K_M.gguf",
    "dir": MODELS_DIR,
    "urls": [
        f"{HF_MIRROR}/Qwen/Qwen3-8B-GGUF/resolve/main/Qwen3-8B-Q4_K_M.gguf",
        f"{HF_ORIGIN}/Qwen/Qwen3-8B-GGUF/resolve/main/Qwen3-8B-Q4_K_M.gguf",
    ],
    "size_mb": 4915,
}

SOLVER_QWEN25 = {
    "name": "Qwen2.5-7B (legacy fallback)",
    "filename": "qwen2.5-7b-instruct-q3_k_m.gguf",
    "dir": MODELS_DIR,
    "urls": [
        f"{HF_MIRROR}/Qwen/Qwen2.5-7B-Instruct-GGUF/resolve/main/qwen2.5-7b-instruct-q3_k_m.gguf",
        f"{HF_ORIGIN}/Qwen/Qwen2.5-7B-Instruct-GGUF/resolve/main/qwen2.5-7b-instruct-q3_k_m.gguf",
    ],
    "size_mb": 3632,
}

# Pre-built llama-cpp-python wheel index (avoids VS Build Tools)
LLAMA_WHEEL_INDEX = "https://abetlen.github.io/llama-cpp-python/whl/cpu"
LLAMA_WHEEL_INDEX_CUDA = "https://abetlen.github.io/llama-cpp-python/whl/cu124"


# ─── Helpers ─────────────────────────────────────────────────

def clear():
    os.system("cls" if os.name == "nt" else "clear")


def run_cmd(cmd, timeout=300):
    try:
        result = subprocess.run(
            cmd, capture_output=True, text=True, timeout=timeout,
            shell=isinstance(cmd, str),
        )
        return result.returncode == 0, result.stdout.strip(), result.stderr.strip()
    except Exception as e:
        return False, "", str(e)


def download_file(url, dest, name="", size_mb=0):
    """Download file with progress bar."""
    os.makedirs(os.path.dirname(dest), exist_ok=True)
    tmp = dest + ".tmp"

    try:
        req = urllib.request.Request(url, headers={"User-Agent": "MuraveyChina/2.0"})
        with urllib.request.urlopen(req, timeout=60) as resp:
            total = int(resp.headers.get("Content-Length", 0)) or (size_mb * 1024 * 1024)
            downloaded = 0
            chunk_size = 1024 * 1024  # 1MB

            with open(tmp, "wb") as f:
                while True:
                    chunk = resp.read(chunk_size)
                    if not chunk:
                        break
                    f.write(chunk)
                    downloaded += len(chunk)
                    pct = int(downloaded / total * 100) if total else 0
                    mb_done = downloaded / (1024 * 1024)
                    mb_total = total / (1024 * 1024)
                    print(f"\r    {name}: {mb_done:.0f}/{mb_total:.0f} MB ({pct}%)", end="", flush=True)

        print()
        shutil.move(tmp, dest)
        return True
    except Exception as e:
        print(f"\n    ERROR: {e}")
        if os.path.exists(tmp):
            os.remove(tmp)
        return False


def pr(msg, status=""):
    """Print formatted step message."""
    if status:
        print(f"  [{status}] {msg}")
    else:
        print(f"  {msg}")


# ─── Step 1: Python 3.11 ────────────────────────────────────

def find_python311():
    """Find Python 3.11 on the system."""
    # Check common locations
    candidates = [
        r"C:\Python311\python.exe",
        r"C:\Program Files\Python311\python.exe",
        r"C:\Users\{}\AppData\Local\Programs\Python\Python311\python.exe".format(
            os.environ.get("USERNAME", "")
        ),
    ]
    # Also check PATH
    for p in candidates:
        if os.path.isfile(p):
            return p

    # Check if current python is 3.11
    if sys.version_info[:2] == (3, 11):
        return sys.executable

    # Search PATH for python3.11 or python with correct version
    for name in ["python3.11", "python3", "python"]:
        path = shutil.which(name)
        if path:
            ok, out, _ = run_cmd([path, "--version"])
            if ok and "3.11" in out:
                return path

    return None


def ensure_python():
    """Check for Python 3.11, install if missing."""
    py = find_python311()
    if py:
        ok, out, _ = run_cmd([py, "--version"])
        pr(f"{t('python_ok')}: {out}", "OK")
        return py

    # Python 3.11 not found — download and install
    pr(t("python_installing"))
    print()

    if not os.path.isfile(PYTHON_INSTALLER):
        print(f"    Downloading Python 3.11...")
        ok = download_file(PYTHON_URL, PYTHON_INSTALLER, "Python 3.11", 25)
        if not ok:
            pr("Failed to download Python 3.11", "FAIL")
            return None

    # Silent install: /quiet, add to PATH, install to C:\Python311
    print("    Installing Python 3.11 (this takes a minute)...")
    cmd = [
        PYTHON_INSTALLER,
        "/quiet",
        "InstallAllUsers=1",
        "PrependPath=1",
        "Include_test=0",
        "TargetDir=C:\\Python311",
    ]
    ok, out, err = run_cmd(cmd, timeout=300)

    # Check if it installed
    if os.path.isfile(r"C:\Python311\python.exe"):
        pr("Python 3.11 installed to C:\\Python311", "OK")
        # Clean up installer
        try:
            os.remove(PYTHON_INSTALLER)
        except Exception:
            pass
        print()
        pr(t("python_restart"), "!!")
        print()
        input("  Press Enter / Нажмите Enter...")
        sys.exit(0)
    else:
        pr("Python install may have failed. Check C:\\Python311\\", "FAIL")
        return None


# ─── Step 2: Visual Studio Build Tools ──────────────────────

def has_vs_buildtools():
    """Check if VS Build Tools or Visual Studio is installed."""
    if shutil.which("cl"):
        return True
    vs_paths = [
        r"C:\Program Files (x86)\Microsoft Visual Studio",
        r"C:\Program Files\Microsoft Visual Studio",
        r"C:\BuildTools",
    ]
    for p in vs_paths:
        if os.path.isdir(p):
            return True
    return False


def ensure_vs_buildtools():
    """Check for VS Build Tools, download if missing."""
    if has_vs_buildtools():
        pr(t("vs_ok"), "OK")
        return True

    pr(t("vs_installing"))
    print()

    if not os.path.isfile(VS_INSTALLER):
        print("    Downloading VS Build Tools...")
        ok = download_file(VS_BUILDTOOLS_URL, VS_INSTALLER, "VS Build Tools", 2)
        if not ok:
            pr("Failed to download VS Build Tools", "FAIL")
            return False

    # Launch installer — it's interactive, user must complete it
    print()
    print("  ╔══════════════════════════════════════════════╗")
    print("  ║  Visual Studio Build Tools installer will    ║")
    print("  ║  open now. You MUST:                         ║")
    print("  ║                                              ║")
    print("  ║  1. Select 'Desktop development with C++'   ║")
    print("  ║  2. Click 'Install'                         ║")
    print("  ║  3. Wait for it to finish                   ║")
    print("  ║  4. Come back here and press Enter           ║")
    print("  ╚══════════════════════════════════════════════╝")
    print()

    try:
        subprocess.Popen([VS_INSTALLER], shell=True)
    except Exception as e:
        pr(f"Failed to launch installer: {e}", "FAIL")
        return False

    input("  Press Enter after VS Build Tools is installed / Нажмите Enter после установки...")
    print()

    # Clean up
    try:
        os.remove(VS_INSTALLER)
    except Exception:
        pass

    if has_vs_buildtools():
        pr(t("vs_ok"), "OK")
        return True
    else:
        pr("VS Build Tools not detected. llama-cpp-python may fail to compile.", "WARN")
        return False


# ─── Step 3: Dependencies ───────────────────────────────────

def install_dependencies(python_exe):
    """Install all pip requirements including PyQt6 for Windows."""
    pr(t("installing_deps"))

    # Use requirements-win.txt on Windows (includes PyQt6, pywin32)
    if sys.platform == "win32":
        req_file = os.path.join(APP_DIR, "requirements-win.txt")
    else:
        req_file = os.path.join(APP_DIR, "requirements-mac.txt")

    if not os.path.exists(req_file):
        req_file = os.path.join(APP_DIR, "requirements.txt")

    if not os.path.exists(req_file):
        pr("No requirements file found", "FAIL")
        return False

    # Upgrade pip first
    run_cmd([python_exe, "-m", "pip", "install", "--upgrade", "pip", "-q"], timeout=120)

    # Install requirements
    ok, out, err = run_cmd(
        [python_exe, "-m", "pip", "install", "-r", req_file, "-q"],
        timeout=600,
    )
    if ok:
        pr(t("deps_ok"), "OK")
        return True

    # Retry without -q to see errors
    pr("Retrying with verbose output...", "...")
    ok2, out2, err2 = run_cmd(
        [python_exe, "-m", "pip", "install", "-r", req_file],
        timeout=600,
    )
    if ok2:
        pr(t("deps_ok"), "OK")
        return True

    pr(f"Dependencies failed: {err2[:200]}", "FAIL")
    return False


# ─── Step 4: llama-cpp-python ────────────────────────────────

def install_llamacpp(python_exe, vram_mb):
    """Install llama-cpp-python — tries pre-built wheel first, then source."""
    pr(t("installing_ai"))

    # Check if already installed
    ok, _, _ = run_cmd([python_exe, "-c",
                        "from llama_cpp import Llama; "
                        "from llama_cpp.llama_chat_format import Qwen25VLChatHandler; "
                        "print('OK')"])
    if ok:
        pr(t("ai_ok") + " (already installed)", "OK")
        return True

    # Strategy 1: Try pre-built wheel (NO VS Build Tools needed!)
    if vram_mb >= 4000:
        print("    Trying pre-built GPU wheel (no compilation needed)...")
        ok, _, _ = run_cmd(
            f'{python_exe} -m pip install llama-cpp-python==0.3.19 '
            f'--extra-index-url {LLAMA_WHEEL_INDEX_CUDA} --no-cache-dir',
            timeout=300,
        )
        if ok:
            pr(t("ai_ok") + " (GPU wheel)", "OK")
            return True
        print("    GPU wheel not available, trying CPU wheel...")

    print("    Trying pre-built CPU wheel (no compilation needed)...")
    ok, _, _ = run_cmd(
        f'{python_exe} -m pip install llama-cpp-python==0.3.19 '
        f'--extra-index-url {LLAMA_WHEEL_INDEX} --no-cache-dir',
        timeout=300,
    )
    if ok:
        pr(t("ai_ok") + " (CPU wheel)", "OK")
        return True

    # Strategy 2: Compile from source (needs VS Build Tools)
    print("    Pre-built wheel not available. Compiling from source...")

    if vram_mb >= 4000:
        print("    Compiling GPU version (needs VS Build Tools)...")
        env_cmd = 'set CMAKE_ARGS=-DGGML_CUDA=ON && '
        ok, _, _ = run_cmd(
            f'{env_cmd}{python_exe} -m pip install llama-cpp-python==0.3.19 --no-cache-dir',
            timeout=600,
        )
        if ok:
            pr(t("ai_ok") + " (GPU compiled)", "OK")
            return True
        print("    GPU compile failed, trying CPU...")

    if sys.platform == "win32":
        env_cmd = 'set CMAKE_ARGS=-DGGML_CUDA=OFF && '
    else:
        env_cmd = 'CMAKE_ARGS="-DGGML_CUDA=OFF" '

    ok, _, _ = run_cmd(
        f'{env_cmd}{python_exe} -m pip install llama-cpp-python==0.3.19 --no-cache-dir',
        timeout=600,
    )
    if ok:
        pr(t("ai_ok") + " (CPU compiled)", "OK")
        return True

    pr(t("ai_fail"), "FAIL")
    return False


# ─── Step 5: Download models ────────────────────────────────

def download_models(vram_mb=0):
    """Download AI models — hybrid: Phi-4 for math, Qwen3.5-4B for Chinese."""
    os.makedirs(MODELS_DIR, exist_ok=True)
    os.makedirs(QWEN_VL_DIR, exist_ok=True)

    # Hybrid approach: always download both Phi-4 (math) and Qwen3.5-4B (Chinese)
    # Total extra: ~5.2 GB — both fit on 4GB VRAM, no GPU overlap needed
    if LANG == "ru":
        pr("Скачиваем гибридные модели: Phi-4 (математика) + Qwen3.5-4B (китайский)")
    else:
        pr("Downloading hybrid models: Phi-4 (math) + Qwen3.5-4B (Chinese)")

    # Check which models already exist
    phi4_dest   = os.path.join(SOLVER_PHI4["dir"],   SOLVER_PHI4["filename"])
    qwen35_dest = os.path.join(SOLVER_QWEN35["dir"], SOLVER_QWEN35["filename"])
    has_phi4   = os.path.exists(phi4_dest)   and os.path.getsize(phi4_dest)   > 2000 * 1024 * 1024
    has_qwen35 = os.path.exists(qwen35_dest) and os.path.getsize(qwen35_dest) > 2000 * 1024 * 1024

    solvers = []
    if not has_phi4:
        solvers.append(SOLVER_PHI4)
    if not has_qwen35:
        solvers.append(SOLVER_QWEN35)

    # Combine base models + solvers to download
    all_models = MODELS + solvers

    all_ok = True
    for m in all_models:
        dest = os.path.join(m["dir"], m["filename"])

        # Check existing file
        if os.path.exists(dest):
            fsize_mb = os.path.getsize(dest) / (1024 * 1024)
            if fsize_mb >= m["size_mb"] * 0.95:
                print(f"    {m['name']}: {t('ok_size')} ({fsize_mb:.0f} MB)")
                continue
            else:
                print(f"    {m['name']}: {t('incomplete')} ({fsize_mb:.0f}/{m['size_mb']} MB)")
                os.remove(dest)

        # Clean leftover .tmp
        tmp = dest + ".tmp"
        if os.path.exists(tmp):
            os.remove(tmp)

        # Try each URL (China mirror first, then HuggingFace)
        ok = False
        for url_idx, url in enumerate(m["urls"]):
            source = "China mirror" if "hf-mirror" in url else "HuggingFace"
            if url_idx > 0:
                print(f"    Trying {source}...")

            for attempt in range(2):
                if attempt > 0:
                    print(f"    {t('attempt')} {attempt + 1}/2...")
                    time.sleep(3)

                ok = download_file(url, dest, m["name"], m["size_mb"])
                if ok and os.path.exists(dest):
                    fsize_mb = os.path.getsize(dest) / (1024 * 1024)
                    if fsize_mb >= m["size_mb"] * 0.95:
                        break
                    print(f"    {t('corrupted')} ({fsize_mb:.0f}/{m['size_mb']} MB)")
                    os.remove(dest)
                    ok = False

            if ok:
                break

        if not ok:
            all_ok = False
            print(f"    {t('failed_download')}: {m['name']}")

    return all_ok


# ─── Step 6: Shortcut ───────────────────────────────────────

def create_shortcut(python_exe):
    """Create desktop shortcut pointing to start.pyw (no console, no bat)."""
    if sys.platform != "win32":
        return True

    desktop = os.path.join(os.path.expanduser("~"), "Desktop")

    # Find pythonw.exe
    python_dir = os.path.dirname(python_exe)
    pythonw = os.path.join(python_dir, "pythonw.exe")
    if not os.path.isfile(pythonw):
        pythonw = python_exe

    # Target: pythonw.exe start.pyw (no console window at all)
    pyw_path = os.path.join(APP_DIR, "start.pyw")
    shortcut_path = os.path.join(desktop, "Muravey.lnk")
    icon_path = os.path.join(APP_DIR, "assets", "icon.ico")

    try:
        icon_arg = f'$s.IconLocation = "{icon_path}"' if os.path.isfile(icon_path) else ""
        ps_cmd = f'''
$WshShell = New-Object -ComObject WScript.Shell
$s = $WshShell.CreateShortcut("{shortcut_path}")
$s.TargetPath = "{pythonw}"
$s.Arguments = '"{pyw_path}"'
$s.WorkingDirectory = "{APP_DIR}"
$s.WindowStyle = 7
$s.Description = "Muravey AI"
{icon_arg}
$s.Save()
'''
        subprocess.run(["powershell", "-Command", ps_cmd], capture_output=True, timeout=15)
        if os.path.isfile(shortcut_path):
            pr("Shortcut created: Muravey.lnk", "OK")
            return True
    except Exception:
        pass

    # Fallback: copy start.pyw to desktop
    try:
        import shutil
        shutil.copy2(pyw_path, os.path.join(desktop, "Muravey.pyw"))
        pr("Copied start.pyw to Desktop", "OK")
        return True
    except Exception:
        return False


# ─── Helpers: system info ────────────────────────────────────

def check_gpu():
    try:
        result = subprocess.run(
            ["nvidia-smi", "--query-gpu=memory.total,name", "--format=csv,noheader,nounits"],
            capture_output=True, text=True, timeout=5,
        )
        if result.returncode == 0:
            line = result.stdout.strip().split("\n")[0]
            parts = line.split(",")
            vram = int(parts[0].strip())
            name = parts[1].strip() if len(parts) > 1 else "Unknown GPU"
            return vram, name
    except Exception:
        pass
    return 0, None


def check_ram():
    try:
        if sys.platform == "win32":
            import ctypes
            class MEMORYSTATUSEX(ctypes.Structure):
                _fields_ = [
                    ("dwLength", ctypes.c_ulong), ("dwMemoryLoad", ctypes.c_ulong),
                    ("ullTotalPhys", ctypes.c_ulonglong), ("ullAvailPhys", ctypes.c_ulonglong),
                    ("ullTotalPageFile", ctypes.c_ulonglong), ("ullAvailPageFile", ctypes.c_ulonglong),
                    ("ullTotalVirtual", ctypes.c_ulonglong), ("ullAvailVirtual", ctypes.c_ulonglong),
                    ("sullAvailExtendedVirtual", ctypes.c_ulonglong),
                ]
            stat = MEMORYSTATUSEX()
            stat.dwLength = ctypes.sizeof(stat)
            ctypes.windll.kernel32.GlobalMemoryStatusEx(ctypes.byref(stat))
            return stat.ullTotalPhys // (1024 * 1024)
        else:
            ok, out, _ = run_cmd(["sysctl", "-n", "hw.memsize"])
            if ok:
                return int(out) // (1024 * 1024)
    except Exception:
        pass
    return 0


def get_speed_label(vram_mb):
    if vram_mb >= 8000:
        return t("fast")
    elif vram_mb >= 4000:
        return t("medium")
    elif vram_mb >= 2000:
        return t("slower")
    else:
        return t("slowest")


def get_machine_id():
    try:
        sys.path.insert(0, APP_DIR)
        from src.license.machine_fingerprint import get_machine_id
        return get_machine_id()
    except Exception:
        pass
    parts = []
    if sys.platform == "win32":
        try:
            ok, out, _ = run_cmd('reg query "HKLM\\SOFTWARE\\Microsoft\\Cryptography" /v MachineGuid')
            if ok and "MachineGuid" in out:
                parts.append(out.split("MachineGuid")[-1].strip().split()[-1])
        except Exception:
            pass
    import uuid
    parts.append(str(uuid.getnode()))
    parts.append(platform.node())
    return hashlib.sha256(":".join(parts).encode()).hexdigest()[:32]


# ─── Main ────────────────────────────────────────────────────

def main():
    clear()
    choose_language()
    clear()

    print()
    print("  ╔══════════════════════════════════════════════╗")
    print("  ║        MuraveyChina — Installer              ║")
    print("  ╚══════════════════════════════════════════════╝")
    print()

    # ── System info ──
    pr(t("checking"))
    vram_mb, gpu_name = check_gpu()
    ram_mb = check_ram()
    speed = get_speed_label(vram_mb)

    print()
    print(f"  {t('ram')}: {ram_mb // 1024} GB")
    if gpu_name:
        print(f"  {t('gpu')}: {gpu_name} ({vram_mb} MB)")
    else:
        print(f"  {t('gpu')}: {t('gpu_none')}")
    print(f"  {t('speed')}: {speed}")
    print()

    if ram_mb > 0 and ram_mb < 7000:
        pr(t("low_ram"), "!!")
    if not gpu_name:
        pr(t("no_gpu_warn"), "!!")
        pr(t("cpu_note1"))
        pr(t("cpu_note2"))
        pr(t("with_gpu"))
    print()

    input(f"  {t('press_enter')}")
    print()

    # ── Step 1: Python 3.11 ──
    print("  [1/7] Python 3.11")
    python_exe = ensure_python()
    if not python_exe:
        print()
        print("  Cannot continue without Python 3.11.")
        input("  Press Enter to exit...")
        return
    print()

    # ── Step 2: Dependencies (including PyQt6) ──
    print("  [2/7] Dependencies")
    install_dependencies(python_exe)
    print()

    # ── Step 3: llama-cpp-python (tries wheel first, then source) ──
    print("  [3/7] AI Engine")
    ai_ok = install_llamacpp(python_exe, vram_mb)
    if not ai_ok:
        # Wheel failed — need VS Build Tools to compile from source
        print()
        print("  [3/7] AI wheel not available — need VS Build Tools to compile")
        ensure_vs_buildtools()
        print()
        print("  Retrying AI engine installation...")
        ai_ok = install_llamacpp(python_exe, vram_mb)
        if not ai_ok:
            print()
            print("  !! AI engine failed to install.")
            print("  !! Run INSTALL.bat again after VS Build Tools finishes.")
            print()
            input("  Press Enter to exit...")
            return
    print()

    # ── Step 4: Models ──
    # Check disk space first (need ~10 GB for models + temp)
    try:
        models_drive = os.path.splitdrive(MODELS_DIR)[0] or MODELS_DIR
        disk_free_gb = shutil.disk_usage(os.path.expanduser("~")).free / (1024**3)
        if disk_free_gb < 10:
            if LANG == "ru":
                pr(f"Мало места на диске: {disk_free_gb:.1f} ГБ свободно (нужно ~10 ГБ)", "!!")
            else:
                pr(f"Low disk space: {disk_free_gb:.1f} GB free (need ~10 GB)", "!!")
            print()
            input("  Press Enter to continue anyway / Нажмите Enter для продолжения...")
            print()
    except Exception:
        pass

    print(f"  [4/7] {t('downloading_models')}")
    models_ok = download_models(vram_mb=vram_mb)
    pr(t("models_ok") if models_ok else t("models_fail"), "OK" if models_ok else "!!")
    print()

    # ── Step 5: Shortcut ──
    print(f"  [5/7] {t('creating_shortcut')}")
    short_ok = create_shortcut(python_exe)
    pr(t("shortcut_ok") if short_ok else "Shortcut failed", "OK" if short_ok else "FAIL")
    print()

    # ── Step 6: Preload model (warm up, so first question is faster) ──
    if models_ok:
        if LANG == "ru":
            print("  [6/7] Предзагрузка AI модели (первый запуск будет быстрее)...")
        else:
            print("  [6/7] Preloading AI model (first launch will be faster)...")
        try:
            ok, _, _ = run_cmd(
                [python_exe, "-c",
                 "from llama_cpp import Llama; "
                 f"m = Llama(model_path=r'{os.path.join(MODELS_DIR, 'Qwen3-8B-Q4_K_M.gguf')}', "
                 "n_ctx=512, n_gpu_layers=0, verbose=False); "
                 "print('Model loaded OK')"],
                timeout=120,
            )
            pr("Model preloaded" if ok else "Preload skipped (not critical)", "OK" if ok else "SKIP")
        except Exception:
            pr("Preload skipped", "SKIP")
    print()

    # ── Step 7: Verify installation ──
    if LANG == "ru":
        print("  [7/7] Проверка установки...")
    else:
        print("  [7/7] Verifying installation...")
    checks = []
    # Check Python
    ok, out, _ = run_cmd([python_exe, "--version"])
    checks.append(("Python", ok))
    # Check llama_cpp
    ok, _, _ = run_cmd([python_exe, "-c", "from llama_cpp import Llama; print('OK')"])
    checks.append(("AI Engine", ok))
    # Check PyQt6
    ok, _, _ = run_cmd([python_exe, "-c", "from PyQt6.QtWidgets import QApplication; print('OK')"])
    checks.append(("PyQt6", ok))
    # Check models
    model_ok = all(
        os.path.exists(os.path.join(m["dir"], m["filename"]))
        for m in MODELS
    )
    checks.append(("AI Models", model_ok))

    all_ok = True
    for name, ok in checks:
        pr(f"{name}", "OK" if ok else "FAIL")
        if not ok:
            all_ok = False

    if not all_ok:
        print()
        if LANG == "ru":
            pr("Некоторые компоненты не установлены. Запустите INSTALL.bat ещё раз.", "!!")
        else:
            pr("Some components missing. Run INSTALL.bat again.", "!!")
    print()

    # ── Done ──
    machine_id = get_machine_id()
    print()
    print("  ╔══════════════════════════════════════════════╗")
    print(f"  ║     {t('complete'):<41}║")
    print("  ╠══════════════════════════════════════════════╣")
    print(f"  ║                                              ║")
    print(f"  ║  {t('speed')}: {speed:<35}║")
    print(f"  ║                                              ║")
    print(f"  ║  {t('machine_id'):<41}║")
    print(f"  ║  {machine_id}                ║")
    print(f"  ║                                              ║")
    print(f"  ║  {t('activate_1'):<41}║")
    print(f"  ║  {t('activate_2'):<41}║")
    print(f"  ║  {t('activate_3'):<41}║")
    print(f"  ║                                              ║")
    print(f"  ║  {t('launch'):<41}║")
    print(f"  ║                                              ║")
    print("  ╚══════════════════════════════════════════════╝")
    print()

    # Copy machine ID to clipboard
    try:
        if sys.platform == "win32":
            subprocess.run(["clip"], input=machine_id.encode(), check=True,
                           creationflags=subprocess.CREATE_NO_WINDOW)
        pr(t("id_copied"))
    except Exception:
        pass

    print()
    input("  Press Enter to close / Нажмите Enter для закрытия...")


if __name__ == "__main__":
    main()
