"""
BizNode Service Manager
========================
Core module for starting, stopping, and checking all BizNode services.
Provides backup-before-stop functionality.

Services managed (start order):
  1. Qdrant   — vector DB (memory/qdrant/qdrant.exe, port 6333)
  2. Ollama   — LLM server (ollama serve, port 11434)
  3. Dashboard — FastAPI server (launcher.py, port 7777)
  4. Bot      — Telegram bot watchdog (bot_runner.py)

Usage:
    python service_manager.py start       # Start all services
    python service_manager.py stop        # Stop all (prompts for backup)
    python service_manager.py stop --no-backup   # Stop without backup
    python service_manager.py stop --backup      # Stop with backup (no prompt)
    python service_manager.py status      # Show service status
    python service_manager.py restart     # Stop + Start
"""

from __future__ import annotations

import os
import sys
import subprocess
import time
import signal
import logging

_DASH_PORT = int(os.environ.get("DASHBOARD_PORT", "7777"))
from pathlib import Path

_ROOT = Path(__file__).parent.resolve()

logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s [%(name)s] %(levelname)s: %(message)s",
)
logger = logging.getLogger("biznode.services")

# Service process names / identifiers
OLLAMA_PROCESS = "ollama.exe" if sys.platform == "win32" else "ollama"
QDRANT_PROCESS = "qdrant.exe" if sys.platform == "win32" else "qdrant"
QDRANT_EXE = _ROOT / "memory" / "qdrant" / "qdrant.exe"
QDRANT_STORAGE = _ROOT / "memory" / "qdrant"
DASHBOARD_SCRIPT = "launcher.py"
BOT_SCRIPT = "bot_runner.py"
MONITOR_SCRIPT = str(_ROOT / "services" / "node_automonitor.py")


# ── Process detection ────────────────────────────────────────────────────────

def _find_pids_by_name(process_name: str) -> list[int]:
    """Find PIDs of running processes by name (Windows + Unix)."""
    pids = []
    try:
        if sys.platform == "win32":
            r = subprocess.run(
                ["tasklist", "/FI", f"IMAGENAME eq {process_name}", "/FO", "CSV", "/NH"],
                capture_output=True, text=True, timeout=10,
            )
            for line in r.stdout.strip().splitlines():
                parts = line.strip('"').split('","')
                if len(parts) >= 2:
                    try:
                        pids.append(int(parts[1]))
                    except ValueError:
                        pass
        else:
            r = subprocess.run(
                ["pgrep", "-f", process_name],
                capture_output=True, text=True, timeout=5,
            )
            for line in r.stdout.strip().splitlines():
                try:
                    pids.append(int(line.strip()))
                except ValueError:
                    pass
    except Exception:
        pass
    return pids


def _find_python_pids(script_name: str) -> list[int]:
    """Find PIDs of python processes running a specific script."""
    pids = []
    try:
        if sys.platform == "win32":
            # Use PowerShell (WMIC is deprecated on Windows 11+)
            ps_cmd = (
                "Get-CimInstance Win32_Process -Filter \"Name='python.exe'\" "
                "| Select-Object ProcessId,CommandLine "
                "| ForEach-Object { \"$($_.ProcessId)|$($_.CommandLine)\" }"
            )
            r = subprocess.run(
                ["powershell", "-NoProfile", "-Command", ps_cmd],
                capture_output=True, text=True, timeout=10,
            )
            for line in r.stdout.strip().splitlines():
                line = line.strip()
                if not line or "|" not in line:
                    continue
                pid_str, cmdline = line.split("|", 1)
                if script_name in cmdline:
                    try:
                        pid = int(pid_str.strip())
                        if pid != os.getpid():
                            pids.append(pid)
                    except ValueError:
                        pass
        else:
            r = subprocess.run(
                ["pgrep", "-f", script_name],
                capture_output=True, text=True, timeout=5,
            )
            for line in r.stdout.strip().splitlines():
                try:
                    pid = int(line.strip())
                    if pid != os.getpid():
                        pids.append(pid)
                except ValueError:
                    pass
    except Exception:
        pass
    return pids


