"""
BizNode Setup Wizard
====================
Automated dependency detection, installation, and health verification.

Detects and installs:
  - Python packages (from requirements.txt)
  - Docker Desktop (Windows)
  - Ollama LLM server
  - Qdrant vector database
  - PostgreSQL (optional, falls back to SQLite)

All actions are logged to logs/install.log for support troubleshooting.

Usage:
    python setup_wizard.py
"""

from __future__ import annotations

import json
import logging
import os
import platform
import shutil
import socket
import subprocess
import sys
import tempfile
import time
import urllib.request
import urllib.error
from datetime import datetime
from pathlib import Path

_ROOT = Path(__file__).parent.resolve()
_LOG_DIR = _ROOT / "logs"
_LOG_FILE = _LOG_DIR / "install.log"
_NO_WINDOW = subprocess.CREATE_NO_WINDOW if sys.platform == "win32" else 0

DOCKER_INSTALLER_URL = "https://desktop.docker.com/win/main/amd64/Docker%20Desktop%20Installer.exe"
OLLAMA_INSTALLER_URL = "https://ollama.com/download/OllamaSetup.exe"
QDRANT_DOWNLOAD_URL = "https://github.com/qdrant/qdrant/releases/latest/download/qdrant-x86_64-pc-windows-msvc.zip"
OLLAMA_API = "http://localhost:11434"
DEFAULT_MODEL = "qwen3.5:4b"
EMBEDDING_MODEL = "nomic-embed-text"
MIN_PYTHON = (3, 10)

_LOG_DIR.mkdir(parents=True, exist_ok=True)

logging.basicConfig(
    level=logging.DEBUG,
    format="%(asctime)s [%(levelname)s] %(message)s",
    handlers=[
        logging.FileHandler(str(_LOG_FILE), encoding="utf-8"),
        logging.StreamHandler(sys.stdout),
    ],
)
log = logging.getLogger("biznode.setup")


def _banner(text: str):
    width = 60
    print()
    print("=" * width)
    print(f"  {text}")
    print("=" * width)
    print()


def _step(num: int, total: int, msg: str):
    log.info(f"[Step {num}/{total}] {msg}")


def _ok(msg: str):
    log.info(f"  [OK] {msg}")


def _warn(msg: str):
    log.warning(f"  [!] {msg}")


def _fail(msg: str):
    log.error(f"  [FAIL] {msg}")


def _run(cmd: list, timeout: int = 30, **kwargs) -> subprocess.CompletedProcess:
    kw = {"capture_output": True, "text": True, "timeout": timeout}
    if sys.platform == "win32":
        kw["creationflags"] = _NO_WINDOW
    kw.update(kwargs)
    return subprocess.run(cmd, **kw)


def _port_open(port: int, host: str = "127.0.0.1") -> bool:
    with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
        s.settimeout(2)
        return s.connect_ex((host, port)) == 0


def _wait_for_port(port: int, timeout: int = 30) -> bool:
    deadline = time.time() + timeout
    while time.time() < deadline:
        if _port_open(port):
            return True
        time.sleep(1)
    return False


