#!/usr/bin/env python3
"""
🧠 HiveMind — Geführte Installation
Funktioniert auf Linux und Windows.
"""

import os
import sys
import json
import shutil
import platform
import subprocess
import ssl
import urllib.request
import urllib.error
from pathlib import Path

# ─── Farben (Terminal) ──────────────────────────────────────────────
if platform.system() == "Windows":
    os.system("")  # Enable ANSI on Windows 10+

BOLD = "\033[1m"
DIM = "\033[2m"
GREEN = "\033[32m"
CYAN = "\033[36m"
YELLOW = "\033[33m"
RED = "\033[31m"
MAGENTA = "\033[35m"
RESET = "\033[0m"


# ─── Verfügbare Modelle ─────────────────────────────────────────────
# Schnell-Modell — wird automatisch bei jeder Installation heruntergeladen (kein Benutzereingriff)
FAST_MODEL = {
    "id": "qwen2.5-0.5b",
    "name": "Qwen 2.5 0.5B (Schnell-Modell)",
    "size": "0.4 GB",
    "url": "https://huggingface.co/bartowski/Qwen2.5-0.5B-Instruct-GGUF/resolve/main/Qwen2.5-0.5B-Instruct-Q4_K_M.gguf",
    "filename": "Qwen2.5-0.5B-Instruct-Q4_K_M.gguf",
}

# Hauptmodell-Auswahl (1.5B+). Das 0.5B-Schnell-Modell wird separat automatisch installiert.
MODELS = [
    {
        "id": "qwen2.5-1.5b",
        "name": "Qwen 2.5 1.5B (Klein)",
        "size": "1.0 GB",
        "ram": "2 GB",
        "speed": "⚡⚡⚡ Schnell",
        "quality": "★★☆☆☆ Alltägliche Aufgaben",
        "url": "https://huggingface.co/bartowski/Qwen2.5-1.5B-Instruct-GGUF/resolve/main/Qwen2.5-1.5B-Instruct-Q4_K_M.gguf",
        "filename": "Qwen2.5-1.5B-Instruct-Q4_K_M.gguf",
    },
    {
        "id": "qwen2.5-3b",
        "name": "Qwen 2.5 3B (Mittel)",
        "size": "1.8 GB",
        "ram": "4 GB",
        "speed": "⚡⚡ Mittel",
        "quality": "★★★☆☆ Gute Qualität",
        "url": "https://huggingface.co/bartowski/Qwen2.5-3B-Instruct-GGUF/resolve/main/Qwen2.5-3B-Instruct-Q4_K_M.gguf",
        "filename": "Qwen2.5-3B-Instruct-Q4_K_M.gguf",
    },
    {
        "id": "qwen2.5-7b",
        "name": "Qwen 2.5 7B (Groß)",
        "size": "4.4 GB",
        "ram": "8 GB",
        "speed": "⚡ Langsamer",
        "quality": "★★★★☆ Sehr gute Qualität",
        "url": "https://huggingface.co/bartowski/Qwen2.5-7B-Instruct-GGUF/resolve/main/Qwen2.5-7B-Instruct-Q4_K_M.gguf",
        "filename": "Qwen2.5-7B-Instruct-Q4_K_M.gguf",
    },
    {
        "id": "llama3.2-3b",
        "name": "Llama 3.2 3B (Meta)",
        "size": "2.0 GB",
        "ram": "4 GB",
        "speed": "⚡⚡ Mittel",
        "quality": "★★★☆☆ Gut für Englisch",
        "url": "https://huggingface.co/bartowski/Llama-3.2-3B-Instruct-GGUF/resolve/main/Llama-3.2-3B-Instruct-Q4_K_M.gguf",
        "filename": "Llama-3.2-3B-Instruct-Q4_K_M.gguf",
    },
    {
        "id": "none",
        "name": "Kein Modell (später manuell)",
        "size": "-",
        "ram": "-",
        "speed": "",
        "quality": "",
        "url": None,
        "filename": None,
    },
]

MIN_PYTHON = (3, 10)
MAX_PYTHON = (3, 13)  # 3.14+ not yet supported by llama-cpp-python/numpy

_MOLTBOOK_DEFAULTS = """
moltbook:
  api_key: ""
  agent_name: ""
  claim_url: ""
  assistant:
    enabled: false
    interval_minutes: 60
    upvote_posts: true
    follow_agents: false
    upvote_comments: false
    upvote_own_post_comments: true
    reply_own_post_comments: false
    comment_posts: false
"""



def _create_brain_icon(path: Path):
    """Erstelle ein 32x32 Brain-Icon als .ico (pure Python, keine Deps)."""
    import struct, zlib

    # 32x32 RGBA pixel data — purple brain on transparent background
    W = 32
    pixels = bytearray(W * W * 4)  # RGBA

    def put(x, y, r, g, b, a=255):
        if 0 <= x < W and 0 <= y < W:
            i = (y * W + x) * 4
            pixels[i:i+4] = bytes([r, g, b, a])

    def circle(cx, cy, rx, ry, r, g, b, a=255):
        for y in range(W):
            for x in range(W):
                dx = (x - cx) / max(rx, 0.1)
                dy = (y - cy) / max(ry, 0.1)
                if dx*dx + dy*dy <= 1.0:
                    put(x, y, r, g, b, a)

    def line_h(x1, x2, y, r, g, b, a=255):
        for x in range(x1, x2+1):
            put(x, y, r, g, b, a)

    # Brain shape — two hemispheres
    # Left hemisphere
    circle(12, 14, 9, 11, 168, 85, 247)   # main purple
    circle(10, 10, 6, 5, 147, 60, 230)    # upper left lobe
    circle(12, 19, 7, 5, 147, 60, 230)    # lower left lobe
    # Right hemisphere
    circle(20, 14, 9, 11, 168, 85, 247)
    circle(22, 10, 6, 5, 147, 60, 230)
    circle(20, 19, 7, 5, 147, 60, 230)

    # Center line (fissure)
    for y in range(5, 27):
        put(16, y, 90, 30, 160)
        put(15, y, 130, 50, 200, 128)
        put(17, y, 130, 50, 200, 128)

    # Sulci (grooves) — lighter lines
    for y in range(8, 22):
        if y % 3 == 0:
            line_h(8, 14, y, 190, 120, 255, 180)
            line_h(18, 24, y, 190, 120, 255, 180)

    # Highlight (shine)
    circle(11, 9, 3, 2, 210, 170, 255, 100)
    circle(21, 9, 3, 2, 210, 170, 255, 100)

    # Brain stem
    circle(16, 26, 3, 3, 130, 50, 200)
    circle(16, 28, 2, 2, 110, 40, 180)

    # Convert RGBA to BGRA for ICO format
    bgra = bytearray(len(pixels))
    for i in range(0, len(pixels), 4):
        bgra[i] = pixels[i+2]      # B
        bgra[i+1] = pixels[i+1]    # G
        bgra[i+2] = pixels[i]      # R
        bgra[i+3] = pixels[i+3]    # A

    # Build ICO file
    # ICO header: reserved(2) + type(2) + count(2)
    ico_header = struct.pack('<HHH', 0, 1, 1)
    # Image entry: w, h, colors, reserved, planes, bpp, size, offset
    bmp_header_size = 40
    pixel_data_size = W * W * 4
    image_size = bmp_header_size + pixel_data_size
    ico_entry = struct.pack('<BBBBHHII', W, W, 0, 0, 1, 32, image_size, 22)
    # BMP info header (BITMAPINFOHEADER)
    bmp_header = struct.pack('<IiiHHIIiiII',
        40,     # size
        W,      # width
        W * 2,  # height (doubled for ICO: image + mask)
        1,      # planes
        32,     # bpp
        0,      # compression
        pixel_data_size,
        0, 0, 0, 0
    )
    # ICO BMPs are stored bottom-up
    flipped = bytearray()
    for y in range(W - 1, -1, -1):
        row_start = y * W * 4
        flipped.extend(bgra[row_start:row_start + W * 4])

    path.write_bytes(ico_header + ico_entry + bmp_header + bytes(flipped))


def banner():
    print(f"""
{MAGENTA}{BOLD}╔══════════════════════════════════════════════╗
║          🧠 HiveMind — Installation          ║
║       Dezentrale P2P-AI für alle             ║
╚══════════════════════════════════════════════╝{RESET}
""")


def check_python():
    """Prüfe Python-Version."""
    v = sys.version_info
    print(f"  Python: {v.major}.{v.minor}.{v.micro}", end="")
    if v >= MIN_PYTHON and (v.major, v.minor) <= MAX_PYTHON:
        print(f"  {GREEN}✓{RESET}")
        return True
    elif (v.major, v.minor) > MAX_PYTHON:
        print(f"  {RED}✗ (Python {v.major}.{v.minor} zu neu! Max: {MAX_PYTHON[0]}.{MAX_PYTHON[1]}){RESET}")
        print(f"  {YELLOW}  llama-cpp-python/numpy unterstützen noch kein Python {v.major}.{v.minor}{RESET}")
        print(f"  {DIM}  Empfohlen: Python 3.12 von https://python.org/downloads/{RESET}")
        return False
    else:
        print(f"  {RED}✗ (mindestens {MIN_PYTHON[0]}.{MIN_PYTHON[1]} benötigt){RESET}")
        return False


def check_disk(install_dir: Path):
    """Prüfe verfügbaren Speicherplatz."""
    try:
        usage = shutil.disk_usage(install_dir.parent)
        free_gb = usage.free / (1024 ** 3)
        print(f"  Speicherplatz: {free_gb:.1f} GB frei", end="")
        if free_gb >= 2:
            print(f"  {GREEN}✓{RESET}")
        else:
            print(f"  {YELLOW}⚠ Wenig Platz{RESET}")
        return free_gb
    except Exception:
        print(f"  Speicherplatz: {DIM}nicht ermittelbar{RESET}")
        return 999


def check_ram():
    """Prüfe verfügbaren RAM."""
    try:
        if platform.system() == "Windows":
            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),
                    ("ullAvailExtendedVirtual", ctypes.c_ulonglong),
                ]
            stat = MEMORYSTATUSEX()
            stat.dwLength = ctypes.sizeof(stat)
            ctypes.windll.kernel32.GlobalMemoryStatusEx(ctypes.byref(stat))
            total_gb = stat.ullTotalPhys / (1024 ** 3)
        else:
            with open("/proc/meminfo") as f:
                for line in f:
                    if line.startswith("MemTotal:"):
                        total_gb = int(line.split()[1]) / (1024 ** 2)
                        break
        print(f"  RAM: {total_gb:.1f} GB", end="")
        print(f"  {GREEN}✓{RESET}" if total_gb >= 2 else f"  {YELLOW}⚠{RESET}")
        return total_gb
    except Exception:
        print(f"  RAM: {DIM}nicht ermittelbar{RESET}")
        return 16


def get_default_install_dir():
    """Standard-Installationsverzeichnis."""
    if platform.system() == "Windows":
        base = Path(os.environ.get("LOCALAPPDATA", Path.home() / "AppData" / "Local"))
        return base / "HiveMind"
    else:
        return Path.home() / ".hivemind"


def ask(prompt, default=None):
    """Frage mit optionalem Default."""
    if default:
        prompt = f"{prompt} [{default}]"
    try:
        answer = input(f"{CYAN}  → {prompt}: {RESET}").strip()
    except (EOFError, KeyboardInterrupt):
        print(f"\n{RED}  Abgebrochen.{RESET}")
        sys.exit(1)
    return answer if answer else default


def ask_choice(prompt, options, default=1):
    """Auswahl aus nummerierten Optionen."""
    print(f"\n{BOLD}  {prompt}{RESET}\n")
    for i, opt in enumerate(options, 1):
        marker = f"{GREEN}→{RESET}" if i == default else " "
        print(f"  {marker} {BOLD}{i}{RESET}) {opt}")
    while True:
        try:
            answer = input(f"\n{CYAN}  → Auswahl [1-{len(options)}, Standard={default}]: {RESET}").strip()
        except (EOFError, KeyboardInterrupt):
            print(f"\n{RED}  Abgebrochen.{RESET}")
            sys.exit(1)
        if not answer:
            return default - 1
        try:
            idx = int(answer) - 1
            if 0 <= idx < len(options):
                return idx
        except ValueError:
            pass
        print(f"  {RED}Ungültige Eingabe.{RESET}")