def _kill_pid(pid: int, force: bool = False):
    """Kill a process by PID."""
    try:
        if sys.platform == "win32":
            cmd = ["taskkill", "/PID", str(pid), "/T"]
            if force:
                cmd.append("/F")
            subprocess.run(cmd, capture_output=True, timeout=10)
        else:
            sig = signal.SIGKILL if force else signal.SIGTERM
            os.kill(pid, sig)
    except Exception as e:
        logger.warning(f"Could not kill PID {pid}: {e}")


def _wait_for_port(port: int, timeout: int = 15) -> bool:
    """Wait until a port starts accepting connections."""
    import socket
    deadline = time.time() + timeout
    while time.time() < deadline:
        with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
            s.settimeout(1)
            if s.connect_ex(("127.0.0.1", port)) == 0:
                return True
        time.sleep(1)
    return False


# ── Service status ───────────────────────────────────────────────────────────

def is_qdrant_running() -> bool:
    return len(_find_pids_by_name(QDRANT_PROCESS)) > 0


def is_ollama_running() -> bool:
    return len(_find_pids_by_name(OLLAMA_PROCESS)) > 0


def is_dashboard_running() -> bool:
    return len(_find_python_pids(DASHBOARD_SCRIPT)) > 0


def is_bot_running() -> bool:
    return len(_find_python_pids(BOT_SCRIPT)) > 0


def check_port(port: int) -> bool:
    """Check if a port is in use."""
    import socket
    with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
        s.settimeout(2)
        return s.connect_ex(("127.0.0.1", port)) == 0


def is_watchdog_running() -> bool:
    """Check if the watchdog daemon is running."""
    pid_file = _ROOT / "watchdog" / "daemon.pid"
    if not pid_file.exists():
        return False
    try:
        pid = int(pid_file.read_text().strip())
        if sys.platform == "win32":
            r = subprocess.run(f"tasklist /FI \"PID eq {pid}\"",
                               shell=True, capture_output=True, text=True, timeout=5)
            return str(pid) in r.stdout
        else:
            os.kill(pid, 0)
            return True
    except (ValueError, OSError, subprocess.TimeoutExpired):
        return False


def get_status() -> dict:
    """Get status of all services."""
    return {
        "watchdog":  {"running": is_watchdog_running(),  "port": None,  "port_open": None},
        "qdrant":    {"running": is_qdrant_running(),    "port": 6333,  "port_open": check_port(6333)},
        "ollama":    {"running": is_ollama_running(),    "port": 11434, "port_open": check_port(11434)},
        "dashboard": {"running": is_dashboard_running(), "port": _DASH_PORT,  "port_open": check_port(_DASH_PORT)},
        "bot":       {"running": is_bot_running(),       "port": None,  "port_open": None},
        "monitor":   {"running": is_monitor_running(),   "port": None,  "port_open": None},
    }


def print_status():
    """Print service status table."""
    status = get_status()
    print()
    print("=" * 54)
    print("  BizNode Service Status")
    print("=" * 54)
    for name, info in status.items():
        proc_up = info["running"]
        port_up = info["port_open"]
        if proc_up:
            state = "[RUNNING]"
        elif port_up:
            state = "[RUNNING]"
        elif proc_up is False and port_up is False:
            state = "[STOPPED]"
        elif proc_up is False:
            state = "[STOPPED]"
        else:
            state = "[UNKNOWN]"

        port_str = f"  (port {info['port']})" if info["port"] else ""
        print(f"  {state:<12} {name:<14}{port_str}")
    print("=" * 54)
    print()


# ── Installation helpers ─────────────────────────────────────────────────────

