#!/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:
            # PowerShell script that streams download and reports progress to stdout
            ps_script = (
                "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; "
                f"$url = '{url}'; "
                f"$out = '{dest_path}'; "
                "$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 proc.returncode == 0 and 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 = proc.stderr.read().decode("utf-8", errors="replace").strip().split('\n')[-1] if proc.stderr else "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)


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
    )

    # Install llama-cpp-python (may need compilation)
    print(f"\n  {BOLD}Installiere llama-cpp-python...{RESET}")
    print(f"  {DIM}(Dies kann einige (bis zu 30) Minuten dauern — C++ wird kompiliert){RESET}")

    env = os.environ.copy()
    env["CMAKE_ARGS"] = "-DGGML_NATIVE=OFF"

    result = subprocess.run(
        [str(python_path), "-m", "pip", "install", "llama-cpp-python>=0.3.0"],
        capture_output=True, text=True, env=env, timeout=600
    )
    if result.returncode != 0:
        print(f"  {RED}✗ llama-cpp-python Installation fehlgeschlagen{RESET}")
        print(f"  {DIM}Häufige Ursache: Fehlende Build-Tools{RESET}")
        if platform.system() == "Windows":
            print(f"  {YELLOW}→ Installiere 'Visual Studio Build Tools' von:")
            print(f"    https://visualstudio.microsoft.com/visual-cpp-build-tools/{RESET}")
        else:
            print(f"  {YELLOW}→ Installiere: sudo apt install build-essential cmake{RESET}")
        # Show last few lines of error
        err_lines = result.stderr.strip().split("\n")[-5:]
        for line in err_lines:
            print(f"  {DIM}{line}{RESET}")
        return False
    print(f"  {GREEN}✓ llama-cpp-python installiert{RESET}")

    # 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()