def _download_with_progress(url: str, dest: str, label: str = ""):
    def _hook(count, block_size, total_size):
        downloaded = count * block_size
        if total_size > 0:
            pct = min(100, downloaded * 100 // total_size)
            mb_done = downloaded // (1024 * 1024)
            mb_total = total_size // (1024 * 1024)
            print(f"\r    {label} {mb_done}/{mb_total} MB ({pct}%)   ", end="", flush=True)

    log.info(f"  Downloading {url}")
    urllib.request.urlretrieve(url, dest, reporthook=_hook)
    print()


# ── Check functions ──────────────────────────────────────────────────────────

def check_python() -> bool:
    ver = sys.version_info
    log.info(f"  Python version: {ver.major}.{ver.minor}.{ver.micro} ({sys.executable})")
    if ver >= MIN_PYTHON:
        _ok(f"Python {ver.major}.{ver.minor} meets minimum requirement ({MIN_PYTHON[0]}.{MIN_PYTHON[1]}+)")
        return True
    _fail(f"Python {ver.major}.{ver.minor} is below minimum {MIN_PYTHON[0]}.{MIN_PYTHON[1]}")
    log.info("  Download Python from https://www.python.org/downloads/")
    return False


def check_pip_packages() -> bool:
    req_file = _ROOT / "requirements.txt"
    if not req_file.exists():
        _warn("requirements.txt not found, skipping package check")
        return True

    probes = {
        "python-telegram-bot": "telegram",
        "langgraph": "langgraph",
        "langchain-core": "langchain_core",
        "qdrant-client": "qdrant_client",
        "apscheduler": "apscheduler",
        "fastapi": "fastapi",
        "uvicorn": "uvicorn",
        "python-dotenv": "dotenv",
        "requests": "requests",
    }

    missing = []
    for pkg_name, import_name in probes.items():
        try:
            __import__(import_name)
        except ImportError:
            missing.append(pkg_name)

    if not missing:
        _ok("All core Python packages are installed")
        return True

    log.info(f"  Missing packages: {', '.join(missing)}")
    log.info("  Installing from requirements.txt...")

    r = _run(
        [sys.executable, "-m", "pip", "install", "-r", str(req_file), "--user", "-q"],
        timeout=600,
    )
    if r.returncode == 0:
        _ok("Python packages installed successfully")
        return True

    _warn("Some packages may have failed to install")
    log.debug(f"  pip stderr: {r.stderr[:500]}")
    return False


def check_docker_installed() -> bool:
    docker_path = shutil.which("docker")
    if docker_path:
        try:
            r = _run(["docker", "--version"])
            _ok(f"Docker found: {r.stdout.strip()}")
            return True
        except Exception:
            pass
    if sys.platform == "win32":
        common = [
            Path(os.environ.get("PROGRAMFILES", "")) / "Docker" / "Docker" / "resources" / "bin" / "docker.exe",
            Path(os.environ.get("LOCALAPPDATA", "")) / "Docker" / "Docker" / "resources" / "bin" / "docker.exe",
        ]
        for p in common:
            if p.exists():
                _ok(f"Docker found at {p}")
                return True
    return False


def check_docker_running() -> bool:
    try:
        r = _run(["docker", "info"], timeout=15)
        if r.returncode == 0:
            _ok("Docker Desktop is running")
            return True
    except Exception:
        pass
    return False


def check_ollama_installed() -> bool:
    if shutil.which("ollama"):
        try:
            r = _run(["ollama", "--version"])
            _ok(f"Ollama found: {r.stdout.strip()}")
            return True
        except Exception:
            pass
    if sys.platform == "win32":
        common = [
            Path(os.environ.get("LOCALAPPDATA", "")) / "Programs" / "Ollama" / "ollama.exe",
            Path(os.environ.get("PROGRAMFILES", "")) / "Ollama" / "ollama.exe",
        ]
        for p in common:
            if p.exists():
                _ok(f"Ollama found at {p}")
                return True
    return False


def check_ollama_running() -> bool:
    try:
        req = urllib.request.Request(f"{OLLAMA_API}/api/tags", method="GET")
        with urllib.request.urlopen(req, timeout=3):
            return True
    except Exception:
        return False


def check_qdrant() -> bool:
    qdrant_exe = _ROOT / "memory" / "qdrant" / "qdrant.exe"
    if qdrant_exe.exists():
        _ok(f"Qdrant binary found: {qdrant_exe}")
        return True
    if shutil.which("qdrant"):
        _ok("Qdrant found on PATH")
        return True
    return False


def check_postgresql() -> bool:
    try:
        r = _run(["psql", "--version"])
        if r.returncode == 0:
            _ok(f"PostgreSQL found: {r.stdout.strip()}")
            return True
    except Exception:
        pass
    if _port_open(5432):
        _ok("PostgreSQL detected on port 5432")
        return True
    return False


# ── Install functions ────────────────────────────────────────────────────────

def install_docker() -> bool:
    if sys.platform != "win32":
        _warn("Automatic Docker install only supported on Windows")
        log.info("  Install Docker manually: https://docs.docker.com/engine/install/")
        return False

    log.info("  Docker Desktop not found. Downloading installer...")
    installer = os.path.join(tempfile.gettempdir(), "DockerDesktopInstaller.exe")

    try:
        _download_with_progress(DOCKER_INSTALLER_URL, installer, "Docker Desktop")
        log.info(f"  Downloaded: {os.path.getsize(installer) / 1024 / 1024:.0f} MB")
    except Exception as e:
        _fail(f"Failed to download Docker Desktop: {e}")
        log.info("  Download manually: https://www.docker.com/products/docker-desktop/")
        return False

    log.info("  Running Docker Desktop installer...")
    log.info("  This may take several minutes and require a system restart.")
    print()
    print("  NOTE: Docker Desktop installer will open.")
    print("  Follow the prompts to complete installation.")
    print("  If asked to restart, restart and re-run this setup wizard.")
    print()

    try:
        r = subprocess.run(
            [installer, "install", "--quiet", "--accept-license"],
            timeout=600,
            creationflags=_NO_WINDOW,
        )
        if r.returncode == 0:
            _ok("Docker Desktop installed successfully")
            log.info("  You may need to restart your computer and re-run setup_wizard.py")
            return True
        elif r.returncode == 3010:
            _ok("Docker Desktop installed. RESTART REQUIRED.")
            log.info("  Please restart your computer, then re-run: python setup_wizard.py")
            return True
        else:
            _warn(f"Docker installer exited with code {r.returncode}")
            log.info("  Try running the installer manually from:")
            log.info(f"  {installer}")
            return False
    except subprocess.TimeoutExpired:
        _warn("Docker installation timed out after 10 minutes")
        return False
    except Exception as e:
        _fail(f"Docker installation error: {e}")
        return False
    finally:
        try:
            os.remove(installer)
        except OSError:
            pass


def install_ollama() -> bool:
    if sys.platform != "win32":
        _warn("Automatic Ollama install only supported on Windows")
        log.info("  Install Ollama: curl -fsSL https://ollama.com/install.sh | sh")
        return False

    log.info("  Ollama not found. Downloading installer...")
    installer = os.path.join(tempfile.gettempdir(), "OllamaSetup.exe")

    try:
        _download_with_progress(OLLAMA_INSTALLER_URL, installer, "Ollama")
        log.info(f"  Downloaded: {os.path.getsize(installer) / 1024 / 1024:.0f} MB")
    except Exception as e:
        _fail(f"Failed to download Ollama: {e}")
        log.info("  Download manually: https://ollama.com/download")
        return False

    log.info("  Running Ollama installer (silent)...")
    try:
        r = subprocess.run(
            [installer, "/S"],
            timeout=120,
            creationflags=_NO_WINDOW,
        )
        if r.returncode == 0:
            _ok("Ollama installed successfully")
            try:
                os.remove(installer)
            except OSError:
                pass
            return True
        else:
            _warn(f"Ollama installer exited with code {r.returncode}")
            log.info(f"  Try running manually: {installer}")
            return False
    except subprocess.TimeoutExpired:
        _warn("Ollama installation timed out")
        return False
    except Exception as e:
        _fail(f"Ollama installation error: {e}")
        return False


def install_qdrant() -> bool:
    import zipfile

    qdrant_dir = _ROOT / "memory" / "qdrant"
    qdrant_exe = qdrant_dir / "qdrant.exe"

    log.info("  Qdrant not found. Downloading...")
    qdrant_dir.mkdir(parents=True, exist_ok=True)
    zip_path = os.path.join(tempfile.gettempdir(), "qdrant.zip")

    try:
        _download_with_progress(QDRANT_DOWNLOAD_URL, zip_path, "Qdrant")
        log.info(f"  Downloaded: {os.path.getsize(zip_path) / 1024 / 1024:.0f} MB")
    except Exception as e:
        _fail(f"Failed to download Qdrant: {e}")
        log.info("  Download manually: https://github.com/qdrant/qdrant/releases")
        return False

    try:
        with zipfile.ZipFile(zip_path, "r") as zf:
            zf.extractall(str(qdrant_dir))

        if qdrant_exe.exists():
            _ok(f"Qdrant installed to {qdrant_exe}")
            return True

        for f in qdrant_dir.rglob("qdrant.exe"):
            if f != qdrant_exe:
                shutil.move(str(f), str(qdrant_exe))
            _ok(f"Qdrant installed to {qdrant_exe}")
            return True

        _fail("Downloaded archive did not contain qdrant.exe")
        return False
    except Exception as e:
        _fail(f"Failed to extract Qdrant: {e}")
        return False
    finally:
        try:
            os.remove(zip_path)
        except OSError:
            pass


def start_docker() -> bool:
    if check_docker_running():
        return True

    log.info("  Starting Docker Desktop...")
    docker_paths = []
    if sys.platform == "win32":
        docker_paths = [
            Path(os.environ.get("PROGRAMFILES", "")) / "Docker" / "Docker" / "Docker Desktop.exe",
            Path(os.environ.get("LOCALAPPDATA", "")) / "Docker" / "Docker Desktop.exe",
        ]

    started = False
    for p in docker_paths:
        if p.exists():
            try:
                subprocess.Popen(
                    [str(p)],
                    stdout=subprocess.DEVNULL,
                    stderr=subprocess.DEVNULL,
                    creationflags=subprocess.CREATE_NO_WINDOW | subprocess.DETACHED_PROCESS if sys.platform == "win32" else 0,
                )
                started = True
                break
            except Exception as e:
                log.debug(f"  Could not start {p}: {e}")

    if not started:
        _warn("Could not find Docker Desktop executable to start automatically")
        log.info("  Please start Docker Desktop manually and re-run this wizard")
        return False

    log.info("  Waiting for Docker to become ready (up to 90 seconds)...")
    for i in range(90):
        time.sleep(1)
        if i % 10 == 9:
            print(f"\r    Waiting for Docker... {i+1}s", end="", flush=True)
        if check_docker_running():
            print()
            _ok("Docker Desktop is now running")
            return True

    print()
    _warn("Docker Desktop did not become ready within 90 seconds")
    log.info("  Docker may still be starting. Wait and re-run this wizard.")
    return False


def start_ollama() -> bool:
    if check_ollama_running():
        _ok("Ollama is already running")
        return True

    log.info("  Starting Ollama service...")
    ollama_cmd = shutil.which("ollama") or "ollama"

    if sys.platform == "win32":
        common = [
            Path(os.environ.get("LOCALAPPDATA", "")) / "Programs" / "Ollama" / "ollama.exe",
            Path(os.environ.get("PROGRAMFILES", "")) / "Ollama" / "ollama.exe",
        ]
        for p in common:
            if p.exists():
                ollama_cmd = str(p)
                break

    try:
        kw = {"stdout": subprocess.DEVNULL, "stderr": subprocess.DEVNULL}
        if sys.platform == "win32":
            kw["creationflags"] = subprocess.CREATE_NO_WINDOW | subprocess.DETACHED_PROCESS
        subprocess.Popen([ollama_cmd, "serve"], **kw)
    except FileNotFoundError:
        _fail(f"Ollama executable not found: {ollama_cmd}")
        return False
    except Exception as e:
        _fail(f"Failed to start Ollama: {e}")
        return False

    log.info("  Waiting for Ollama API (up to 30 seconds)...")
    if _wait_for_port(11434, timeout=30):
        _ok("Ollama service is running (port 11434)")
        return True

    _warn("Ollama did not respond within 30 seconds")
    return False


def pull_ollama_models() -> bool:
    ollama_cmd = shutil.which("ollama") or "ollama"

    if sys.platform == "win32":
        common = [
            Path(os.environ.get("LOCALAPPDATA", "")) / "Programs" / "Ollama" / "ollama.exe",
            Path(os.environ.get("PROGRAMFILES", "")) / "Ollama" / "ollama.exe",
        ]
        for p in common:
            if p.exists():
                ollama_cmd = str(p)
                break

    models_to_pull = [DEFAULT_MODEL, EMBEDDING_MODEL]
    try:
        req = urllib.request.Request(f"{OLLAMA_API}/api/tags", method="GET")
        with urllib.request.urlopen(req, timeout=5) as resp:
            data = json.loads(resp.read())
        local_models = [m["name"] for m in data.get("models", [])]
    except Exception:
        local_models = []

    all_ok = True
    for model in models_to_pull:
        base = model.split(":")[0]
        if any(m == model or m.startswith(base + ":") for m in local_models):
            _ok(f"Model '{model}' already present")
            continue

        log.info(f"  Pulling model '{model}' (this may take several minutes)...")
        try:
            pull_kw = {"timeout": 1200}
            if sys.platform == "win32":
                pull_kw["creationflags"] = _NO_WINDOW
            r = subprocess.run(
                [ollama_cmd, "pull", model],
                **pull_kw,
            )
            if r.returncode == 0:
                _ok(f"Model '{model}' pulled successfully")
            else:
                _warn(f"Failed to pull model '{model}'")
                all_ok = False
        except subprocess.TimeoutExpired:
            _warn(f"Model pull timed out for '{model}'")
            all_ok = False
        except Exception as e:
            _warn(f"Error pulling model '{model}': {e}")
            all_ok = False

    return all_ok


def start_qdrant() -> bool:
    if _port_open(6333):
        _ok("Qdrant is already running (port 6333)")
        return True

    qdrant_exe = _ROOT / "memory" / "qdrant" / "qdrant.exe"
    qdrant_path = shutil.which("qdrant")

    if qdrant_exe.exists():
        exe = str(qdrant_exe)
        cwd = str(qdrant_exe.parent)
    elif qdrant_path:
        exe = qdrant_path
        cwd = str(_ROOT / "memory" / "qdrant")
    else:
        _warn("Qdrant executable not found, cannot start")
        return False

    log.info(f"  Starting Qdrant ({exe})...")
    try:
        kw = {"stdout": subprocess.DEVNULL, "stderr": subprocess.DEVNULL, "cwd": cwd}
        if sys.platform == "win32":
            kw["creationflags"] = subprocess.CREATE_NO_WINDOW | subprocess.DETACHED_PROCESS
        subprocess.Popen([exe], **kw)
    except Exception as e:
        _fail(f"Failed to start Qdrant: {e}")
        return False

    if _wait_for_port(6333, timeout=15):
        _ok("Qdrant started successfully (port 6333)")
        return True

    _warn("Qdrant did not respond on port 6333")
    return False


def setup_database_config():
    env_file = _ROOT / ".env"
    has_pg = check_postgresql()

    if env_file.exists():
        content = env_file.read_text(encoding="utf-8")
        if "DB_TYPE=" in content:
            log.info("  Database configuration already exists in .env")
            if "DB_TYPE=postgresql" in content and not has_pg:
                _warn("PostgreSQL configured but not detected; BizNode will fall back to SQLite")
            return

    if has_pg:
        log.info("  PostgreSQL detected, configuring as primary database")
        db_config = "\n# Database\nDB_TYPE=postgresql\nDB_HOST=localhost\nDB_PORT=5432\nDB_NAME=biznode\nDB_USER=biznode_user\nDB_PASSWORD=\n"
    else:
        log.info("  PostgreSQL not found, configuring SQLite (no extra setup needed)")
        db_config = "\n# Database (SQLite — no PostgreSQL needed)\nDB_TYPE=sqlite\n"

    # Set node tier for alerts
    db_config += "\n# Node tier (used in watchdog alerts)\nNODE_TIER=basic\n"

    if env_file.exists():
        with open(env_file, "a", encoding="utf-8") as f:
            f.write(db_config)
    else:
        with open(env_file, "w", encoding="utf-8") as f:
            f.write("# BizNode Configuration\n" + db_config)

    _ok("Database configuration written to .env")


# ── Health check ─────────────────────────────────────────────────────────────

def run_health_check() -> dict:
    results = {}

    results["python"] = {
        "ok": sys.version_info >= MIN_PYTHON,
        "version": f"{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}",
    }

    results["ollama_running"] = {"ok": check_ollama_running()}
    results["qdrant_running"] = {"ok": _port_open(6333)}
    results["dashboard_port"] = {"ok": _port_open(7777)}

    docker_ok = False
    try:
        r = _run(["docker", "info"], timeout=10)
        docker_ok = r.returncode == 0
    except Exception:
        pass
    results["docker"] = {"ok": docker_ok}

    results["postgresql"] = {"ok": check_postgresql()}

    probes = ["telegram", "langgraph", "langchain_core", "qdrant_client", "fastapi"]
    pkg_ok = all(_importable(p) for p in probes)
    results["packages"] = {"ok": pkg_ok}

    return results


def _importable(pkg: str) -> bool:
    try:
        __import__(pkg)
        return True
    except ImportError:
        return False


def print_health_report(results: dict):
    _banner("Health Check Results")
    all_ok = True
    for name, info in results.items():
        status = "[OK]" if info["ok"] else "[!!]"
        extra = f" ({info['version']})" if "version" in info else ""
        label = name.replace("_", " ").title()
        log.info(f"  {status:<6} {label}{extra}")
        if not info["ok"]:
            all_ok = False

    print()
    if all_ok:
        log.info("  All checks passed. BizNode is ready to run!")
        log.info("  Start with: python launcher.py")
    else:
        log.info("  Some checks failed. Review the log above for details.")
        log.info(f"  Full log: {_LOG_FILE}")
    print()


# ── Node Registration ────────────────────────────────────────────────────────

REGISTER_URL = "https://biznode.1bz.biz/api_client_register.php?action=register"


def _register_with_server():
    """Register this node with BizNode server after setup completes."""
    env_file = _ROOT / ".env"
    env_vars = {}
    if env_file.exists():
        for line in env_file.read_text(encoding="utf-8").splitlines():
            line = line.strip()
            if not line or line.startswith("#") or "=" not in line:
                continue
            k, _, v = line.partition("=")
            env_vars[k.strip()] = v.strip().strip('"').strip("'")

    owner_email = env_vars.get("OWNER_EMAIL", "")
    if not owner_email:
        log.info("  Skipping server registration (no OWNER_EMAIL in .env)")
        return

    payload = {
        "owner_email":       owner_email,
        "bot_name":          env_vars.get("BOT_NAME", "BizNode"),
        "bot_username":      env_vars.get("TELEGRAM_BOT_USERNAME", ""),
        "telegram_chat_id":  env_vars.get("TELEGRAM_GROUP_ID", ""),
        "owner_telegram_id": env_vars.get("OWNER_TELEGRAM_ID", ""),
        "node_tier":         env_vars.get("NODE_TIER", "basic"),
        "business_name":     env_vars.get("BUSINESS_NAME", ""),
        "ollama_model":      env_vars.get("LLM_MODEL", DEFAULT_MODEL),
        "db_type":           env_vars.get("DB_TYPE", "sqlite"),
        "os_info":           f"{platform.system()} {platform.release()}",
        "version":           "",
    }
    version_file = _ROOT / "VERSION"
    if version_file.exists():
        payload["version"] = version_file.read_text(encoding="utf-8").strip()

    try:
        data = json.dumps(payload).encode("utf-8")
        req = urllib.request.Request(REGISTER_URL, data=data, method="POST")
        req.add_header("Content-Type", "application/json")
        with urllib.request.urlopen(req, timeout=10) as resp:
            result = json.loads(resp.read())
            if result.get("ok"):
                _ok("Node registered with BizNode server")
            else:
                log.info("  Registration response: %s", result)
    except Exception as exc:
        log.info("  Server registration skipped (non-critical): %s", exc)


# ── Main wizard ──────────────────────────────────────────────────────────────

def main():
    start_time = datetime.now()
    _banner("BizNode Setup Wizard")
    log.info(f"Setup started at {start_time.isoformat()}")
    log.info(f"Platform: {platform.platform()}")
    log.info(f"Python: {sys.version}")
    log.info(f"Install path: {_ROOT}")
    log.info(f"Log file: {_LOG_FILE}")
    print()

    total_steps = 7
    results = {}

    # Step 1: Python version
    _step(1, total_steps, "Checking Python version")
    results["python"] = check_python()

    if not results["python"]:
        _fail("Python 3.10+ is required. Please upgrade and re-run this wizard.")
        sys.exit(1)

    # Step 2: Python packages
    _step(2, total_steps, "Checking Python packages")
    results["packages"] = check_pip_packages()

    # Step 3: Docker
    _step(3, total_steps, "Checking Docker Desktop")
    docker_installed = check_docker_installed()
    if not docker_installed:
        log.info("  Docker Desktop not found.")
        log.info("  Docker is optional for Basic tier (Qdrant runs natively).")
        print()
        try:
            answer = input("  Install Docker Desktop? [y/N]: ").strip().lower()
        except (EOFError, KeyboardInterrupt):
            answer = "n"

        if answer in ("y", "yes"):
            docker_installed = install_docker()
        else:
            log.info("  Skipping Docker installation (not required for Basic)")

    if docker_installed and not check_docker_running():
        log.info("  Docker is installed but not running.")
        start_docker()

    results["docker"] = docker_installed

    # Step 4: Ollama
    _step(4, total_steps, "Checking Ollama LLM server")
    ollama_installed = check_ollama_installed()
    if not ollama_installed:
        log.info("  Ollama not found. Installing...")
        ollama_installed = install_ollama()

    if ollama_installed:
        ollama_running = start_ollama()
        if ollama_running:
            log.info("  Pulling required AI models...")
            pull_ollama_models()
    else:
        _warn("Ollama is required for AI features. Install manually from https://ollama.com/download")

    results["ollama"] = ollama_installed

    # Step 5: Qdrant
    _step(5, total_steps, "Checking Qdrant vector database")
    qdrant_installed = check_qdrant()
    if not qdrant_installed:
        log.info("  Qdrant not found. Installing...")
        qdrant_installed = install_qdrant()

    if qdrant_installed:
        start_qdrant()

    results["qdrant"] = qdrant_installed

    # Step 6: Database
    _step(6, total_steps, "Checking database configuration")
    setup_database_config()

    # Step 7: Health check
    _step(7, total_steps, "Running final health check")
    health = run_health_check()
    print_health_report(health)

    # Register node with BizNode server (for support tracking)
    _register_with_server()

    elapsed = (datetime.now() - start_time).total_seconds()
    log.info(f"Setup completed in {elapsed:.0f} seconds")
    log.info(f"Full installation log: {_LOG_FILE}")

    _banner("Setup Complete")
    log.info("  To start BizNode, run:")
    log.info("    python launcher.py")
    log.info("")
    log.info("  Or use the service manager:")
    log.info("    python service_manager.py start")
    print()

    all_critical_ok = results.get("python") and results.get("ollama") and results.get("qdrant")
    if not all_critical_ok:
        log.info("  Some critical components are missing. Review the log above.")
        log.info("  You can re-run this wizard at any time: python setup_wizard.py")
        return 1

    return 0


if __name__ == "__main__":
    try:
        sys.exit(main())
    except KeyboardInterrupt:
        print("\n\n  Setup cancelled by user.")
        sys.exit(1)
    except Exception as e:
        log.exception(f"Setup wizard crashed: {e}")
        print(f"\n  Unexpected error: {e}")
        print(f"  Check the log for details: {_LOG_FILE}")
        sys.exit(1)