QDRANT_DOWNLOAD_URL = "https://github.com/qdrant/qdrant/releases/latest/download/qdrant-x86_64-pc-windows-msvc.zip"
OLLAMA_DOWNLOAD_URL = "https://ollama.com/download/OllamaSetup.exe"


def _find_qdrant_exe() -> Path | None:
    """Find Qdrant executable: local install → system PATH → None."""
    if QDRANT_EXE.exists():
        return QDRANT_EXE
    from shutil import which
    found = which("qdrant")
    if found:
        return Path(found)
    return None


def _find_ollama_exe() -> str | None:
    """Find Ollama executable: system PATH → common install locations → None."""
    from shutil import which
    found = which("ollama")
    if found:
        return found
    # Check common Windows install paths
    if sys.platform == "win32":
        common_paths = [
            Path(os.environ.get("LOCALAPPDATA", "")) / "Programs" / "Ollama" / "ollama.exe",
            Path(os.environ.get("PROGRAMFILES", "")) / "Ollama" / "ollama.exe",
        ]
        for p in common_paths:
            if p.exists():
                return str(p)
    return None


def _install_qdrant() -> Path | None:
    """Download and install Qdrant to memory/qdrant/."""
    import urllib.request
    import zipfile
    import tempfile

    logger.info("Qdrant not found. Downloading...")
    print("  [*] Qdrant not installed. Downloading latest release...")

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

    try:
        zip_path = Path(tempfile.mktemp(suffix=".zip"))
        urllib.request.urlretrieve(QDRANT_DOWNLOAD_URL, str(zip_path))
        logger.info(f"Downloaded Qdrant ({zip_path.stat().st_size / 1024 / 1024:.1f} MB)")

        with zipfile.ZipFile(str(zip_path), "r") as zf:
            zf.extractall(str(QDRANT_STORAGE))
        zip_path.unlink()

        # The zip may contain qdrant.exe at root or in a subfolder
        if QDRANT_EXE.exists():
            logger.info(f"Qdrant installed to {QDRANT_EXE}")
            print(f"  [OK] Qdrant installed to {QDRANT_EXE}")
            return QDRANT_EXE

        # Search for qdrant.exe in extracted contents
        for f in QDRANT_STORAGE.rglob("qdrant.exe"):
            if f != QDRANT_EXE:
                import shutil
                shutil.move(str(f), str(QDRANT_EXE))
            logger.info(f"Qdrant installed to {QDRANT_EXE}")
            print(f"  [OK] Qdrant installed to {QDRANT_EXE}")
            return QDRANT_EXE

        logger.error("Downloaded zip did not contain qdrant.exe")
        print("  [!] Download succeeded but qdrant.exe not found in archive.")
        return None

    except Exception as e:
        logger.error(f"Failed to download Qdrant: {e}")
        print(f"  [!] Failed to download Qdrant: {e}")
        print(f"  [!] Download manually from: https://github.com/qdrant/qdrant/releases")
        return None


def _install_ollama() -> bool:
    """Download and run the Ollama installer (Windows)."""
    import urllib.request
    import tempfile

    logger.info("Ollama not found. Downloading installer...")
    print("  [*] Ollama not installed. Downloading installer...")

    try:
        installer_path = Path(tempfile.mktemp(suffix=".exe"))
        urllib.request.urlretrieve(OLLAMA_DOWNLOAD_URL, str(installer_path))
        logger.info(f"Downloaded Ollama installer ({installer_path.stat().st_size / 1024 / 1024:.1f} MB)")

        print("  [*] Running Ollama installer...")
        print("      Please complete the installation when prompted.")
        r = subprocess.run([str(installer_path)], timeout=300)
        installer_path.unlink(missing_ok=True)

        if _find_ollama_exe():
            logger.info("Ollama installed successfully.")
            print("  [OK] Ollama installed.")
            return True
        else:
            logger.warning("Ollama installer ran but ollama.exe not found in PATH.")
            print("  [!] Installer completed but Ollama not found.")
            print("      You may need to restart the terminal or add Ollama to PATH.")
            return False

    except Exception as e:
        logger.error(f"Failed to install Ollama: {e}")
        print(f"  [!] Failed to install Ollama: {e}")
        print(f"  [!] Install manually from: https://ollama.com/download")
        return False