def download_file(url, dest_path, label=""):
    """Download mit Fortschrittsanzeige. Nutzt PowerShell auf Windows als Fallback."""
    print(f"\n  {BOLD}Downloading: {label}{RESET}")
    print(f"  {DIM}{url}{RESET}\n")

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

    # On Windows, prefer PowerShell (handles SSL/redirects via system certs)
    if platform.system() == "Windows":
        try:
            # Forward-Slashes vermeiden BackslashInterpretation in PS-Command-String
            ps_url = str(url).replace("'", "''")
            ps_out = str(dest_path).replace("\\", "/").replace("'", "''")
            # PowerShell script that streams download and reports progress to stdout
            ps_script = (
                "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; "
                f"$url = '{ps_url}'; "
                f"$out = '{ps_out}'; "
                "$req = [System.Net.HttpWebRequest]::Create($url); "
                "$req.UserAgent = 'HiveMind-Installer/1.0'; "
                "$req.AllowAutoRedirect = $true; "
                "$resp = $req.GetResponse(); "
                "$total = $resp.ContentLength; "
                "$stream = $resp.GetResponseStream(); "
                "$fs = [System.IO.File]::Create($out); "
                "$buf = New-Object byte[] 262144; "
                "$dl = 0; $lastPct = -1; "
                "while (($n = $stream.Read($buf, 0, $buf.Length)) -gt 0) { "
                "  $fs.Write($buf, 0, $n); $dl += $n; "
                "  if ($total -gt 0) { "
                "    $pct = [int]($dl * 100 / $total); "
                "    if ($pct -ne $lastPct) { "
                "      Write-Host \"PROGRESS:$pct:$dl:$total\"; $lastPct = $pct "
                "    } "
                "  } else { Write-Host \"PROGRESS:-1:$dl:0\" } "
                "} "
                "$fs.Close(); $stream.Close(); $resp.Close(); "
                "Write-Host 'DONE'"
            )
            print(f"  Lade herunter...")
            proc = subprocess.Popen(
                ["powershell", "-NoProfile", "-ExecutionPolicy", "Bypass", "-Command", ps_script],
                stdout=subprocess.PIPE, stderr=subprocess.PIPE
            )
            bar_chars = "=" * 40
            while True:
                raw = proc.stdout.readline()
                if not raw:
                    break
                line = raw.decode("utf-8", errors="replace").strip()
                if line.startswith("PROGRESS:"):
                    parts = line.split(":")
                    pct = int(parts[1])
                    dl = int(parts[2])
                    total = int(parts[3])
                    dl_mb = dl / (1024 * 1024)
                    if pct >= 0 and total > 0:
                        total_mb = total / (1024 * 1024)
                        filled = int(40 * pct // 100)
                        bar = "#" * filled + "-" * (40 - filled)
                        print(f"\r  [{bar}] {pct:3d}% ({dl_mb:.0f}/{total_mb:.0f} MB)", end="", flush=True)
                    else:
                        print(f"\r  {dl_mb:.0f} MB heruntergeladen...", end="", flush=True)
                elif line == "DONE":
                    break
            proc.wait(timeout=60)
            print()
            if dest_path.exists() and dest_path.stat().st_size > 1000:
                size_mb = dest_path.stat().st_size / (1024 * 1024)
                print(f"  {GREEN}✓ Download abgeschlossen ({size_mb:.0f} MB){RESET}")
                return True
            else:
                err_lines = proc.stderr.read().decode("utf-8", errors="replace").strip().split("\n") if proc.stderr else []
                err = next((l for l in err_lines if l.strip()), "Unbekannt")
                print(f"  {YELLOW}PowerShell-Download fehlgeschlagen: {err}{RESET}")
                if dest_path.exists():
                    dest_path.unlink()
        except Exception as e:
            print(f"  {YELLOW}PowerShell-Fehler: {e}{RESET}")

    # Fallback: urllib
    try:
        req = urllib.request.Request(url, headers={"User-Agent": "HiveMind-Installer/1.0"})
        try:
            response_ctx = urllib.request.urlopen(req, timeout=30)
        except urllib.error.URLError:
            ctx = ssl.create_default_context()
            ctx.check_hostname = False
            ctx.verify_mode = ssl.CERT_NONE
            opener = urllib.request.build_opener(urllib.request.HTTPSHandler(context=ctx))
            response_ctx = opener.open(req, timeout=30)
        with response_ctx as response:
            total = int(response.headers.get("Content-Length", 0))
            downloaded = 0
            chunk_size = 1024 * 256  # 256KB chunks

            dest_path.parent.mkdir(parents=True, exist_ok=True)
            with open(dest_path, "wb") as f:
                while True:
                    chunk = response.read(chunk_size)
                    if not chunk:
                        break
                    f.write(chunk)
                    downloaded += len(chunk)
                    if total > 0:
                        pct = downloaded / total * 100
                        bar_len = 40
                        filled = int(bar_len * downloaded // total)
                        bar = "#" * filled + "-" * (bar_len - filled)
                        dl_mb = downloaded / (1024 * 1024)
                        total_mb = total / (1024 * 1024)
                        print(f"\r  {bar} {pct:5.1f}% ({dl_mb:.0f}/{total_mb:.0f} MB)", end="", flush=True)
                    else:
                        dl_mb = downloaded / (1024 * 1024)
                        print(f"\r  {dl_mb:.0f} MB heruntergeladen...", end="", flush=True)
            print(f"\n  {GREEN}✓ Download abgeschlossen{RESET}")
            return True
    except (urllib.error.URLError, urllib.error.HTTPError, OSError) as e:
        print(f"\n  {RED}✗ Download fehlgeschlagen: {e}{RESET}")
        if dest_path.exists():
            dest_path.unlink()
        return False


def download_fast_model(install_dir: Path) -> bool:
    """Lade das Schnell-Modell (0.5B) herunter, falls noch nicht vorhanden."""
    model_path = install_dir / "models" / FAST_MODEL["filename"]
    if model_path.exists():
        size_mb = model_path.stat().st_size / (1024 * 1024)
        print(f"\n  {GREEN}✓ Schnell-Modell bereits vorhanden: {FAST_MODEL['filename']} ({size_mb:.0f} MB){RESET}")
        return True
    print(f"\n  {BOLD}Schnell-Modell (Qwen 2.5 0.5B) wird automatisch installiert...{RESET}")
    print(f"  {DIM}Wird als Vorantwort-KI fuer sofortige Erste-Antworten verwendet.{RESET}")
    return download_file(FAST_MODEL["url"], model_path, label=FAST_MODEL["name"])


def find_system_python():
    """Finde das echte System-Python (nicht Store-Stub/embedded).

    Microsoft-Store-Python (WindowsApps) ist ein App-Execution-Alias —
    kein echtes .exe. venv-Erstellung scheitert damit mit Permission Denied.
    """
    current = sys.executable
    current_lower = current.lower()

    # Pfade die NICHT für venv geeignet sind
    bad_markers = ["inkscape", "blender", "gimp", "embedded", "windowsapps"]

    def is_bad(path: str) -> bool:
        p = path.lower()
        return any(m in p for m in bad_markers)

    def version_ok(path: str) -> bool:
        """Prüft ob Python-Version in [MIN_PYTHON, MAX_PYTHON] liegt."""
        try:
            r = subprocess.run(
                [path, "-c",
                 "import sys; print(sys.version_info.major, sys.version_info.minor)"],
                capture_output=True, text=True, timeout=5,
            )
            if r.returncode == 0:
                maj, minor = map(int, r.stdout.strip().split())
                return (maj, minor) >= MIN_PYTHON and (maj, minor) <= MAX_PYTHON
        except Exception:
            pass
        return False

    # Aktuelles Python verwenden wenn es passt
    if not is_bad(current) and version_ok(current):
        return current

    if is_bad(current):
        reason = "Microsoft Store Python-Stub erkannt" if "windowsapps" in current_lower else "Eingebettetes Python erkannt"
    else:
        reason = "Python-Version außerhalb des unterstützten Bereichs"
    print(f"  {YELLOW}⚠ {reason} — suche passende Installation...{RESET}")
    print(f"  {DIM}  ({current}){RESET}")

    if platform.system() == "Windows":
        # Bekannte Installationspfade (python.org-Installer, NICHT Store)
        localapp = os.environ.get("LOCALAPPDATA", "")
        candidates = []
        for ver in ["312", "311", "313", "310"]:
            candidates += [
                Path(localapp) / "Programs" / "Python" / f"Python{ver}" / "python.exe",
                Path(f"C:/Program Files/Python{ver}/python.exe"),
                Path(f"C:/Python{ver}/python.exe"),
            ]
        for c in candidates:
            if c.exists() and not is_bad(str(c)) and version_ok(str(c)):
                print(f"  {GREEN}✓ Python gefunden: {c}{RESET}")
                return str(c)

        # py-Launcher mit expliziten Versionen probieren
        for pyver in ["-3.12", "-3.11", "-3.13", "-3.10"]:
            try:
                r = subprocess.run(
                    ["py", pyver, "-c", "import sys; print(sys.executable)"],
                    capture_output=True, text=True, timeout=10,
                )
                if r.returncode == 0:
                    found = r.stdout.strip()
                    if found and not is_bad(found) and version_ok(found):
                        print(f"  {GREEN}✓ Python {pyver} gefunden: {found}{RESET}")
                        return found
            except Exception:
                pass

        # Kein geeignetes Python — automatisch installieren
        found = _auto_install_python_windows(is_bad, version_ok)
        if found:
            return found

    # Kein Windows oder Auto-Install fehlgeschlagen
    print(f"""
  {RED}✗ Kein geeignetes Python gefunden!{RESET}

  {BOLD}Lösung:{RESET}
  1. Python 3.12 von {CYAN}https://python.org/downloads/{RESET} herunterladen
  2. Im Installer: {BOLD}"Add Python to PATH"{RESET} und {BOLD}"Install for all users"{RESET} ankreuzen
  3. {BOLD}NICHT{RESET} den Microsoft Store Python verwenden
     (Einstellungen → Apps → App-Ausführungsaliase → Python deaktivieren)
  4. Setup erneut starten
""")
    sys.exit(1)


def _auto_install_python_windows(is_bad, version_ok) -> str | None:
    """Lädt Python 3.12.7 herunter und installiert es still (Windows only).

    Nutzt den offiziellen python.org-Installer mit den Flags:
      /quiet InstallAllUsers=0 PrependPath=1 Include_pip=1 Include_test=0

    InstallAllUsers=0  → kein Admin nötig, Installation unter %LOCALAPPDATA%
    PrependPath=1      → fügt Python dem PATH hinzu (aber erst nach Neustart
                         in HKCU sichtbar — wir aktualisieren os.environ selbst)

    Gibt den Pfad zur frisch installierten python.exe zurück oder None.
    """
    PY_VERSION = "3.12.7"
    arch = platform.machine().lower()
    if arch in ("amd64", "x86_64"):
        url = f"https://www.python.org/ftp/python/{PY_VERSION}/python-{PY_VERSION}-amd64.exe"
        label = "Python 3.12.7 (64-bit)"
    elif arch == "arm64":
        url = f"https://www.python.org/ftp/python/{PY_VERSION}/python-{PY_VERSION}-arm64.exe"
        label = "Python 3.12.7 (ARM64)"
    else:
        url = f"https://www.python.org/ftp/python/{PY_VERSION}/python-{PY_VERSION}.exe"
        label = "Python 3.12.7 (32-bit)"

    print(f"\n  {YELLOW}⚠ Kein geeignetes Python gefunden.{RESET}")
    print(f"  {BOLD}Python wird jetzt automatisch installiert.{RESET}")
    print(f"  {DIM}Quelle: {url}{RESET}\n")

    answer = input(f"{CYAN}  → Jetzt herunterladen und installieren? (j/n) [j]: {RESET}").strip().lower()
    if answer not in ("", "j", "ja", "y", "yes"):
        return None

    # Download
    dl_path = Path(os.environ.get("TEMP", os.environ.get("TMP", "."))) / f"python-{PY_VERSION}-installer.exe"
    ok = download_file(url, dl_path, label=label)
    if not ok or not dl_path.exists():
        print(f"  {RED}✗ Download fehlgeschlagen.{RESET}")
        return None

    # Silent install (per-user, kein Admin erforderlich)
    print(f"\n  {BOLD}Installiere {label}...{RESET}")
    print(f"  {DIM}(Stilles Setup — kein Fenster erscheint, bitte warten){RESET}")
    result = subprocess.run(
        [
            str(dl_path),
            "/quiet",
            "InstallAllUsers=0",   # per-user → kein Admin nötig
            "PrependPath=1",       # PATH-Eintrag anlegen
            "Include_pip=1",
            "Include_test=0",      # Test-Suite weglassen (~50 MB sparen)
            "Include_doc=0",
            "Include_launcher=1",  # py.exe Launcher installieren
        ],
        timeout=300,
    )
    dl_path.unlink(missing_ok=True)

    if result.returncode not in (0, 3010):  # 3010 = Neustart empfohlen (aber ok)
        print(f"  {RED}✗ Python-Installation fehlgeschlagen (Code {result.returncode}).{RESET}")
        print(f"  {YELLOW}  Tipp: Als Administrator erneut versuchen, oder manuell von python.org installieren.{RESET}")
        return None

    print(f"  {GREEN}✓ {label} installiert!{RESET}")

    # PATH für den aktuellen Prozess aktualisieren
    # Der Installer schreibt nach HKCU\Environment — wir lesen es direkt aus der Registry
    new_python: str | None = None
    try:
        import winreg
        with winreg.OpenKey(winreg.HKEY_CURRENT_USER, "Environment") as key:
            user_path, _ = winreg.QueryValueEx(key, "PATH")
        # PATH-Variable im laufenden Prozess ergänzen
        os.environ["PATH"] = user_path + os.pathsep + os.environ.get("PATH", "")
    except Exception:
        # Fallback: typische Pfade manuell prüfen
        localapp = os.environ.get("LOCALAPPDATA", "")
        extra_paths = [
            str(Path(localapp) / "Programs" / "Python" / "Python312"),
            str(Path(localapp) / "Programs" / "Python" / "Python312" / "Scripts"),
        ]
        os.environ["PATH"] = os.pathsep.join(extra_paths) + os.pathsep + os.environ.get("PATH", "")

    # Frisch installierten Python-Pfad suchen
    localapp = os.environ.get("LOCALAPPDATA", "")
    candidates = [
        Path(localapp) / "Programs" / "Python" / "Python312" / "python.exe",
        Path(localapp) / "Programs" / "Python" / "Python312" / "python.exe",
    ]
    for c in candidates:
        if c.exists() and not is_bad(str(c)) and version_ok(str(c)):
            new_python = str(c)
            break

    # py-Launcher als zweiter Versuch
    if not new_python:
        try:
            r = subprocess.run(
                ["py", "-3.12", "-c", "import sys; print(sys.executable)"],
                capture_output=True, text=True, timeout=10,
            )
            if r.returncode == 0:
                found = r.stdout.strip()
                if found and not is_bad(found) and version_ok(found):
                    new_python = found
        except Exception:
            pass

    if new_python:
        print(f"  {GREEN}✓ Python bereit: {new_python}{RESET}")
        return new_python

    # Sollte nach erfolgreichem Install eigentlich nicht passieren
    print(f"  {YELLOW}⚠ Python installiert, aber Pfad nicht gefunden.{RESET}")
    print(f"  {DIM}  Bitte Setup einmal neu starten — dann wird Python erkannt.{RESET}")
    sys.exit(0)


# ─── GPU-Erkennung & llama-cpp-python Installation ──────────────────────────

def detect_gpu() -> dict:
    """Erkennt NVIDIA/AMD-GPU.
    Gibt zurück: {'vendor': 'nvidia'|'amd'|None, 'cuda_version': str|None, 'name': str|None}
    """
    import re as _re

    # 1. NVIDIA via nvidia-smi
    try:
        r = subprocess.run(
            ["nvidia-smi", "--query-gpu=name", "--format=csv,noheader"],
            capture_output=True, text=True, timeout=10,
        )
        if r.returncode == 0 and r.stdout.strip():
            gpu_name = r.stdout.strip().splitlines()[0].strip()
            r2 = subprocess.run(["nvidia-smi"], capture_output=True, text=True, timeout=10)
            cuda_ver = None
            if r2.returncode == 0:
                m = _re.search(r"CUDA Version:\s*([\d.]+)", r2.stdout)
                if m:
                    cuda_ver = m.group(1)
            return {"vendor": "nvidia", "cuda_version": cuda_ver, "name": gpu_name}
    except (FileNotFoundError, subprocess.TimeoutExpired, Exception):
        pass

    # 2. AMD via rocm-smi (Linux/ROCm)
    try:
        r = subprocess.run(
            ["rocm-smi", "--showdriverversion"],
            capture_output=True, text=True, timeout=10,
        )
        if r.returncode == 0:
            name = r.stdout.strip().splitlines()[0] if r.stdout.strip() else "AMD ROCm GPU"
            return {"vendor": "amd", "cuda_version": None, "name": name}
    except (FileNotFoundError, subprocess.TimeoutExpired, Exception):
        pass

    # 3. AMD via /sys/class/drm (Linux ohne ROCm-Tools)
    if platform.system() == "Linux":
        try:
            for vendor_file in Path("/sys/class/drm").glob("card*/device/vendor"):
                if vendor_file.read_text().strip() == "0x1002":
                    name_f = vendor_file.parent / "product_name"
                    name = name_f.read_text().strip() if name_f.exists() else "AMD GPU (sysfs)"
                    return {"vendor": "amd", "cuda_version": None, "name": name}
        except Exception:
            pass

    # 4. Windows WMI (AMD — NVIDIA wird durch nvidia-smi oben erfasst)
    if platform.system() == "Windows":
        try:
            r = subprocess.run(
                ["wmic", "path", "win32_VideoController", "get", "Name"],
                capture_output=True, text=True, timeout=15,
            )
            if r.returncode == 0:
                for line in r.stdout.splitlines():
                    line = line.strip()
                    if "amd" in line.lower() or "radeon" in line.lower():
                        return {"vendor": "amd", "cuda_version": None, "name": line}
        except (FileNotFoundError, subprocess.TimeoutExpired, Exception):
            pass

    return {"vendor": None, "cuda_version": None, "name": None}


def _cuda_wheel_tag(cuda_version: str) -> str:
    """Mappt CUDA-Treiberversion auf verfügbaren Prebuilt-Wheel-Tag (cu117–cu124).
    Hinweis: cu124 ist der höchste verfügbare Tag auf abetlen.github.io (Stand 2025).
    Auf Windows existieren dort keine Wheels — diese Funktion dient nur als Referenz.
    """
    try:
        parts = cuda_version.split(".")
        major = int(parts[0])
        minor = int(parts[1]) if len(parts) > 1 else 0
    except Exception:
        return "cu124"
    if major == 11:
        return "cu118" if minor >= 8 else "cu117"
    if major == 12:
        if minor <= 1:
            return "cu121"
        if minor == 2:
            return "cu122"
        if minor == 3:
            return "cu123"
        return "cu124"  # 12.4+ → cu124 ist der höchste verfügbare Tag
    return "cu124"  # Sicherer Default für unbekannte Versionen


def _install_llama_cpp(python_path) -> bool:
    """GPU-bewusste Installation von llama-cpp-python.
    Reihenfolge:
      NVIDIA Windows → CUDA-Kompilierung (erfordert CUDA Toolkit + VS BuildTools)
      NVIDIA Linux   → CUDA-Kompilierung
      AMD Linux      → ROCm/HIP-Kompilierung
      AMD Windows    → CPU-Modus (ROCm Windows nicht unterstützt)
      Kein GPU       → CPU-Modus
    Gibt True bei Erfolg zurück.
    """
    gpu = detect_gpu()
    vendor = gpu["vendor"]
    cuda_ver = gpu.get("cuda_version")
    gpu_name = gpu.get("name") or "unbekannt"

    def _run_pip(*args, timeout=1800, extra_env=None):
        env = os.environ.copy()
        if extra_env:
            env.update(extra_env)
        return subprocess.run(
            [str(python_path), "-m", "pip", "install", *args],
            capture_output=True, text=True, env=env, timeout=timeout,
        )

    def _show_tail(result):
        for line in (result.stderr or result.stdout or "").strip().split("\n")[-8:]:
            if line.strip():
                print(f"  {DIM}  {line}{RESET}")

    def _gpu_offload_active() -> bool:
        try:
            r = subprocess.run(
                [str(python_path), "-c",
                 "from llama_cpp import llama_cpp; print(llama_cpp.llama_supports_gpu_offload())"],
                capture_output=True, text=True, timeout=30,
            )
            return r.returncode == 0 and r.stdout.strip() == "True"
        except Exception:
            return False

    def _check_nvcc() -> str | None:
        """Gibt nvcc-Pfad zurück wenn CUDA Toolkit installiert ist, sonst None."""
        for cmd in ["nvcc", "nvcc.exe"]:
            try:
                r = subprocess.run([cmd, "--version"], capture_output=True,
                                   text=True, timeout=5)
                if r.returncode == 0:
                    return cmd
            except (FileNotFoundError, subprocess.TimeoutExpired):
                pass
        # Typische Windows-Installationspfade prüfen
        if platform.system() == "Windows":
            cuda_paths = list(Path("C:/Program Files/NVIDIA GPU Computing Toolkit/CUDA").glob("v*/bin/nvcc.exe"))
            if cuda_paths:
                return str(sorted(cuda_paths)[-1])  # neueste Version
        return None

    def _check_vs_buildtools() -> bool:
        """Prüft ob MSVC-Compiler (cl.exe) oder cmake+ninja verfügbar ist."""
        for cmd in ["cl.exe", "cl"]:
            try:
                r = subprocess.run([cmd], capture_output=True, text=True, timeout=5)
                if r.returncode in (0, 2):  # cl.exe gibt 2 ohne Argumente zurück
                    return True
            except (FileNotFoundError, subprocess.TimeoutExpired):
                pass
        # cmake + nmake/ninja (cmake kann selbst MSVC finden)
        try:
            r = subprocess.run(["cmake", "--version"], capture_output=True,
                               text=True, timeout=5)
            if r.returncode == 0:
                return True
        except (FileNotFoundError, subprocess.TimeoutExpired):
            pass
        return False

    def _ensure_cuda_runtime_windows(cuda_major: str):
        """Prüft ob CUDA Runtime + cuBLAS DLLs vorhanden sind und installiert sie ggf. nach.
        Die Runtime DLLs (cudart64_*.dll, cublas64_*.dll, cublasLt64_*.dll) werden für das
        CUDA-Wheel benötigt, sind aber NICHT im Wheel enthalten.
        Kleinstes Paket: 'nvidia-cuda-runtime-cu{major}' + 'nvidia-cublas-cu{major}' von PyPI.
        """
        import shutil as _shutil

        # DLL-Suffix je nach CUDA-Major
        dll_suffix_map = {"11": "110", "12": "12", "13": "13"}
        dll_suffix = dll_suffix_map.get(cuda_major, cuda_major)
        cudart_dll = f"cudart64_{dll_suffix}.dll"
        cublas_dll = f"cublas64_{dll_suffix}.dll"
        cublaslt_dll = f"cublasLt64_{dll_suffix}.dll"

        venv_sp = Path(python_path).parent.parent / "Lib" / "site-packages"
        llama_lib = venv_sp / "llama_cpp" / "lib"

        # CUDA Toolkit bin-Verzeichnis (als zusätzliche DLL-Quelle)
        cuda_toolkit_bin = None
        for p in sorted(Path("C:/Program Files/NVIDIA GPU Computing Toolkit/CUDA").glob("v*/bin"),
                        reverse=True):
            if (p / cudart_dll).exists():
                cuda_toolkit_bin = p
                break

        def _copy_dll_to_lib(src: Path) -> bool:
            if llama_lib.exists() and src.exists():
                try:
                    _shutil.copy2(str(src), str(llama_lib / src.name))
                    print(f"  {GREEN}  ✓ {src.name} → llama_cpp/lib{RESET}")
                    return True
                except Exception:
                    pass
            return False

        def _dll_already_present(name: str) -> bool:
            """Prüft ob DLL schon in llama_cpp/lib oder System32 vorhanden ist."""
            if llama_lib.exists() and (llama_lib / name).exists():
                return True
            if (Path("C:/Windows/System32") / name).exists():
                return True
            if cuda_toolkit_bin and (cuda_toolkit_bin / name).exists():
                return True
            return False

        missing_dlls = [d for d in [cudart_dll, cublas_dll, cublaslt_dll]
                        if not _dll_already_present(d)]
        if not missing_dlls:
            return  # alle DLLs bereits vorhanden

        print(f"\n  {YELLOW}⚠ CUDA DLLs fehlen in llama_cpp/lib: {', '.join(missing_dlls)}{RESET}")
        print(f"  {DIM}  Installiere nvidia-cuda-runtime-cu{cuda_major} + nvidia-cublas-cu{cuda_major}...{RESET}")

        # nvidia-cuda-runtime-cuXX existiert auf PyPI nur für cu11 und cu12.
        # Für cu13 (und spätere) gibt es keine pip-Pakete → cu12 als Fallback.
        pypi_major = cuda_major if cuda_major in ("11", "12") else "12"
        if pypi_major != cuda_major:
            print(f"  {DIM}  (nvidia-cuda-runtime-cu{cuda_major} nicht auf PyPI — "
                  f"versuche cu{pypi_major} als Fallback){RESET}")
        pkgs = [f"nvidia-cuda-runtime-cu{pypi_major}", f"nvidia-cublas-cu{pypi_major}"]
        for pkg in pkgs:
            r = _run_pip(pkg, "--quiet", timeout=300)
            if r.returncode != 0:
                print(f"  {YELLOW}  ⚠ {pkg} Installation fehlgeschlagen.{RESET}")

        # DLLs aus dem installierten Paket nach llama_cpp/lib kopieren
        # Für cu13 kommen die DLLs evtl. aus cu12-Paket (andere Suffix-Namen)
        pypi_suffix_map = {"11": "110", "12": "12"}
        pypi_dll_suffix = pypi_suffix_map.get(pypi_major, pypi_major)
        dll_patterns = [
            (f"nvidia/cuda_runtime/bin/{cudart_dll}",   cudart_dll,   f"cudart64_{pypi_dll_suffix}.dll"),
            (f"nvidia/cublas/bin/{cublas_dll}",         cublas_dll,   f"cublas64_{pypi_dll_suffix}.dll"),
            (f"nvidia/cublas/bin/{cublaslt_dll}",       cublaslt_dll, f"cublasLt64_{pypi_dll_suffix}.dll"),
        ]

        # Zusätzliche Suchpfade: CUDA-Toolkit alle Versionen + PATH + DriverStore
        _extra_search_dirs: list[Path] = []
        for _cu_dir in sorted(
            Path("C:/Program Files/NVIDIA GPU Computing Toolkit/CUDA").glob("v*/bin"),
            reverse=True,
        ):
            _extra_search_dirs.append(_cu_dir)
        for _path_entry in os.environ.get("PATH", "").split(os.pathsep):
            _p = Path(_path_entry)
            if _p.exists() and "nvidia" in str(_p).lower():
                _extra_search_dirs.append(_p)
        # Windows DriverStore (enthält manchmal cudart)
        _ds = Path("C:/Windows/System32/DriverStore/FileRepository")
        if _ds.exists():
            for _drv in list(_ds.glob("nv*"))[:20]:
                _extra_search_dirs.append(_drv)

        for _whl_path, dll_name, fallback_dll_name in dll_patterns:
            copied = False
            # 1. Aus pip-Paket mit exaktem DLL-Namen
            src = venv_sp / _whl_path
            if src.exists():
                _copy_dll_to_lib(src)
                copied = True
            # 2. Aus pip-Paket mit Fallback-DLL-Namen (cu12 für cu13-Anfrage)
            if not copied and fallback_dll_name != dll_name:
                fallback_src = venv_sp / _whl_path.replace(dll_name, fallback_dll_name)
                if fallback_src.exists():
                    # Kopieren aber unter dem richtigen Ziel-Namen ablegen
                    if llama_lib.exists() and fallback_src.exists():
                        import shutil as _sh2
                        try:
                            _sh2.copy2(str(fallback_src), str(llama_lib / dll_name))
                            print(f"  {GREEN}  ✓ {fallback_dll_name} → llama_cpp/lib/{dll_name} (Fallback){RESET}")
                            copied = True
                        except Exception:
                            pass
            # 3. Aus CUDA-Toolkit oder PATH
            if not copied:
                for _sdir in _extra_search_dirs:
                    if (_sdir / dll_name).exists():
                        _copy_dll_to_lib(_sdir / dll_name)
                        copied = True
                        break
            # 4. Breit im venv suchen (exakter Name)
            if not copied:
                for found in venv_sp.rglob(dll_name):
                    _copy_dll_to_lib(found)
                    copied = True
                    break
            if not copied:
                print(f"  {YELLOW}  ⚠ {dll_name} nicht gefunden — GPU evtl. nur im CPU-Modus{RESET}")

    def _check_cpu_has_avx512() -> bool:
        """Prüft ob die CPU AVX-512 unterstützt (CPUID).
        AMD Zen1/Zen2/Zen3-CPUs (Ryzen 3000/5000) ohne AVX-512 geben False zurück.
        Intel Skylake-X / Ice Lake / Zen4+ mit AVX-512 geben True zurück.
        """
        try:
            import cpuinfo  # type: ignore
            info = cpuinfo.get_cpu_info()
            flags = info.get("flags", [])
            return "avx512f" in flags
        except Exception:
            pass
        # Fallback: /proc/cpuinfo (Linux) oder Windows CPUID via ctypes
        if platform.system() == "Windows":
            try:
                import ctypes
                # CPUID leaf 7, ECX=0 → EBX bit 16 = AVX-512F
                class _CONTEXT(ctypes.Structure):
                    _fields_ = [("eax", ctypes.c_uint32), ("ebx", ctypes.c_uint32),
                                 ("ecx", ctypes.c_uint32), ("edx", ctypes.c_uint32)]
                # Nicht direkt via ctypes möglich ohne WinAPI Assembly-Trick
                # Stattdessen: Prozessorname analysieren
                import winreg
                key = winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE,
                    r"HARDWARE\DESCRIPTION\System\CentralProcessor\0")
                cpu_name = winreg.QueryValueEx(key, "ProcessorNameString")[0]
                winreg.CloseKey(key)
                cpu_name_lower = cpu_name.lower()
                # Bekannte CPUs OHNE AVX-512
                no_avx512_keywords = [
                    "ryzen 3", "ryzen 5", "ryzen 7", "ryzen 9",  # Zen2 (3xxx), Zen3 (5xxx)
                    "ryzen threadripper 3",                         # TRX40 (Zen2)
                    "athlon",
                ]
                # AMD Zen4+ (7xxx, 9xxx) HAT AVX-512
                has_avx512_keywords = ["ryzen 7000", "ryzen 9000", "ryzen threadripper 7"]
                for kw in has_avx512_keywords:
                    if kw in cpu_name_lower:
                        return True
                for kw in no_avx512_keywords:
                    if kw in cpu_name_lower:
                        return False
                # Intel: Skylake-X, Cascade Lake, Ice Lake, Tiger Lake, Rocket Lake+  haben AVX-512
                # Intel Core Ultra (Meteor Lake+) hat KEIN AVX-512
                # Im Zweifel: True annehmen (sicherer für Intel mainstream)
            except Exception:
                pass
        return True  # Im Zweifel annehmen dass AVX-512 vorhanden (kein False-Negative)

    def _copy_cuda_integration_to_vs(cuda_major_minor: str) -> bool:
        """Kopiert CUDA MSBuild Integration in alle gefundenen VS-Instanzen.
        Notwendig wenn CUDA Toolkit VOR Visual Studio installiert wurde oder
        wenn der CUDA-Installer die VS-Integration ausgelassen hat.
        """
        cuda_integ_src = Path(
            f"C:/Program Files/NVIDIA GPU Computing Toolkit/CUDA/v{cuda_major_minor}"
            "/extras/visual_studio_integration/MSBuildExtensions"
        )
        if not cuda_integ_src.exists():
            return False  # Keine Integration-Dateien vorhanden

        # Alle VS-Instanzen via vswhere ermitteln
        vswhere = Path("C:/Program Files (x86)/Microsoft Visual Studio/Installer/vswhere.exe")
        vs_dirs = []
        if vswhere.exists():
            try:
                r = subprocess.run(
                    [str(vswhere), "-all", "-property", "installationPath"],
                    capture_output=True, text=True, timeout=10
                )
                for line in r.stdout.strip().splitlines():
                    if line.strip():
                        vs_dirs.append(line.strip())
            except Exception:
                pass
        # Fallback: bekannte Pfade
        for fallback in [
            "C:/Program Files/Microsoft Visual Studio/2022/Community",
            "C:/Program Files/Microsoft Visual Studio/2022/BuildTools",
            "C:/Program Files (x86)/Microsoft Visual Studio/2019/Community",
            "C:/Program Files (x86)/Microsoft Visual Studio/2019/BuildTools",
        ]:
            if Path(fallback).exists() and fallback not in vs_dirs:
                vs_dirs.append(fallback)

        copied_any = False
        for vs_dir in vs_dirs:
            for vc_subdir in ["MSBuild/Microsoft/VC/v170", "MSBuild/Microsoft/VC/v160"]:
                bc_dir = Path(vs_dir) / vc_subdir / "BuildCustomizations"
                if not bc_dir.exists():
                    continue
                # Prüfen ob Integration schon vorhanden
                cuda_props = list(bc_dir.glob("CUDA*.props"))
                if cuda_props:
                    continue  # schon vorhanden
                # Kopieren (braucht ggf. Admin-Rechte)
                try:
                    for src_file in cuda_integ_src.iterdir():
                        shutil.copy2(str(src_file), str(bc_dir / src_file.name))
                    print(f"  {GREEN}  ✓ CUDA VS Integration → {bc_dir}{RESET}")
                    copied_any = True
                except PermissionError:
                    # Ohne Admin: bat-Skript als Admin ausführen
                    bat = Path(os.environ.get("TEMP", ".")) / "copy_cuda_vs.bat"
                    lines = ["@echo off"]
                    for src_file in cuda_integ_src.iterdir():
                        lines.append(f'copy /Y "{src_file}" "{bc_dir / src_file.name}"')
                    bat.write_text("\n".join(lines))
                    try:
                        import subprocess as _sp
                        _sp.run(["cmd", "/c", f"start /wait runas /user:Administrator \"{bat}\""],
                                timeout=60)
                    except Exception:
                        # Letzter Versuch: Start-Process RunAs
                        _sp.run([
                            "powershell", "-Command",
                            f"Start-Process cmd -ArgumentList '/c \"{bat}\"' -Verb RunAs -Wait"
                        ], timeout=120)
                    copied_any = True
                    bat.unlink(missing_ok=True)
                except Exception:
                    pass
        return copied_any

    def _find_vcvarsall() -> str | None:
        """Sucht vcvarsall.bat in allen installierten VS-Versionen (2019, 2022)."""
        vswhere = Path("C:/Program Files (x86)/Microsoft Visual Studio/Installer/vswhere.exe")
        if vswhere.exists():
            try:
                r = subprocess.run(
                    [str(vswhere), "-all", "-find", "VC/Auxiliary/Build/vcvarsall.bat"],
                    capture_output=True, text=True, timeout=10
                )
                for line in r.stdout.strip().splitlines():
                    p = line.strip()
                    if p and Path(p).exists():
                        return p
            except Exception:
                pass
        # Fallback-Suche
        for candidate in [
            "C:/Program Files/Microsoft Visual Studio/2022/Community/VC/Auxiliary/Build/vcvarsall.bat",
            "C:/Program Files/Microsoft Visual Studio/2022/BuildTools/VC/Auxiliary/Build/vcvarsall.bat",
            "C:/Program Files (x86)/Microsoft Visual Studio/2019/Community/VC/Auxiliary/Build/vcvarsall.bat",
            "C:/Program Files (x86)/Microsoft Visual Studio/2019/BuildTools/VC/Auxiliary/Build/vcvarsall.bat",
        ]:
            if Path(candidate).exists():
                return candidate
        return None

    def _build_llama_from_source_windows(cuda_major_minor: str) -> bool:
        """Baut llama-cpp-python aus den Quellen mit CUDA-Support und AVX2 (kein AVX-512).
        Voraussetzungen: CUDA Toolkit + VS mit C++-Workload installiert.
        """
        vcvarsall = _find_vcvarsall()
        if not vcvarsall:
            return False

        # Sicherstellen: CUDA VS Integration vorhanden
        _copy_cuda_integration_to_vs(cuda_major_minor)

        # CMake 3.x bevorzugen (kompatibel mit CUDA 11/12, CMake 4.x hat Probleme)
        _run_pip("cmake<4.0", "--upgrade", "--quiet", timeout=120)

        # Build-Deps für --no-build-isolation
        _run_pip("scikit-build-core", "setuptools", "wheel", "ninja",
                 "pyproject_metadata", "pathspec", "--quiet", timeout=120)

        cuda_path = f"C:/Program Files/NVIDIA GPU Computing Toolkit/CUDA/v{cuda_major_minor}"
        arch = "86"  # Wird zur Laufzeit korrekt gesetzt wenn nvidia-smi verfügbar
        try:
            r = subprocess.run(
                ["nvidia-smi", "--query-gpu=compute_cap", "--format=csv,noheader,nounits"],
                capture_output=True, text=True, timeout=8
            )
            if r.returncode == 0:
                cap = r.stdout.strip().split("\n")[0].strip().replace(".", "")
                arch = cap
        except Exception:
            pass

        cmake_args = (
            f"-DGGML_CUDA=ON "
            f"-DCMAKE_CUDA_ARCHITECTURES={arch} "
            f"-DGGML_AVX512=OFF "
            f"-DGGML_AVX2=ON "
            f"-DGGML_NATIVE=OFF "
            f"-DGGML_CCACHE=OFF"
        )

        # Build-Skript als .bat schreiben (vcvarsall hebt PATH für MSVC an)
        bat = Path(os.environ.get("TEMP", ".")) / "hivemind_build_llama.bat"
        pip_exe = str(python_path).replace("python.exe", "pip.exe").replace(
            "python3.exe", "pip3.exe"
        )
        if not Path(pip_exe).exists():
            pip_exe = str(Path(python_path).parent / "pip.exe")

        bat.write_text(
            f'@echo off\r\n'
            f'call "{vcvarsall}" x64\r\n'
            f'set CUDA_PATH={cuda_path}\r\n'
            f'set CUDA_HOME={cuda_path}\r\n'
            f'set CMAKE_ARGS={cmake_args}\r\n'
            f'set FORCE_CMAKE=1\r\n'
            f'"{pip_exe}" install --force-reinstall --no-cache-dir --no-build-isolation '
            f'"llama-cpp-python>=0.3.0"\r\n'
            f'exit /b %ERRORLEVEL%\r\n',
            encoding="utf-8"
        )
        print(f"  {DIM}  vcvarsall: {vcvarsall}{RESET}")
        print(f"  {DIM}  CUDA: {cuda_path}{RESET}")
        print(f"  {DIM}  Architektur: sm{arch}, AVX2=ON, AVX512=OFF{RESET}")
        print(f"  {DIM}  Kompiliere... (kann 20–40 Min. dauern){RESET}")
        r = subprocess.run(
            ["cmd", "/c", str(bat)],
            timeout=3600
        )
        bat.unlink(missing_ok=True)
        if r.returncode == 0:
            print(f"  {GREEN}✓ llama-cpp-python (CUDA sm{arch}, AVX2) kompiliert und installiert!{RESET}")
            # CUDA Runtime DLLs nachinstallieren
            _ensure_cuda_runtime_windows(cuda_major_minor.split(".")[0])
            return True
        print(f"  {YELLOW}⚠ Source-Build fehlgeschlagen (Code {r.returncode}).{RESET}")
        return False

    def _dougeeai_wheel_windows() -> "bool | None":
        """Installiert vorgebautes Windows CUDA-Wheel von dougeeai/llama-cpp-python-wheels.
        Kein CUDA Toolkit, kein Visual Studio erforderlich — einfach herunterladen & installieren.
        Repo: https://github.com/dougeeai/llama-cpp-python-wheels
        Rückgabewerte: True = OK, False = kein Wheel gefunden, None = DLL-Fehler (→ CPU-Fallback)
        """
        import json as _json

        # 1. SM-Architektur + Treiberversion via nvidia-smi
        try:
            r = subprocess.run(
                ["nvidia-smi", "--query-gpu=compute_cap,driver_version",
                 "--format=csv,noheader,nounits"],
                capture_output=True, text=True, timeout=8,
            )
            if r.returncode != 0:
                return False
            first_line = r.stdout.strip().split("\n")[0]
            parts = [p.strip() for p in first_line.split(",")]
            cap_str = parts[0]              # e.g. "8.6"
            drv_str = parts[1] if len(parts) > 1 else "0.0"
        except Exception:
            return False

        sm_parts = cap_str.replace(" ", "").split(".")
        sm_num = f"{sm_parts[0]}{sm_parts[1]}"  # "86"
        arch_map = {"75": "turing", "86": "ampere", "89": "ada", "100": "blackwell"}
        arch_name = arch_map.get(sm_num)
        if not arch_name:
            print(f"  {DIM}  Kein vorgebautes Wheel für SM{sm_num} — überspringe.{RESET}")
            return False

        v = sys.version_info
        py_tag = f"cp{v.major}{v.minor}"   # "cp312"

        # 2. Alle Releases der GitHub API abrufen (40 reichen für alle Kombinationen)
        try:
            import ssl as _ssl
            # Windows-System-Python hat oft kein aktuelles CA-Bundle → unverified context
            _ctx = _ssl.create_default_context()
            try:
                _ctx.load_default_certs()
            except Exception:
                pass
            _ctx.check_hostname = False
            _ctx.verify_mode = _ssl.CERT_NONE
            api_url = ("https://api.github.com/repos/dougeeai/llama-cpp-python-wheels"
                       "/releases?per_page=40")
            req = urllib.request.Request(
                api_url, headers={"User-Agent": "HiveMind-Installer/1.0"})
            with urllib.request.urlopen(req, timeout=15, context=_ctx) as resp:
                releases = _json.loads(resp.read())
        except Exception as exc:
            print(f"  {DIM}  GitHub API nicht erreichbar ({exc}) — überspringe.{RESET}")
            return False

        # Mindest-Treiberversion pro CUDA-Major (für Kompatibilitätscheck)
        _cuda_min_driver = {"13": 580.0, "12": 525.0, "11": 450.0}
        try:
            drv_major_minor = float(".".join(drv_str.split(".")[:2]))
        except Exception:
            drv_major_minor = 999.0

        # 3. Passendes Asset finden:
        #    Name-Muster: llama_cpp_python-*+cuda*.sm{sm_num}.{arch}-{py_tag}-...-win_amd64.whl
        #    Bevorzuge höchste CUDA-Version die der Treiber unterstützt
        cuda_prio = {"13": 3, "12": 2, "11": 1}
        best = None  # (prio, download_url, whl_name, cuda_label, cuda_major)
        for release in releases:
            for asset in release.get("assets", []):
                name = asset.get("name", "")
                dl_url = asset.get("browser_download_url", "")
                if not name.endswith("-win_amd64.whl"):
                    continue
                if f".sm{sm_num}." not in name:
                    continue
                if arch_name not in name:
                    continue
                if f"-{py_tag}-{py_tag}-" not in name:
                    continue
                # CUDA-Version aus Name extrahieren z.B. "cuda12.1"
                try:
                    cuda_part = [p for p in name.split("+")[1].split(".") if "cuda" in p][0]
                    cuda_major = cuda_part.replace("cuda", "").split(".")[0]
                    prio = cuda_prio.get(cuda_major, 0)
                except Exception:
                    cuda_major = "0"
                    prio = 0
                # Treiber-Kompatibilität prüfen
                min_drv = _cuda_min_driver.get(cuda_major, 999.0)
                if drv_major_minor < min_drv:
                    continue  # Treiber zu alt für diese CUDA-Version
                if best is None or prio > best[0]:
                    try:
                        cuda_label = name.split("+")[1].split(".sm")[0].replace("cuda", "CUDA ")
                    except Exception:
                        cuda_label = "CUDA ?"
                    best = (prio, dl_url, name, cuda_label, cuda_major)

        if best is None:
            print(f"  {DIM}  Kein passendes Wheel für SM{sm_num}/{arch_name}/{py_tag} (Treiber {drv_str}) gefunden.{RESET}")
            return False

        _, dl_url, whl_name, cuda_label, cuda_major = best
        tmp = Path(os.environ.get("TEMP", ".")) / whl_name
        print(f"  {DIM}Quelle: github.com/dougeeai/llama-cpp-python-wheels"
              f" ({arch_name}, {cuda_label}){RESET}")
        ok = download_file(dl_url, tmp, label=f"llama-cpp-python ({arch_name}, {cuda_label})")
        if not ok:
            tmp.unlink(missing_ok=True)
            return False

        # Wheel ohne Deps installieren, dann Deps nachholen
        r = _run_pip(str(tmp), "--force-reinstall", "--no-cache-dir", "--no-deps", timeout=120)
        tmp.unlink(missing_ok=True)
        if r.returncode != 0:
            _show_tail(r)
            return False
        _run_pip("diskcache", "jinja2", "numpy", "typing-extensions", timeout=180)
        print(f"  {GREEN}✓ llama-cpp-python ({arch_name}, {cuda_label}) installiert!{RESET}")
        print(f"  {GREEN}  CUDA Toolkit und VS Build Tools waren nicht erforderlich.{RESET}")
        # CUDA Runtime DLLs prüfen und ggf. nachinstallieren
        _ensure_cuda_runtime_windows(cuda_major)
        # Verifizieren ob llama_cpp tatsächlich ladbar ist (DLL-Abhängigkeiten vollständig?)
        if not _verify_llama_import():
            print(f"  {YELLOW}⚠ CUDA-Wheel lädt nicht korrekt (fehlende DLL-Abhängigkeiten).{RESET}")
            print(f"  {DIM}  Falle auf CPU-Modus zurück...{RESET}")
            return None  # Signal: sofort CPU, nicht erst Kompilierung versuchen
        return True

    def _verify_llama_import() -> bool:
        """Prüft ob llama_cpp im aktuellen venv tatsächlich importierbar ist (keine DLL-Fehler)."""
        try:
            r = subprocess.run(
                [python_path, "-c", "from llama_cpp import Llama; print('ok')"],
                capture_output=True, text=True, timeout=30,
            )
            return r.returncode == 0 and "ok" in r.stdout
        except Exception:
            return False

    def _install_cpu() -> bool:
        print(f"\n  {BOLD}Installiere llama-cpp-python (CPU-Modus)...{RESET}")
        print(f"  {DIM}(Kann einige Minuten dauern — C++ wird kompiliert){RESET}")
        r = _run_pip(
            "llama-cpp-python>=0.3.0",
            "--force-reinstall", "--no-cache-dir",
            extra_env={"CMAKE_ARGS": "-DGGML_NATIVE=OFF"},
        )
        if r.returncode != 0:
            print(f"  {RED}✗ llama-cpp-python Installation fehlgeschlagen{RESET}")
            if platform.system() == "Windows":
                print(f"  {YELLOW}  → Visual Studio Build Tools installieren:{RESET}")
                print(f"  {DIM}    https://visualstudio.microsoft.com/visual-cpp-build-tools/{RESET}")
            else:
                print(f"  {YELLOW}  → sudo apt install build-essential cmake{RESET}")
            _show_tail(r)
            return False
        print(f"  {GREEN}✓ llama-cpp-python (CPU) installiert{RESET}")
        return True

    def _winget_available() -> bool:
        try:
            r = subprocess.run(["winget", "--version"], capture_output=True, text=True, timeout=5)
            return r.returncode == 0
        except (FileNotFoundError, subprocess.TimeoutExpired):
            return False

    def _do_install_vs_buildtools(winget: bool) -> bool:
        """Installiert VS Build Tools — erst per winget, dann Direktdownload."""
        if winget:
            print(f"  {DIM}  winget install Microsoft.VisualStudio.2022.BuildTools ...{RESET}")
            r = subprocess.run([
                "winget", "install",
                "--id", "Microsoft.VisualStudio.2022.BuildTools",
                "--silent", "--accept-package-agreements", "--accept-source-agreements",
                "--override",
                "--quiet --wait --norestart --add Microsoft.VisualStudio.Workload.VCTools --includeRecommended",
            ], timeout=2400)
            if r.returncode in (0, 3010):
                print(f"  {GREEN}  ✓ VS Build Tools installiert{RESET}")
                return True
            print(f"  {YELLOW}  winget fehlgeschlagen (Code {r.returncode}) — versuche Direktdownload...{RESET}")
        # Direktdownload — stabile Microsoft-URL
        url = "https://aka.ms/vs/17/release/vs_buildtools.exe"
        tmp = Path(os.environ.get("TEMP", ".")) / "vs_buildtools.exe"
        print(f"  {DIM}  Lade Visual Studio Build Tools herunter...{RESET}")
        ok = download_file(url, tmp, label="VS Build Tools 2022")
        if not ok:
            print(f"  {RED}  ✗ Download fehlgeschlagen.{RESET}")
            return False
        print(f"  {DIM}  Installiere (kann 10–20 Min. dauern)...{RESET}")
        r = subprocess.run([
            str(tmp), "--quiet", "--wait", "--norestart",
            "--add", "Microsoft.VisualStudio.Workload.VCTools",
            "--includeRecommended",
        ], timeout=2400)
        tmp.unlink(missing_ok=True)
        if r.returncode in (0, 3010):
            print(f"  {GREEN}  ✓ VS Build Tools installiert{RESET}")
            return True
        print(f"  {RED}  ✗ Installation fehlgeschlagen (Code {r.returncode}){RESET}")
        return False

    def _do_install_cuda(ver: str, winget: bool) -> bool:
        """Installiert CUDA Toolkit — per winget, dann direkte Download-Seite."""
        if winget:
            # Versuche zuerst versionsspezifisches Paket, dann generisch
            major = ver.split(".")[0]
            for pkg_id in [f"Nvidia.CUDA.{major}", "Nvidia.CUDA"]:
                print(f"  {DIM}  winget install {pkg_id} ...{RESET}")
                r = subprocess.run([
                    "winget", "install", "--id", pkg_id,
                    "--silent", "--accept-package-agreements", "--accept-source-agreements",
                ], timeout=3600)
                if r.returncode in (0, 3010):
                    print(f"  {GREEN}  ✓ CUDA Toolkit installiert{RESET}")
                    return True
            print(f"  {YELLOW}  winget fehlgeschlagen — versuche Netzwerk-Installer...{RESET}")
        # Netzwerk-Installer (klein, ~5 MB) — lädt Rest nach
        major_minor = ".".join(ver.split(".")[:2])
        # Bekannte Netzwerk-Installer-URL-Muster probieren
        for patch in ["0", "1", "2", "3"]:
            full_ver = f"{major_minor}.{patch}"
            url = (f"https://developer.download.nvidia.com/compute/cuda/{full_ver}/"
                   f"network_installers/cuda_{full_ver}_windows_network.exe")
            tmp = Path(os.environ.get("TEMP", ".")) / f"cuda_{full_ver}_network.exe"
            print(f"  {DIM}  Probiere CUDA {full_ver} Netzwerk-Installer...{RESET}")
            try:
                req = urllib.request.Request(url, method="HEAD",
                                             headers={"User-Agent": "HiveMind-Installer/1.0"})
                urllib.request.urlopen(req, timeout=8)
            except Exception:
                continue  # Version existiert nicht, nächste versuchen
            ok = download_file(url, tmp, label=f"CUDA Toolkit {full_ver} (Netzwerk)")
            if not ok:
                continue
            print(f"  {DIM}  Installiere CUDA Toolkit {full_ver} (Netzwerkinstall — NVCC + Compiler)...{RESET}")
            r = subprocess.run([
                str(tmp), "-s",
                "nvcc_12.*", "cuda_profiler_api_12.*", "cuda_cccl_12.*",
                "visual_studio_integration_12.*",
            ], timeout=3600)
            tmp.unlink(missing_ok=True)
            if r.returncode in (0, 2):  # 2 = bereits installiert / partial
                print(f"  {GREEN}  ✓ CUDA Toolkit {full_ver} installiert{RESET}")
                return True
            print(f"  {YELLOW}  Installer Code {r.returncode} — ggf. Neustart erforderlich{RESET}")
            return r.returncode in (0, 2, 3010)
        # Kein Installer gefunden → manuelle Anleitung
        print(f"  {YELLOW}  ✗ Automatischer Download fehlgeschlagen.{RESET}")
        print(f"  {DIM}  Bitte manuell installieren: https://developer.nvidia.com/cuda-downloads{RESET}")
        return False

    def _auto_install_prerequisites_windows(nvcc_path, vs_ok: bool) -> tuple:
        """Fragt Nutzer, ob fehlende Voraussetzungen automatisch installiert werden sollen."""
        needs_cuda = not nvcc_path
        needs_vs = not vs_ok
        missing = []
        if needs_cuda:
            missing.append(f"  • CUDA Toolkit {cuda_ver}  (stellt nvcc bereit)")
        if needs_vs:
            missing.append("  • Visual Studio Build Tools 2022  (C++-Compiler)")
        print(f"\n  {YELLOW}Folgende Voraussetzungen für GPU-Beschleunigung fehlen:{RESET}")
        for m in missing:
            print(f"  {DIM}{m}{RESET}")
        winget = _winget_available()
        method = "winget (Windows Package Manager)" if winget else "Direktdownload"
        print(f"\n  {BOLD}Jetzt automatisch installieren via {method}?{RESET}")
        print(f"  {DIM}(Gesamtgröße: {'~5 MB + Netzwerk' if winget else '~5 MB CUDA + ~1 GB VS'})"
              f"  [J/n] {RESET}", end="", flush=True)
        try:
            answer = input().strip().lower()
        except (EOFError, KeyboardInterrupt):
            answer = "n"
        if answer in ("n", "nein", "no"):
            return nvcc_path, vs_ok
        # VS Build Tools zuerst (wird für CUDA-Kompilierung benötigt)
        new_vs = vs_ok
        if needs_vs:
            print(f"\n  {BOLD}► Visual Studio Build Tools 2022:{RESET}")
            new_vs = _do_install_vs_buildtools(winget)
        new_nvcc = nvcc_path
        if needs_cuda:
            print(f"\n  {BOLD}► CUDA Toolkit {cuda_ver}:{RESET}")
            if _do_install_cuda(cuda_ver, winget):
                new_nvcc = _check_nvcc()  # Pfad nach Installation neu ermitteln
                if not new_nvcc:
                    print(f"  {YELLOW}  ⚠ nvcc noch nicht im PATH — ggf. Terminal/PC neustarten.{RESET}")
                    # Typischen Installationspfad direkt prüfen
                    major_minor = ".".join(cuda_ver.split(".")[:2])
                    guess = Path(f"C:/Program Files/NVIDIA GPU Computing Toolkit/CUDA/v{major_minor}/bin/nvcc.exe")
                    if guess.exists():
                        new_nvcc = str(guess)
                        print(f"  {GREEN}  ✓ nvcc gefunden: {new_nvcc}{RESET}")
        return new_nvcc, new_vs

    # ── NVIDIA ──────────────────────────────────────────────────────
    if vendor == "nvidia":
        if cuda_ver:
            print(f"\n  {BOLD}Installiere llama-cpp-python (NVIDIA CUDA {cuda_ver})...{RESET}")
            print(f"  {DIM}GPU: {gpu_name}{RESET}")
            if _gpu_offload_active():
                print(f"  {GREEN}✓ CUDA-Support bereits aktiv — kein Neuinstall nötig.{RESET}")
                return True
            if platform.system() == "Windows":
                # AVX-512-Prüfung: Vorgebaute Wheels (dougeeai) haben AVX-512 in ggml-cpu.dll
                # und crashen auf AMD Zen1/Zen2/Zen3 CPUs (Ryzen 1000-5000 ohne AVX-512)
                cpu_has_avx512 = _check_cpu_has_avx512()
                if not cpu_has_avx512:
                    print(f"  {DIM}CPU unterstützt kein AVX-512 — überspringe vorgebaute Wheels "
                          f"(würden crashen).{RESET}")
                    print(f"  {DIM}Verwende Source-Build mit AVX2-Optimierung...{RESET}")
                else:
                    # Versuch 1: Vorgebautes Wheel (dougeeai) — kein nvcc, kein VS Build Tools nötig
                    print(f"  {DIM}Suche vorgebautes CUDA-Wheel (kein Compiler nötig)...{RESET}")
                    _wheel_result = _dougeeai_wheel_windows()
                    if _wheel_result is True:
                        return True
                    elif _wheel_result is None:
                        # Wheel installiert aber DLL-Fehler → direkt CPU, keine Kompilierung
                        return _install_cpu()
                    print(f"  {YELLOW}⚠ Kein passendes vorgebautes Wheel gefunden — versuche Kompilierung.{RESET}")

                # Versuch 2: Aus Quellen kompilieren (erfordert nvcc + VS Build Tools)
                nvcc = _check_nvcc()
                has_vs = bool(_find_vcvarsall())
                # Fehlende Voraussetzungen → automatische Installation anbieten
                if not nvcc or not has_vs:
                    nvcc, has_vs = _auto_install_prerequisites_windows(nvcc, has_vs)
                if not nvcc:
                    print(f"\n  {YELLOW}⚠ CUDA Toolkit (nvcc) nicht verfügbar.{RESET}")
                    print(f"  {DIM}  → https://developer.nvidia.com/cuda-downloads{RESET}")
                    print(f"  {DIM}  Nach Installation Installer erneut ausführen.{RESET}")
                    return _install_cpu()
                if not has_vs:
                    print(f"\n  {YELLOW}⚠ VS C++-Compiler nicht verfügbar.{RESET}")
                    print(f"  {DIM}  → https://visualstudio.microsoft.com/visual-cpp-build-tools/{RESET}")
                    print(f"  {DIM}  Nach Installation Installer erneut ausführen.{RESET}")
                    return _install_cpu()
                # Baue aus Quellen mit AVX2 (kein AVX-512)
                cuda_major_minor = ".".join(cuda_ver.split(".")[:2])
                if _build_llama_from_source_windows(cuda_major_minor):
                    return True
                print(f"  {YELLOW}⚠ CUDA Source-Build fehlgeschlagen — CPU-Fallback.{RESET}")
            else:
                # Linux: normaler CUDA-Source-Build
                nvcc = _check_nvcc()
                has_vs = _check_vs_buildtools()
                if not nvcc:
                    print(f"\n  {YELLOW}⚠ CUDA Toolkit (nvcc) nicht verfügbar.{RESET}")
                    print(f"  {DIM}  → https://developer.nvidia.com/cuda-downloads{RESET}")
                    return _install_cpu()
                print(f"  {DIM}nvcc: {nvcc} | Kompiliere llama-cpp-python (CUDA)...{RESET}")
                extra = {"CMAKE_ARGS": "-DGGML_CUDA=on -DGGML_NATIVE=OFF", "FORCE_CMAKE": "1"}
                nvcc_dir = str(Path(nvcc).parent) if Path(nvcc).exists() else None
                if nvcc_dir:
                    extra["PATH"] = nvcc_dir + os.pathsep + os.environ.get("PATH", "")
                r = _run_pip(
                    "llama-cpp-python>=0.3.0",
                    "--force-reinstall", "--no-cache-dir",
                    extra_env=extra,
                    timeout=2400,
                )
                if r.returncode == 0:
                    print(f"  {GREEN}✓ llama-cpp-python (CUDA) kompiliert und installiert!{RESET}")
                    return True
                print(f"  {YELLOW}⚠ CUDA-Kompilierung fehlgeschlagen — CPU-Fallback.{RESET}")
                _show_tail(r)
        else:
            # CUDA-Version unbekannt — auf Windows trotzdem Wheel versuchen (wenn AVX-512 vorhanden)
            if platform.system() == "Windows":
                print(f"\n  {BOLD}NVIDIA GPU erkannt — versuche vorgebautes CUDA-Wheel...{RESET}")
                print(f"  {DIM}GPU: {gpu_name}{RESET}")
                if _gpu_offload_active():
                    print(f"  {GREEN}✓ CUDA-Support bereits aktiv.{RESET}")
                    return True
                if _check_cpu_has_avx512() and _dougeeai_wheel_windows():
                    return True
            print(f"\n  {YELLOW}⚠ NVIDIA GPU erkannt, aber CUDA-Version unbekannt — CPU-Modus.{RESET}")
        return _install_cpu()

    # ── AMD (ROCm — Linux) ───────────────────────────────────────────
    if vendor == "amd" and platform.system() == "Linux":
        print(f"\n  {BOLD}Installiere llama-cpp-python (AMD ROCm/HIP)...{RESET}")
        print(f"  {DIM}GPU: {gpu_name}{RESET}")
        print(f"  {DIM}(Kompilierung mit -DGGML_HIPBLAS=on — kann 5–15 Minuten dauern){RESET}")
        if _gpu_offload_active():
            print(f"  {GREEN}✓ ROCm-Support bereits aktiv — kein Neuinstall nötig.{RESET}")
            return True
        r = _run_pip(
            "llama-cpp-python>=0.3.0", "--no-cache-dir", "--force-reinstall",
            extra_env={"CMAKE_ARGS": "-DGGML_HIPBLAS=on -DGGML_NATIVE=OFF "
                                     "-DCMAKE_PREFIX_PATH=/opt/rocm",
                       "FORCE_CMAKE": "1"},
        )
        if r.returncode == 0:
            print(f"  {GREEN}✓ llama-cpp-python (ROCm/HIP) installiert — GPU-Offload aktiv!{RESET}")
            return True
        print(f"  {YELLOW}⚠ ROCm-Kompilierung fehlgeschlagen — CPU-Fallback.{RESET}")
        print(f"  {DIM}  (ROCm installiert? https://rocm.docs.amd.com/){RESET}")
        _show_tail(r)
        return _install_cpu()

    # ── AMD Windows ──────────────────────────────────────────────────
    if vendor == "amd" and platform.system() == "Windows":
        print(f"\n  {YELLOW}⚠ AMD GPU erkannt ({gpu_name}).{RESET}")
        print(f"  {DIM}  ROCm unter Windows ist eingeschränkt — CPU-Modus wird verwendet.{RESET}")
        print(f"  {DIM}  Für GPU-Beschleunigung: Linux + ROCm empfohlen.{RESET}")
        return _install_cpu()

    # ── Kein GPU ─────────────────────────────────────────────────────
    print(f"\n  {DIM}⚙ Keine GPU erkannt — CPU-Modus.{RESET}")
    return _install_cpu()


def create_venv(install_dir: Path, install_telegram: bool = False):
    """Erstelle Virtual Environment und installiere HiveMind."""
    venv_dir = install_dir / ".venv"
    python_cmd = find_system_python()

    print(f"\n  {BOLD}Erstelle Virtual Environment...{RESET}")
    print(f"  {DIM}Python: {python_cmd}{RESET}")

    def _try_venv(*extra_args):
        return subprocess.run(
            [python_cmd, "-m", "venv", *extra_args, str(venv_dir)],
            capture_output=True, text=True,
        )

    result = _try_venv()
    if result.returncode != 0:
        stderr = result.stderr or ""
        # Permission Denied — meist altes .venv einer laufenden HiveMind-Instanz
        if "permission denied" in stderr.lower() or "errno 13" in stderr.lower():
            print(f"  {YELLOW}⚠ Zugriff verweigert — wird HiveMind gerade ausgeführt?{RESET}")
            print(f"  {DIM}  Versuche altes .venv zu entfernen...{RESET}")
            try:
                shutil.rmtree(venv_dir, ignore_errors=True)
            except Exception:
                pass
            result = _try_venv("--copies")  # --copies vermeidet Symlink-Probleme
            if result.returncode != 0:
                print(f"  {RED}✗ Venv-Erstellung fehlgeschlagen.{RESET}")
                print(f"  {YELLOW}  Bitte HiveMind beenden, dann Setup erneut starten.{RESET}")
                return False
        else:
            print(f"  {RED}✗ venv-Erstellung fehlgeschlagen:{RESET}")
            print(f"  {DIM}{stderr.strip()}{RESET}")
            # Fallback: --without-pip
            print(f"  {YELLOW}Versuche Fallback (ohne pip)...{RESET}")
            result = _try_venv("--without-pip")
            if result.returncode != 0:
                return False

    # Determine paths
    if platform.system() == "Windows":
        pip_path = venv_dir / "Scripts" / "pip.exe"
        python_path = venv_dir / "Scripts" / "python.exe"
    else:
        pip_path = venv_dir / "bin" / "pip"
        python_path = venv_dir / "bin" / "python"

    # Check python exists in venv
    if not python_path.exists():
        print(f"  {RED}✗ Python nicht im venv gefunden: {python_path}{RESET}")
        return False

    # Ensure pip is available
    if not pip_path.exists():
        print(f"  {DIM}Installiere pip...{RESET}")
        result = subprocess.run(
            [str(python_path), "-m", "ensurepip", "--upgrade"],
            capture_output=True, text=True
        )
        if result.returncode != 0:
            # Last resort: get-pip.py
            print(f"  {YELLOW}ensurepip fehlgeschlagen, versuche get-pip.py...{RESET}")
            getpip = install_dir / "get-pip.py"
            try:
                getpip_url = "https://bootstrap.pypa.io/get-pip.py"
                try:
                    urllib.request.urlretrieve(getpip_url, str(getpip))
                except urllib.error.URLError:
                    ctx = ssl.create_default_context()
                    ctx.check_hostname = False
                    ctx.verify_mode = ssl.CERT_NONE
                    req = urllib.request.Request(getpip_url)
                    with urllib.request.urlopen(req, context=ctx) as resp:
                        getpip.write_bytes(resp.read())
                subprocess.run([str(python_path), str(getpip)],
                               capture_output=True, text=True)
                getpip.unlink(missing_ok=True)
            except Exception as e:
                print(f"  {RED}✗ pip-Installation fehlgeschlagen: {e}{RESET}")
                return False

    # Upgrade pip
    print(f"  {DIM}Aktualisiere pip...{RESET}")
    subprocess.run(
        [str(python_path), "-m", "pip", "install", "--upgrade", "pip"],
        capture_output=True, text=True
    )

    # GPU-Erkennung und llama-cpp-python Installation (NVIDIA/AMD/CPU)
    if not _install_llama_cpp(python_path):
        return False

    # Install remaining dependencies
    print(f"\n  {BOLD}Installiere weitere Abhängigkeiten...{RESET}")
    deps = [
        "fastapi>=0.115.0",
        "uvicorn>=0.34.0",
        "numpy>=1.26.0",
        "pyyaml>=6.0",
        "rich>=13.0",
        "prompt-toolkit>=3.0",
        "httpx>=0.27.0",
        "beautifulsoup4>=4.12.0",
    ]
    result = subprocess.run(
        [str(python_path), "-m", "pip", "install"] + deps,
        capture_output=True, text=True, timeout=300
    )
    if result.returncode != 0:
        print(f"  {RED}✗ Abhängigkeiten fehlgeschlagen{RESET}")
        return False
    print(f"  {GREEN}✓ Alle Abhängigkeiten installiert{RESET}")

    # Optional: Telegram
    if install_telegram:
        print(f"\n  {BOLD}Installiere Telegram-Bot...{RESET}")
        result = subprocess.run(
            [str(python_path), "-m", "pip", "install", "python-telegram-bot>=21.0"],
            capture_output=True, text=True, timeout=180
        )
        if result.returncode == 0:
            print(f"  {GREEN}✓ python-telegram-bot installiert{RESET}")
        else:
            print(f"  {YELLOW}⚠ Telegram-Paket fehlgeschlagen — nachträglich installierbar mit:{RESET}")
            print(f"  {DIM}  pip install python-telegram-bot>=21.0{RESET}")

    # Install hivemind itself
    print(f"\n  {BOLD}Installiere HiveMind...{RESET}")
    result = subprocess.run(
        [str(python_path), "-m", "pip", "install", "-e", str(install_dir)],
        capture_output=True, text=True, timeout=120
    )
    if result.returncode != 0:
        print(f"  {YELLOW}⚠ Editable install fehlgeschlagen, versuche normalen Install...{RESET}")
        result = subprocess.run(
            [str(python_path), "-m", "pip", "install", str(install_dir)],
            capture_output=True, text=True, timeout=120
        )
    if result.returncode != 0:
        print(f"  {RED}✗ HiveMind Installation fehlgeschlagen{RESET}")
        return False

    print(f"  {GREEN}✓ HiveMind installiert{RESET}")
    return True


def write_config(install_dir: Path, model_filename: str | None, node_name: str,
                 existing: dict | None = None, install_telegram: bool | None = None):
    """Schreibe oder aktualisiere config.yaml.

    Strategie:
    - Existiert bereits eine config.yaml -> Backup, dann NUR geaenderte
      Felder patchen. Bestehende Einstellungen (Moltbook, Telegram, Plugins,
      Persona ...) werden NIEMALS ueberschrieben.
    - Existiert noch keine config.yaml -> frische Standardkonfiguration.
    """
    model_path = f"models/{model_filename}" if model_filename else ""
    config_path = install_dir / "config.yaml"

    # -- Backup der bestehenden Konfiguration ---------------------------------
    if config_path.exists():
        from datetime import datetime as _dt
        _ts = _dt.now().strftime("%Y%m%d_%H%M%S")
        backup_path = install_dir / f"config.yaml.bak_{_ts}"
        try:
            shutil.copy2(config_path, backup_path)
            all_baks = sorted(install_dir.glob("config.yaml.bak_*"),
                              key=lambda p: p.stat().st_mtime)
            for old in all_baks[:-5]:
                try:
                    old.unlink()
                except OSError:
                    pass
            print(f"  {DIM}Backup: {backup_path.name}{RESET}")
        except Exception as _e:
            print(f"  {YELLOW}Backup konnte nicht erstellt werden: {_e}{RESET}")

    # -- Update bestehende config.yaml (Einstellungen erhalten!) --------------
    if config_path.exists():
        _updated_via_yaml = False
        try:
            import yaml as _yaml
            import re as _re
            # Datei IMMER selbst neu lesen — unabhaengig vom uebergebenen existing-Dict.
            # So gehen keine Einstellungen verloren, selbst wenn existing nur Teilinfos hat.
            _raw = None
            for _enc in ("utf-8-sig", "utf-8", "cp1252", "latin-1"):
                try:
                    _raw = config_path.read_text(encoding=_enc)
                    break
                except (UnicodeDecodeError, Exception):
                    continue
            if _raw is None:
                _raw = config_path.read_bytes().decode("utf-8", errors="replace")
            full = _yaml.safe_load(_raw) or {}
            # Nur die Werte ueberschreiben, die sich durch das Update aendern
            full.setdefault("node", {})["name"] = node_name
            full.setdefault("model", {})["path"] = model_path
            full.setdefault("fast_model", {}).setdefault("path", "")
            full.setdefault("cache", {"enabled": True, "max_entries": 10000,
                                       "similarity_threshold": 0.92})
            full.setdefault("plugins", {"enabled": [
                "chat", "web_search", "pdf_export",
                "datetime_info", "weather", "news_feed", "link_finder",
            ], "directory": "./plugins"})
            node_sect = full["node"]
            node_sect.setdefault("type", "auto")
            node_sect.setdefault("specialization", "")
            node_sect.setdefault("persona", "")
            node_sect.setdefault("expertise_tags", [])
            gw = full.setdefault("gateway", {})
            # Sicherheitscheck: telegram könnte durch einen Fehler ein skalarer
            # Wert (True/False) sein statt ein Dict — in dem Fall zurücksetzen.
            if not isinstance(gw.get("telegram"), dict):
                gw["telegram"] = {}
            tg = gw["telegram"]
            tg.setdefault("enabled", False)
            tg.setdefault("token", "")
            tg.setdefault("allowed_users", [])
            gw.setdefault("api", {"enabled": True, "host": "0.0.0.0", "port": 8420})
            full.setdefault("network", {
                "enabled": True, "listen_port": 9420,
                "bootstrap_nodes": [], "relay_servers": ["https://hive.1seele.de"],
            })
            mb = full.setdefault("moltbook", {})
            mb.setdefault("api_key", "")
            mb.setdefault("agent_name", "")
            mb.setdefault("claim_url", "")
            asst = mb.setdefault("assistant", {})
            asst.setdefault("enabled", False)
            asst.setdefault("interval_minutes", 60)
            asst.setdefault("upvote_posts", True)
            asst.setdefault("follow_agents", False)
            asst.setdefault("upvote_comments", False)
            asst.setdefault("upvote_own_post_comments", True)
            asst.setdefault("reply_own_post_comments", False)
            asst.setdefault("comment_posts", False)
            inst = full.setdefault("installation", {})
            if install_telegram is not None:
                inst["telegram"] = install_telegram
            else:
                inst.setdefault("telegram", False)
            content = ("# HiveMind Node Configuration\n"
                       + _yaml.dump(full, allow_unicode=True,
                                    default_flow_style=False, sort_keys=False))
            config_path.write_text(content, encoding="utf-8")
            _updated_via_yaml = True
        except ImportError:
            pass  # kein yaml -> Regex-Fallback
        except Exception as _exc:
            print(f"  {YELLOW}yaml-Merge fehlgeschlagen ({_exc}) - nutze Regex-Patch.{RESET}")

        if not _updated_via_yaml:
            # Regex-Patch: NUR name, path und fehlende Abschnitte hinzufuegen.
            # Bestehende Werte (Moltbook, Telegram, Plugins ...) bleiben unveraendert!
            import re
            # Encoding-tolerantes Lesen
            text = None
            for _enc in ("utf-8-sig", "utf-8", "cp1252", "latin-1"):
                try:
                    text = config_path.read_text(encoding=_enc)
                    break
                except (UnicodeDecodeError, Exception):
                    continue
            if text is None:
                text = config_path.read_bytes().decode("utf-8", errors="replace")
            text = re.sub(r'(?m)^  name:.*$', f'  name: "{node_name}"', text, count=1)
            text = re.sub(r'(?m)^  path:.*$', f'  path: "{model_path}"', text, count=1)
            # Fehlende Abschnitte am Ende erganzen
            if "moltbook:" not in text:
                text = text.rstrip() + "\n" + _MOLTBOOK_DEFAULTS
            if "installation:" not in text:
                _tg = "true" if install_telegram else "false"
                text = text.rstrip() + f"\ninstallation:\n  telegram: {_tg}\n"
            elif install_telegram is not None:
                _tg = "true" if install_telegram else "false"
                # Nur innerhalb des installation:-Blocks patchen, NICHT gateway.telegram!
                text = re.sub(r'(?m)^(installation:\n)  telegram:.*$',
                               rf'\g<1>  telegram: {_tg}', text)
            if "persona:" not in text:
                text = re.sub(r'(?m)^(node:\n(?:  [^\n]+\n)*)',
                              r'\g<1>  persona: ""\n', text, count=1)
            config_path.write_text(text, encoding="utf-8")
        return

    # -- Erstinstallation: frische Standardkonfiguration ---------------------
    _tg_flag = "true" if install_telegram else "false"
    config = f"""# HiveMind Node Configuration
node:
  name: "{node_name}"
  type: auto
  specialization: ""
  persona: ""
  expertise_tags: []

model:
  path: "{model_path}"
  n_ctx: 4096
  n_gpu_layers: -1
  n_threads: 0

fast_model:
  path: ""
  n_ctx: 2048
  n_gpu_layers: -1
  n_threads: 0
  max_tokens: 256

cache:
  enabled: true
  max_entries: 10000
  similarity_threshold: 0.92

plugins:
  enabled:
    - chat
    - web_search
    - pdf_export
  directory: "./plugins"

gateway:
  telegram:
    enabled: false
    token: ""
    allowed_users: []
  api:
    enabled: true
    host: "0.0.0.0"
    port: 8420

network:
  enabled: true
  listen_port: 9420
  bootstrap_nodes: []
  relay_servers:
    - "https://hive.1seele.de"
""" + _MOLTBOOK_DEFAULTS + f"""
installation:
  telegram: {_tg_flag}
"""
    config_path.write_text(config, encoding="utf-8")


def create_launcher(install_dir: Path):
    """Erstelle Start-Skripte für das jeweilige OS."""
    if platform.system() == "Windows":
        # .bat Datei
        bat = install_dir / "HiveMind.bat"
        bat.write_text(f"""@echo off
chcp 65001 >nul 2>&1
title HiveMind
cd /d "{install_dir}"
echo.
echo  ====================================================
echo       HiveMind - Dezentrale P2P-AI
echo  ====================================================
echo.
echo  Starte HiveMind...
echo  Logs: {install_dir}\\logs\\
echo.
call .venv\\Scripts\\activate.bat
python -m hivemind.cli --daemon --verbose %*
if %errorlevel% neq 0 (
    echo.
    echo  [!] HiveMind wurde mit Fehlercode %errorlevel% beendet.
    echo  Pruefe Logs in: {install_dir}\\logs\\
    echo.
)
pause
""", encoding="utf-8")

        # Icon erstellen
        icon_path = install_dir / "hivemind.ico"
        if not icon_path.exists():
            _create_brain_icon(icon_path)

        # Desktop-Verknüpfung
        desktop = Path.home() / "Desktop" / "HiveMind.lnk"
        try:
            ico = str(icon_path).replace("'", "''")
            # Use PowerShell to create shortcut
            ps_cmd = f"""
$s = (New-Object -COM WScript.Shell).CreateShortcut('{desktop}')
$s.TargetPath = '{bat}'
$s.WorkingDirectory = '{install_dir}'
$s.Description = 'HiveMind - Dezentrale P2P-AI'
$s.IconLocation = '{ico},0'
$s.Save()
"""
            subprocess.run(["powershell", "-Command", ps_cmd],
                           capture_output=True, text=True)
            print(f"  {GREEN}✓ Desktop-Verknüpfung erstellt{RESET}")
        except Exception:
            print(f"  {YELLOW}⚠ Desktop-Verknüpfung konnte nicht erstellt werden{RESET}")
            print(f"  {DIM}Starte manuell: {bat}{RESET}")
    else:
        # Shell-Script
        sh = install_dir / "start.sh"
        sh.write_text(f"""#!/bin/bash
cd "{install_dir}"
source .venv/bin/activate
hivemind "$@"
""", encoding="utf-8")
        sh.chmod(0o755)

        # .desktop Datei
        desktop_dir = Path.home() / "Desktop"
        if not desktop_dir.exists():
            desktop_dir = Path.home() / "Schreibtisch"
        if desktop_dir.exists():
            desktop_file = desktop_dir / "HiveMind.desktop"
            desktop_file.write_text(f"""[Desktop Entry]
Version=1.0
Type=Application
Name=🧠 HiveMind
Comment=Dezentrale P2P-AI
Exec=bash -c 'cd "{install_dir}" && source .venv/bin/activate && hivemind'
Icon=utilities-terminal
Terminal=true
Categories=Development;AI;
""", encoding="utf-8")
            desktop_file.chmod(0o755)
            # Trust the desktop file
            subprocess.run(
                ["gio", "set", str(desktop_file), "metadata::trusted", "true"],
                capture_output=True, text=True
            )
            print(f"  {GREEN}✓ Desktop-Verknüpfung erstellt{RESET}")
        else:
            print(f"  {DIM}Starte mit: {sh}{RESET}")


def copy_source(install_dir: Path):
    """Kopiere HiveMind-Quellcode ins Installationsverzeichnis."""
    src_dir = Path(__file__).parent / "hivemind"
    dest_dir = install_dir / "hivemind"

    if src_dir.exists() and src_dir != dest_dir:
        if dest_dir.exists():
            shutil.rmtree(dest_dir)
        shutil.copytree(src_dir, dest_dir)

    # Copy pyproject.toml and other needed files
    # Hinweis: config.yaml wird von write_config() verwaltet und hier NICHT
    # kopiert, damit bestehende Nutzereinstellungen nie überschrieben werden.
    for fname in ["pyproject.toml", "README.md"]:
        src = Path(__file__).parent / fname
        if src.exists():
            dest = install_dir / fname
            if not dest.exists() or src != dest:
                shutil.copy2(src, dest)

    # Create directories
    (install_dir / "models").mkdir(exist_ok=True)
    (install_dir / "plugins").mkdir(exist_ok=True)
    (install_dir / "cache").mkdir(exist_ok=True)
    (install_dir / "logs").mkdir(exist_ok=True)


def _read_existing_cfg(install_dir: Path) -> dict:
    """Lese vorhandene config.yaml aus dem Installationsverzeichnis.

    Versucht zuerst pyyaml; fällt auf einfaches Regex-Parsing zurück wenn
    yaml im System-Python nicht installiert ist.
    Gibt leeres dict zurück wenn keine Datei existiert.
    """
    config_path = install_dir / "config.yaml"
    if not config_path.exists():
        return {}
    # Encoding-tolerantes Lesen (UTF-8 → CP1252-Fallback → Bytes mit Ersatz)
    raw = None
    for _enc in ("utf-8-sig", "utf-8", "cp1252", "latin-1"):
        try:
            raw = config_path.read_text(encoding=_enc)
            break
        except (UnicodeDecodeError, Exception):
            continue
    if raw is None:
        try:
            raw = config_path.read_bytes().decode("utf-8", errors="replace")
        except Exception:
            return {}
    # Versuch 1: pyyaml
    try:
        import yaml
        return yaml.safe_load(raw) or {}
    except ImportError:
        pass
    except Exception:
        pass
    # Versuch 2: Regex-Parsing der wichtigsten Felder
    import re
    cfg: dict = {}
    m = re.search(r'(?m)^  name:\s*["\']?([^"\' #\n]+?)["\']?\s*$', raw)
    if m:
        cfg.setdefault("node", {})["name"] = m.group(1).strip()
    m = re.search(r'(?m)^  path:\s*["\']?([^"\' #\n]+?)["\']?\s*$', raw)
    if m:
        cfg.setdefault("model", {})["path"] = m.group(1).strip()
    m = re.search(r'(?m)^    token:\s*["\']?([^"\' #\n]+?)["\']?\s*$', raw)
    if m and m.group(1).strip():
        cfg.setdefault("gateway", {}).setdefault("telegram", {})["token"] = m.group(1).strip()
    m = re.search(r'(?m)^    enabled:\s*(true|false)', raw)
    if m:
        cfg.setdefault("gateway", {}).setdefault("telegram", {})["enabled"] = m.group(1) == "true"
    au = re.search(r'(?m)^    allowed_users:\s*\[([^\]]*)\]', raw)
    if au:
        users = [u.strip().strip("'\"")
                 for u in au.group(1).split(",") if u.strip()]
        if users:
            cfg.setdefault("gateway", {}).setdefault("telegram", {})["allowed_users"] = users
    # Spezialisierung und Expertise-Tags beibehalten
    m = re.search(r'(?m)^  specialization:\s*["\']?([^"\' #\n]*)["\']?\s*$', raw)
    if m and m.group(1).strip():
        cfg.setdefault("node", {})["specialization"] = m.group(1).strip()
    tags_match = re.search(r'(?m)^  expertise_tags:\s*\[([^\]]*)\]', raw)
    if tags_match:
        tags = [t.strip().strip("'\"")
                for t in tags_match.group(1).split(",") if t.strip()]
        if tags:
            cfg.setdefault("node", {})["expertise_tags"] = tags
    # Persona (Charakterbeschreibung / System-Prompt)
    m = re.search(r'(?m)^  persona:\s*["\']?(.+?)["\']?\s*$', raw)
    if m and m.group(1).strip() and m.group(1).strip() not in ('""', "''"):
        cfg.setdefault("node", {})["persona"] = m.group(1).strip().strip("'\"")
    # Moltbook API-Key und Agent-Name
    m = re.search(r'(?ms)^moltbook:.*?api_key:\s*["\']?([^"\' #\n]+?)["\']?\s*$', raw)
    if m and m.group(1).strip():
        cfg.setdefault("moltbook", {})["api_key"] = m.group(1).strip()
    m = re.search(r'(?ms)^moltbook:.*?agent_name:\s*["\']?([^"\' #\n]+?)["\']?\s*$', raw)
    if m and m.group(1).strip():
        cfg.setdefault("moltbook", {})["agent_name"] = m.group(1).strip()
    # Installations-Flag (war Telegram-Paket installiert?)
    m = re.search(r'(?ms)^installation:.*?telegram:\s*(true|false)', raw)
    if m:
        cfg.setdefault("installation", {})["telegram"] = m.group(1) == "true"
    # Plugin-Liste
    pl_inline = re.search(r'(?m)^  enabled:\s*\[([^\]]*)\]', raw)
    if pl_inline:
        enabled = [p.strip().strip("\"' ") for p in pl_inline.group(1).split(",") if p.strip()]
        if enabled:
            cfg.setdefault("plugins", {})["enabled"] = enabled
    else:
        pl_block = re.search(r'(?ms)^plugins:.*?enabled:\n((?:    - [^\n]+\n?)+)', raw)
        if pl_block:
            enabled = [ln.strip().lstrip("- ").strip()
                       for ln in pl_block.group(1).splitlines() if ln.strip().startswith("-")]
            if enabled:
                cfg.setdefault("plugins", {})["enabled"] = enabled
    return cfg


def main():
    banner()

    # ─── Schritt 1: Systemcheck ──────────────────────────────────────
    print(f"{BOLD}Schritt 1/5 — Systemcheck{RESET}\n")
    print(f"  System: {platform.system()} {platform.machine()}")

    python_ok = check_python()
    if not python_ok:
        print(f"\n  {RED}Python {MIN_PYTHON[0]}.{MIN_PYTHON[1]}+ wird benötigt.{RESET}")
        print(f"  {DIM}Download: https://python.org{RESET}")
        sys.exit(1)

    total_ram = check_ram()
    default_dir = get_default_install_dir()
    free_disk = check_disk(default_dir.parent)

    # ─── Schritt 2: Installationsverzeichnis ─────────────────────────
    print(f"\n{BOLD}Schritt 2/5 — Installationsort{RESET}")
    install_dir = Path(ask("Installationsverzeichnis", str(default_dir)))
    install_dir = install_dir.expanduser().resolve()

    if install_dir.exists() and any(install_dir.iterdir()):
        print(f"\n  {YELLOW}⚠ Verzeichnis existiert bereits und ist nicht leer.{RESET}")
        overwrite = ask("Trotzdem fortfahren? (j/n)", "j")
        if overwrite.lower() not in ("j", "ja", "y", "yes"):
            print(f"  {RED}Abgebrochen.{RESET}")
            sys.exit(0)
    
    install_dir.mkdir(parents=True, exist_ok=True)

    # Vorhandene Einstellungen früh lesen — BEVOR copy_source() irgendwas anfasst.
    # So bleiben Node-Name, Telegram-Token, Nutzerliste etc. bei Updates erhalten.
    existing_cfg = _read_existing_cfg(install_dir)
    if existing_cfg:
        print(f"\n  {GREEN}✓ Update erkannt — bisherige Einstellungen werden beibehalten:{RESET}")
        _node = existing_cfg.get("node", {})
        if _node.get("name"):
            print(f"  {DIM}  • Node-Name:       {_node['name']}{RESET}")
        if _node.get("specialization"):
            print(f"  {DIM}  • Spezialisierung: {_node['specialization']}{RESET}")
        if _node.get("persona"):
            _p = _node['persona'][:60] + ("…" if len(_node['persona']) > 60 else "")
            print(f"  {DIM}  • Persona:         {_p}{RESET}")
        _tg = existing_cfg.get("gateway", {}).get("telegram", {})
        if str(_tg.get("token", "")).strip():
            print(f"  {DIM}  • Telegram-Token:  ✓ vorhanden{RESET}")
        _mb = existing_cfg.get("moltbook", {})
        if str(_mb.get("api_key", "")).strip():
            print(f"  {DIM}  • Moltbook API-Key: ✓ vorhanden{RESET}")
        if str(_mb.get("agent_name", "")).strip():
            print(f"  {DIM}  • Moltbook Agent:  {_mb['agent_name']}{RESET}")
        _pl = existing_cfg.get("plugins", {}).get("enabled", [])
        if _pl:
            print(f"  {DIM}  • Plugins ({len(_pl)}):    {', '.join(_pl[:5])}{'…' if len(_pl) > 5 else ''}{RESET}")
        _cfg_file = install_dir / "config.yaml"
        if _cfg_file.exists():
            print(f"  {DIM}  (Backup wird vor dem Schreiben erstellt){RESET}")

    # ─── Schritt 3: Modellauswahl ────────────────────────────────────
    print(f"\n{BOLD}Schritt 3/5 — Modell auswählen{RESET}")
    print(f"  {DIM}Das Schnell-Modell (Qwen 2.5 0.5B) wird automatisch installiert \u2014 es liefert sofortige Vorantworten.{RESET}")
    print(f"  {DIM}Hier waehlst du nur das groessere Hauptmodell fuer ausfuehrliche Antworten.{RESET}\n")

    # Check for existing models
    existing_models = []
    models_dir = install_dir / "models"
    if models_dir.exists():
        existing_models = [m for m in models_dir.glob("*.gguf")
                           if m.name != FAST_MODEL["filename"]]
    if existing_models:
        print(f"\n  {GREEN}Vorhandene Modelle gefunden:{RESET}")
        for em in existing_models:
            size_gb = em.stat().st_size / (1024**3)
            print(f"    ✓ {em.name} ({size_gb:.1f} GB)")
        use_existing = ask("Vorhandenes Modell verwenden? (j/n)", "j")
        if use_existing.lower() in ("j", "ja", "y", "yes"):
            if len(existing_models) == 1:
                selected_model = {"name": existing_models[0].name, "filename": existing_models[0].name, "url": None}
                print(f"  {GREEN}→ Verwende {existing_models[0].name}{RESET}")
            else:
                em_options = [f"{em.name} ({em.stat().st_size / (1024**3):.1f} GB)" for em in existing_models]
                em_idx = ask_choice("Welches Modell?", em_options, default=1)
                selected_model = {"name": existing_models[em_idx].name, "filename": existing_models[em_idx].name, "url": None}
            # Skip to step 4
            print(f"\n  {DIM}Modell-Download wird übersprungen.{RESET}")
            # Jump past model selection
            goto_step4 = True
        else:
            goto_step4 = False
    else:
        goto_step4 = False

    if not goto_step4:
        print(f"\n  {DIM}Das AI-Modell ist das 'Gehirn' deines Nodes.")
        print(f"  Größere Modelle = bessere Antworten, aber langsamer + mehr RAM.{RESET}")

        # Filter models based on RAM
        model_options = []
        recommended = 0
        for i, m in enumerate(MODELS):
            if m["url"] is None:
                model_options.append(f"{m['name']}")
                continue
            ram_note = ""
            try:
                ram_needed = float(m["ram"].split()[0])
                if ram_needed > total_ram * 0.8:
                    ram_note = f"  {RED}(evtl. zu wenig RAM){RESET}"
            except (ValueError, IndexError):
                pass

            line = f"{m['name']}  |  {m['size']}  |  RAM: {m['ram']}  |  {m['quality']}{ram_note}"
            model_options.append(line)

            # Recommend based on RAM
            try:
                ram_needed = float(m["ram"].split()[0])
                if ram_needed <= total_ram * 0.6:
                    recommended = i
            except (ValueError, IndexError):
                pass

        model_idx = ask_choice(
            "Welches Modell möchtest du verwenden?",
            model_options,
            default=recommended + 1
        )
        selected_model = MODELS[model_idx]

    # ─── Schritt 4: Node-Name ────────────────────────────────────────
    print(f"\n{BOLD}Schritt 4/5 — Node einrichten{RESET}")
    default_name = (existing_cfg.get("node", {}).get("name", "")
                    or platform.node() or "mein-node")
    if existing_cfg.get("node", {}).get("name"):
        print(f"  {GREEN}✓ Bisheriger Node-Name vorausgewählt.{RESET}")
    node_name = ask("Name fuer deinen Node", default_name)

    # ─── Optional: Telegram ──────────────────────────────────────────
    tg_cfg = existing_cfg.get("gateway", {}).get("telegram", {})
    tg_already = bool(str(tg_cfg.get("token", "")).strip())
    # Telegram-Default: war es bei letzter Installation aktiv?
    _tg_was_installed = bool(
        existing_cfg.get("installation", {}).get("telegram") or tg_already
    )
    tg_default = "j" if _tg_was_installed else "n"
    print(f"\n  {BOLD}Optional: Telegram-Bridge{RESET}")
    if tg_already:
        tok = str(tg_cfg["token"])
        masked = tok[:6] + "..." + tok[-4:] if len(tok) > 10 else "***"
        users = tg_cfg.get("allowed_users", [])
        print(f"  {GREEN}✓ Vorhandener Token erkannt: {masked}{RESET}")
        if users:
            print(f"  {DIM}  Erlaubte Nutzer-IDs: {', '.join(str(u) for u in users)}{RESET}")
        print(f"  {DIM}  Token und Nutzerliste werden automatisch beibehalten.{RESET}")
    else:
        print(f"  {DIM}Damit kannst du deinen HiveMind-Node per Telegram-Bot bedienen.{RESET}")
        print(f"  {DIM}Benötigt ein Telegram-Konto und ~5 MB extra Speicherplatz.{RESET}")
    install_telegram = ask("Telegram-Paket installieren/aktualisieren? (j/n)", tg_default).lower() in ("j", "ja", "y", "yes")

    # ─── Zusammenfassung ─────────────────────────────────────────────
    print(f"\n{BOLD}{'─' * 50}{RESET}")
    print(f"  {BOLD}Zusammenfassung:{RESET}")
    print(f"  Verzeichnis: {install_dir}")
    print(f"  Modell:      {selected_model['name']}")
    print(f"  Node-Name:   {node_name}")
    print(f"  Telegram:    {'✓ Ja' if install_telegram else '– Nein'}")
    print(f"{'─' * 50}")
    
    confirm = ask("\n  Installation starten? (j/n)", "j")
    if confirm.lower() not in ("j", "ja", "y", "yes"):
        print(f"  {RED}Abgebrochen.{RESET}")
        sys.exit(0)

    # ─── Schritt 5: Installation ─────────────────────────────────────
    print(f"\n{BOLD}Schritt 5/5 — Installation{RESET}\n")

    # Copy source files
    print(f"  {BOLD}Kopiere Dateien...{RESET}")
    copy_source(install_dir)
    print(f"  {GREEN}✓ Dateien kopiert{RESET}")

    # Write config
    write_config(install_dir, selected_model["filename"], node_name,
                 existing=existing_cfg, install_telegram=install_telegram)
    print(f"  {GREEN}✓ Konfiguration {'aktualisiert' if existing_cfg else 'erstellt'}{RESET}")

    # Download model
    if selected_model["url"]:
        model_path = install_dir / "models" / selected_model["filename"]
        if model_path.exists():
            print(f"\n  {GREEN}✓ Modell bereits vorhanden: {selected_model['filename']}{RESET}")
        else:
            success = download_file(
                selected_model["url"],
                model_path,
                label=selected_model["name"]
            )
            if not success:
                print(f"  {YELLOW}⚠ Modell konnte nicht heruntergeladen werden.{RESET}")
                print(f"  {DIM}Du kannst es später manuell in {install_dir / 'models'} ablegen.{RESET}")
                write_config(install_dir, None, node_name,
                             existing=existing_cfg, install_telegram=install_telegram)

    # Create venv and install
    if not create_venv(install_dir, install_telegram=install_telegram):
        print(f"\n  {RED}✗ Installation fehlgeschlagen.{RESET}")
        print(f"  {DIM}Prüfe die Fehlermeldungen oben und versuche es erneut.{RESET}")
        sys.exit(1)

    # Create launcher
    print(f"\n  {BOLD}Erstelle Starter...{RESET}")
    create_launcher(install_dir)

    # ─── Fertig! ─────────────────────────────────────────────────────
    print(f"""
{GREEN}{BOLD}╔══════════════════════════════════════════════╗
║        ✅ Installation abgeschlossen!         ║
╚══════════════════════════════════════════════╝{RESET}

  {BOLD}So startest du HiveMind:{RESET}
""")
    if platform.system() == "Windows":
        print(f"  • Doppelklick auf {CYAN}HiveMind.bat{RESET} im Installationsordner")
        print(f"  • Oder die {CYAN}Desktop-Verknüpfung{RESET}")
    else:
        print(f"  • Doppelklick auf {CYAN}🧠 HiveMind{RESET} auf dem Desktop")
        print(f"  • Oder im Terminal: {CYAN}cd {install_dir} && ./start.sh{RESET}")

    print(f"""
  {BOLD}Befehle im Chat:{RESET}
  /status   — Node-Status anzeigen
  /plugins  — Plugins auflisten
  /help     — Hilfe
  /quit     — Beenden

  {DIM}Installationsordner: {install_dir}{RESET}
  {DIM}Konfiguration: {install_dir / 'config.yaml'}{RESET}
""")

    # Wait for keypress on Windows
    if platform.system() == "Windows":
        input("  Drücke Enter zum Beenden...")


if __name__ == "__main__":
    main()