def _ensure_ollama_model(model: str = "qwen3.5:4b") -> bool:
    """Ensure the required Ollama model is pulled."""
    ollama = _find_ollama_exe() or "ollama"
    try:
        r = subprocess.run(
            [ollama, "list"],
            capture_output=True, text=True, timeout=10,
        )
        if model in r.stdout:
            return True

        logger.info(f"Pulling Ollama model '{model}'...")
        print(f"  [*] Pulling Ollama model '{model}' (this may take a while)...")
        r = subprocess.run(
            [ollama, "pull", model],
            capture_output=False, timeout=600,
        )
        if r.returncode == 0:
            logger.info(f"Model '{model}' pulled successfully.")
            print(f"  [OK] Model '{model}' ready.")
            return True
        else:
            logger.warning(f"Failed to pull model '{model}'.")
            return False
    except Exception as e:
        logger.warning(f"Could not check/pull model: {e}")
        return False


# ── Start services ───────────────────────────────────────────────────────────

def start_qdrant() -> bool:
    """Start Qdrant vector DB. Auto-installs if not found."""
    if is_qdrant_running() or check_port(6333):
        logger.info("Qdrant is already running.")
        return True

    # Find or install qdrant
    qdrant_exe = _find_qdrant_exe()
    if not qdrant_exe:
        qdrant_exe = _install_qdrant()
        if not qdrant_exe:
            return False

    logger.info(f"Starting Qdrant ({qdrant_exe})...")
    try:
        if sys.platform == "win32":
            subprocess.Popen(
                [str(qdrant_exe)],
                cwd=str(QDRANT_STORAGE),
                stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
                creationflags=subprocess.CREATE_NO_WINDOW | subprocess.DETACHED_PROCESS,
            )
        else:
            subprocess.Popen(
                [str(qdrant_exe)],
                cwd=str(QDRANT_STORAGE),
                stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
                start_new_session=True,
            )

        if _wait_for_port(6333, timeout=15):
            logger.info("Qdrant started successfully (port 6333).")
            return True
        else:
            logger.error("Qdrant started but port 6333 not responding.")
            return False
    except Exception as e:
        logger.error(f"Failed to start Qdrant: {e}")
        return False


def start_ollama() -> bool:
    """Start Ollama. Auto-installs if not found."""
    if is_ollama_running():
        logger.info("Ollama is already running.")
        return True

    # Find or install ollama
    ollama_exe = _find_ollama_exe()
    if not ollama_exe:
        if not _install_ollama():
            return False
        ollama_exe = _find_ollama_exe() or "ollama"

    logger.info("Starting Ollama...")
    try:
        if sys.platform == "win32":
            subprocess.Popen(
                [ollama_exe, "serve"],
                stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
                creationflags=subprocess.CREATE_NO_WINDOW | subprocess.DETACHED_PROCESS,
            )
        else:
            subprocess.Popen(
                [ollama_exe, "serve"],
                stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
                start_new_session=True,
            )

        if _wait_for_port(11434, timeout=15):
            logger.info("Ollama started successfully (port 11434).")
            # Load .env model name if set
            model = "qwen3.5:4b"
            env_file = _ROOT / ".env"
            if env_file.exists():
                for line in env_file.read_text().splitlines():
                    if line.strip().startswith("LLM_MODEL="):
                        model = line.strip().split("=", 1)[1].strip()
                        break
            _ensure_ollama_model(model)
            return True
        else:
            logger.error("Ollama started but port 11434 not responding.")
            return False
    except FileNotFoundError:
        logger.error("Ollama not found even after install attempt.")
        return False
    except Exception as e:
        logger.error(f"Failed to start Ollama: {e}")
        return False


def start_dashboard() -> bool:
    """Start the dashboard (launcher.py) in background."""
    if is_dashboard_running() or check_port(_DASH_PORT):
        logger.info("Dashboard is already running.")
        return True

    logger.info("Starting Dashboard...")
    try:
        if sys.platform == "win32":
            subprocess.Popen(
                [sys.executable, str(_ROOT / DASHBOARD_SCRIPT)],
                cwd=str(_ROOT),
                stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
                creationflags=subprocess.CREATE_NEW_CONSOLE,
            )
        else:
            subprocess.Popen(
                [sys.executable, str(_ROOT / DASHBOARD_SCRIPT)],
                cwd=str(_ROOT),
                stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
                start_new_session=True,
            )

        if _wait_for_port(_DASH_PORT, timeout=20):
            logger.info("Dashboard started successfully (port %s).", _DASH_PORT)
            return True
        else:
            logger.warning("Dashboard may still be starting up...")
            return True
    except Exception as e:
        logger.error(f"Failed to start Dashboard: {e}")
        return False


def start_bot() -> bool:
    """Start the bot (bot_runner.py) in background."""
    if is_bot_running():
        logger.info("Bot is already running.")
        return True

    logger.info("Starting Bot...")
    try:
        if sys.platform == "win32":
            subprocess.Popen(
                [sys.executable, str(_ROOT / BOT_SCRIPT)],
                cwd=str(_ROOT),
                stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
                creationflags=subprocess.CREATE_NEW_CONSOLE,
            )
        else:
            subprocess.Popen(
                [sys.executable, str(_ROOT / BOT_SCRIPT)],
                cwd=str(_ROOT),
                stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
                start_new_session=True,
            )

        # Wait and verify the bot process stays alive (not crash-looping)
        time.sleep(5)
        if is_bot_running():
            logger.info("Bot started successfully.")
            return True
        else:
            # Process died — likely Telegram conflict or config error
            logger.error("Bot process exited immediately. Check logs/bot.log for details.")
            return False
    except Exception as e:
        logger.error(f"Failed to start Bot: {e}")
        return False


def is_monitor_running() -> bool:
    """Check if the node auto-monitor is running."""
    return len(_find_python_pids("node_automonitor.py")) > 0


def start_monitor() -> bool:
    """Start the node auto-monitor in background."""
    if is_monitor_running():
        logger.info("Node monitor is already running.")
        return True

    logger.info("Starting Node Auto-Monitor...")
    try:
        if sys.platform == "win32":
            subprocess.Popen(
                [sys.executable, MONITOR_SCRIPT, "--start"],
                cwd=str(_ROOT),
                stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
                creationflags=subprocess.CREATE_NO_WINDOW | subprocess.DETACHED_PROCESS,
            )
        else:
            subprocess.Popen(
                [sys.executable, MONITOR_SCRIPT, "--start"],
                cwd=str(_ROOT),
                stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
                start_new_session=True,
            )

        time.sleep(3)
        if is_monitor_running():
            logger.info("Node Auto-Monitor started successfully.")
            return True
        else:
            logger.warning("Node Auto-Monitor may still be starting up...")
            return True
    except Exception as e:
        logger.error(f"Failed to start Node Auto-Monitor: {e}")
        return False


def stop_monitor() -> bool:
    """Stop the node auto-monitor."""
    pids = _find_python_pids("node_automonitor.py")
    if not pids:
        logger.info("Node Monitor is not running.")
        return True

    logger.info(f"Stopping Node Monitor (PIDs: {pids})...")
    for pid in pids:
        _kill_pid(pid, force=True)
    time.sleep(2)

    if not is_monitor_running():
        logger.info("Node Monitor stopped.")
        return True
    else:
        logger.warning("Node Monitor may still be running.")
        return False


def _check_license() -> bool:
    """
    Run the license/trial gate from launcher.py.
    Returns True if allowed to proceed, False if blocked.
    """
    try:
        sys.path.insert(0, str(_ROOT))
        from launcher import _gate
        _gate()
        return True
    except SystemExit:
        return False
    except Exception as e:
        logger.error(f"License check failed: {e}")
        print(f"  [!] License check error: {e}")
        return False


def start_all() -> bool:
    """Start all BizNode services in order: License → Qdrant → Ollama → Dashboard → Bot."""
    print()
    print("=" * 54)
    print("  BizNode — Starting All Services")
    print("=" * 54)
    print()

    # ── License / trial gate ──
    if not _check_license():
        print("\n  [!] License check failed. Cannot start services.")
        return False
    sys.stdout.flush()

    # ── Show tier info ──
    try:
        sys.path.insert(0, str(_ROOT))
        from services.tier_manager import tier_info, TierLevel
        info = tier_info()
        tier_name = info["tier"].upper()
        max_bots = info["max_bots"]
        enabled = len(info["features_enabled"])
        total = info["features_total"]
        print(f"\n  Tier: {tier_name} | Bots: {max_bots} | Features: {enabled}/{total}")
        if info["features_disabled"]:
            print(f"  Pro-only: {', '.join(info['features_disabled'][:5])}"
                  + (f" +{len(info['features_disabled'])-5} more" if len(info['features_disabled']) > 5 else ""))
    except Exception as e:
        logger.debug(f"Tier info: {e}")

    print()
    ok = True
    ok = start_qdrant() and ok
    ok = start_ollama() and ok
    ok = start_dashboard() and ok
    ok = start_bot() and ok

    # Start watchdog daemon (monitors + auto-heals all services)
    start_watchdog()

    # Start support email auto-responder (admin node only)
    start_support_email()

    # Start node auto-monitor (AI-powered health monitoring + Telegram reports)
    start_monitor()

    print()
    print_status()
    return ok


def start_watchdog() -> bool:
    """Start the watchdog daemon as a background process."""
    watchdog_script = _ROOT / "watchdog" / "daemon.py"
    if not watchdog_script.exists():
        logger.debug("Watchdog not found: %s", watchdog_script)
        return False

    # Check if already running
    pid_file = _ROOT / "watchdog" / "daemon.pid"
    if pid_file.exists():
        try:
            pid = int(pid_file.read_text().strip())
            if sys.platform == "win32":
                r = subprocess.run(f"tasklist /FI \"PID eq {pid}\"",
                                   shell=True, capture_output=True, text=True, timeout=5)
                if str(pid) in r.stdout:
                    logger.info("Watchdog already running (PID %d)", pid)
                    return True
            else:
                os.kill(pid, 0)
                logger.info("Watchdog already running (PID %d)", pid)
                return True
        except (ValueError, OSError, subprocess.TimeoutExpired):
            pid_file.unlink(missing_ok=True)

    logger.info("Starting watchdog daemon...")
    try:
        creationflags = 0
        if sys.platform == "win32":
            creationflags = subprocess.DETACHED_PROCESS | subprocess.CREATE_NEW_PROCESS_GROUP

        subprocess.Popen(
            [sys.executable, str(watchdog_script)],
            cwd=str(_ROOT),
            stdout=subprocess.DEVNULL,
            stderr=subprocess.DEVNULL,
            creationflags=creationflags,
        )
        time.sleep(2)
        if pid_file.exists():
            pid = pid_file.read_text().strip()
            logger.info("Watchdog daemon started (PID %s)", pid)
            return True
        else:
            logger.warning("Watchdog daemon may not have started")
            return False
    except Exception as exc:
        logger.error("Failed to start watchdog: %s", exc)
        return False


def start_support_email() -> bool:
    """Start the support email auto-responder as a background process (admin only)."""
    support_script = _ROOT / "support" / "run_support.py"
    if not support_script.exists():
        logger.debug("Support email service not found: %s", support_script)
        return False

    # Check if already running
    if sys.platform == "win32":
        try:
            r = subprocess.run(
                'powershell -NoProfile -Command "'
                "Get-CimInstance Win32_Process -Filter \\\"Name='python.exe' OR Name='pythonw.exe'\\\" "
                "| ForEach-Object { $_.CommandLine }\"",
                shell=True, capture_output=True, text=True, timeout=10,
                creationflags=subprocess.CREATE_NO_WINDOW,
            )
            if "run_support.py" in r.stdout:
                logger.info("Support email service already running")
                return True
        except Exception:
            pass

    logger.info("Starting support email auto-responder...")
    try:
        pythonw = Path(sys.executable).parent / "pythonw.exe"
        exe = str(pythonw) if pythonw.exists() else sys.executable
        creationflags = 0
        if sys.platform == "win32":
            creationflags = subprocess.DETACHED_PROCESS | subprocess.CREATE_NO_WINDOW

        subprocess.Popen(
            [exe, str(support_script)],
            cwd=str(_ROOT),
            stdout=subprocess.DEVNULL,
            stderr=subprocess.DEVNULL,
            creationflags=creationflags,
        )
        logger.info("Support email auto-responder started")
        return True
    except Exception as exc:
        logger.error("Failed to start support email service: %s", exc)
        return False


def stop_watchdog() -> bool:
    """Stop the watchdog daemon."""
    pid_file = _ROOT / "watchdog" / "daemon.pid"
    if not pid_file.exists():
        logger.info("Watchdog not running (no PID file)")
        return True

    try:
        pid = int(pid_file.read_text().strip())
        _kill_pid(pid, force=True)
        pid_file.unlink(missing_ok=True)
        logger.info("Watchdog stopped (PID %d)", pid)
        return True
    except (ValueError, OSError) as exc:
        logger.warning("Watchdog stop issue: %s", exc)
        pid_file.unlink(missing_ok=True)
        return True


# ── Stop services ────────────────────────────────────────────────────────────

def stop_qdrant() -> bool:
    """Stop Qdrant."""
    pids = _find_pids_by_name(QDRANT_PROCESS)
    if not pids:
        logger.info("Qdrant is not running.")
        return True

    logger.info(f"Stopping Qdrant (PIDs: {pids})...")
    for pid in pids:
        _kill_pid(pid, force=True)
    time.sleep(2)

    if not is_qdrant_running():
        logger.info("Qdrant stopped.")
        return True
    else:
        logger.warning("Qdrant may still be running.")
        return False


def stop_ollama() -> bool:
    """Stop Ollama."""
    pids = _find_pids_by_name(OLLAMA_PROCESS)
    if not pids:
        logger.info("Ollama is not running.")
        return True

    logger.info(f"Stopping Ollama (PIDs: {pids})...")
    for pid in pids:
        _kill_pid(pid, force=True)
    time.sleep(2)

    if not is_ollama_running():
        logger.info("Ollama stopped.")
        return True
    else:
        logger.warning("Ollama may still be running.")
        return False


def stop_dashboard() -> bool:
    """Stop the dashboard process."""
    pids = _find_python_pids(DASHBOARD_SCRIPT)
    if not pids:
        logger.info("Dashboard is not running.")
        return True

    logger.info(f"Stopping Dashboard (PIDs: {pids})...")
    for pid in pids:
        _kill_pid(pid, force=True)
    time.sleep(2)

    if not is_dashboard_running():
        logger.info("Dashboard stopped.")
        return True
    else:
        logger.warning("Dashboard may still be running.")
        return False


def stop_bot() -> bool:
    """Stop the bot process."""
    pids = _find_python_pids(BOT_SCRIPT)
    if not pids:
        pids = _find_python_pids("run_bot.py")

    if not pids:
        logger.info("Bot is not running.")
        return True

    logger.info(f"Stopping Bot (PIDs: {pids})...")
    for pid in pids:
        _kill_pid(pid, force=True)
    time.sleep(2)

    if not is_bot_running():
        logger.info("Bot stopped.")
        return True
    else:
        logger.warning("Bot may still be running.")
        return False


def run_backup_before_stop() -> bool:
    """Run a local backup before stopping services (services must still be running)."""
    print()
    print("=" * 54)
    print("  Running backup before shutdown...")
    print("=" * 54)
    print()

    try:
        sys.path.insert(0, str(_ROOT))
        from backup.backup_engine import run_backup
        success, files, error = run_backup(upload_to_gdrive=False)

        if success:
            print(f"\n  [OK] Backup complete — {len(files)} file(s)")
            for f in files:
                print(f"    {f}")
        else:
            print(f"\n  [!] Backup completed with errors: {error}")
            if files:
                print(f"  Files saved: {len(files)}")

        print()
        return success

    except Exception as e:
        logger.error(f"Backup failed: {e}")
        print(f"\n  [!] Backup failed: {e}")
        print("  Continuing with shutdown...\n")
        return False


def stop_all(backup: bool | None = None) -> bool:
    """
    Stop all BizNode services.
    Reverse order: Bot → Dashboard → backup (if requested) → Ollama → Qdrant.
    Backup runs while Qdrant/Ollama are still up so data can be dumped.

    Args:
        backup: True = backup first, False = skip, None = prompt user
    """
    print()
    print("=" * 54)
    print("  BizNode — Stopping All Services")
    print("=" * 54)
    print()

    # ── Backup decision ──
    if backup is None:
        try:
            answer = input("  Take a backup before stopping? [Y/n]: ").strip().lower()
            do_backup = answer in ("", "y", "yes")
        except (EOFError, KeyboardInterrupt):
            do_backup = False
            print()
    else:
        do_backup = backup

    # ── Stop monitor and watchdog first (so they don't restart things we're stopping) ──
    stop_monitor()
    stop_watchdog()

    # ── Stop app-level services ──
    print("\n  Stopping application services...\n")
    ok = True
    ok = stop_bot() and ok
    ok = stop_dashboard() and ok

    # ── Backup while Qdrant + Ollama are still running ──
    if do_backup:
        run_backup_before_stop()

    # ── Now stop infrastructure services ──
    print("  Stopping infrastructure services...\n")
    ok = stop_ollama() and ok
    ok = stop_qdrant() and ok

    print()
    print_status()

    if ok:
        print("  All services stopped successfully.")
    else:
        print("  Some services may still be running.")
        print("  Check with: python service_manager.py status")
    print()
    return ok


# ── CLI entry point ──────────────────────────────────────────────────────────

def main():
    import argparse

    parser = argparse.ArgumentParser(description="BizNode Service Manager")
    parser.add_argument("action", choices=["start", "stop", "status", "restart"],
                        help="Action to perform")
    parser.add_argument("--backup", action="store_true", default=False,
                        help="Take backup before stop (skip prompt)")
    parser.add_argument("--no-backup", action="store_true", default=False,
                        help="Skip backup on stop (skip prompt)")

    args = parser.parse_args()

    if args.action == "start":
        start_all()

    elif args.action == "stop":
        if args.no_backup:
            stop_all(backup=False)
        elif args.backup:
            stop_all(backup=True)
        else:
            stop_all(backup=None)

    elif args.action == "status":
        print_status()

    elif args.action == "restart":
        if args.no_backup:
            stop_all(backup=False)
        elif args.backup:
            stop_all(backup=True)
        else:
            stop_all(backup=None)
        print("\n  Restarting in 3 seconds...\n")
        time.sleep(3)
        start_all()


if __name__ == "__main__":
    main()
