"""
BizNode Health Monitor
=======================
Checks PostgreSQL, Qdrant, and Ollama every run.
If any service is down, attempts to restart it.
Logs all results to logs/health_monitor.log.

Usage:
    python restart/health_monitor.py          (single check)
    python restart/health_monitor.py --json   (output JSON for programmatic use)
"""

import json
import logging
import os
import subprocess
import sys
import time
from datetime import datetime, timezone
from pathlib import Path

_PROJECT_ROOT = Path(__file__).resolve().parent.parent
_LOG_DIR = _PROJECT_ROOT / "logs"
_LOG_DIR.mkdir(exist_ok=True)

# Prevent console window flash on Windows for all subprocess calls
_NO_WINDOW = subprocess.CREATE_NO_WINDOW if sys.platform == "win32" else 0

logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s [%(levelname)s] %(message)s",
    handlers=[
        logging.FileHandler(str(_LOG_DIR / "health_monitor.log"), encoding="utf-8"),
        logging.StreamHandler(),
    ],
)
log = logging.getLogger("biznode.health")


def _load_env():
    """Load .env into os.environ."""
    env_file = _PROJECT_ROOT / ".env"
    if not env_file.exists():
        return
    with open(env_file, encoding="utf-8") as f:
        for line in f:
            line = line.strip()
            if not line or line.startswith("#") or "=" not in line:
                continue
            k, _, v = line.partition("=")
            k, v = k.strip(), v.strip().strip('"').strip("'")
            if k:
                os.environ.setdefault(k, v)


_load_env()


# ── Service Checks ───────────────────────────────────────────────────────────

def check_postgresql() -> dict:
    """Check if PostgreSQL is reachable."""
    db_type = os.getenv("DB_TYPE", "sqlite").lower()
    if db_type != "postgresql":
        return {"service": "postgresql", "status": "skipped", "reason": f"DB_TYPE={db_type}"}

    try:
        import psycopg2
        conn = psycopg2.connect(
            host=os.getenv("DB_HOST", "localhost"),
            port=int(os.getenv("DB_PORT", "5432")),
            dbname=os.getenv("DB_NAME", "biznode"),
            user=os.getenv("DB_USER", "root"),
            password=os.getenv("DB_PASSWORD", ""),
            connect_timeout=5,
        )
        cur = conn.cursor()
        cur.execute("SELECT 1")
        conn.close()
        return {"service": "postgresql", "status": "ok"}
    except Exception as e:
        return {"service": "postgresql", "status": "down", "error": str(e)}


def check_qdrant() -> dict:
    """Check if Qdrant is reachable at port 6333."""
    try:
        import urllib.request
        url = os.getenv("QDRANT_URL", "http://localhost:6333")
        url = url.rstrip("/")
        req = urllib.request.Request(f"{url}/collections", method="GET")
        with urllib.request.urlopen(req, timeout=5) as resp:
            data = json.loads(resp.read())
            if data.get("status") == "ok":
                collections = len(data.get("result", {}).get("collections", []))
                return {"service": "qdrant", "status": "ok", "collections": collections}
        return {"service": "qdrant", "status": "down", "error": "bad response"}
    except Exception as e:
        return {"service": "qdrant", "status": "down", "error": str(e)}


def check_ollama() -> dict:
    """Check if Ollama is reachable at port 11434."""
    try:
        import urllib.request
        url = os.getenv("OLLAMA_URL", "http://localhost:11434/api/generate")
        base = url.rsplit("/api/", 1)[0] if "/api/" in url else "http://localhost:11434"
        req = urllib.request.Request(f"{base}/api/tags", method="GET")
        with urllib.request.urlopen(req, timeout=5) as resp:
            data = json.loads(resp.read())
            models = len(data.get("models", []))
            return {"service": "ollama", "status": "ok", "models": models}
    except Exception as e:
        return {"service": "ollama", "status": "down", "error": str(e)}


def check_bot() -> dict:
    """Check if the Telegram bot process (bot_runner.py) is running."""
    try:
        if sys.platform == "win32":
            ps_cmd = (
                "Get-CimInstance Win32_Process -Filter \"Name='python.exe'\" "
                "| Select-Object CommandLine "
                "| ForEach-Object { $_.CommandLine }"
            )
            r = subprocess.run(
                ["powershell", "-NoProfile", "-Command", ps_cmd],
                capture_output=True, text=True, timeout=10,
                creationflags=_NO_WINDOW,
            )
            if "bot_runner.py" in r.stdout or "run_bot.py" in r.stdout:
                return {"service": "bot", "status": "ok"}
        else:
            r = subprocess.run(
                ["pgrep", "-f", "bot_runner.py"],
                capture_output=True, text=True, timeout=5,
                creationflags=_NO_WINDOW,
            )
            if r.stdout.strip():
                return {"service": "bot", "status": "ok"}

        return {"service": "bot", "status": "down", "error": "bot process not found"}
    except Exception as e:
        return {"service": "bot", "status": "down", "error": str(e)}


# ── Service Restarts ─────────────────────────────────────────────────────────

def restart_postgresql() -> str:
    """Attempt to restart PostgreSQL."""
    try:
        # Try Windows service restart
        result = subprocess.run(
            ["net", "start", "postgresql-x64-17"],
            capture_output=True, text=True, timeout=30,
            creationflags=_NO_WINDOW,
        )
        if result.returncode == 0:
            return "restarted via Windows service"

        # Try pg_ctl
        pg_data = os.getenv("PGDATA", "")
        if pg_data:
            subprocess.run(
                ["pg_ctl", "restart", "-D", pg_data, "-w"],
                capture_output=True, text=True, timeout=30,
                creationflags=_NO_WINDOW,
            )
            return "restarted via pg_ctl"

        # Try Docker (only if available)
        if _docker_available():
            result = subprocess.run(
                ["docker", "start", "postgres"],
                capture_output=True, text=True, timeout=15,
                creationflags=_NO_WINDOW,
            )
            if result.returncode == 0:
                return "restarted via Docker"

        return "restart failed: no method succeeded"
    except Exception as e:
        return f"restart failed: {e}"


def _docker_available() -> bool:
    """Check if Docker is running (avoid errors on client machines without Docker)."""
    try:
        r = subprocess.run(
            ["docker", "info"],
            capture_output=True, text=True, timeout=10,
            creationflags=_NO_WINDOW,
        )
        return r.returncode == 0
    except (FileNotFoundError, Exception):
        return False


def restart_qdrant() -> str:
    """Attempt to restart Qdrant."""
    try:
        # Only try Docker if it's actually available and running
        if _docker_available():
            # Try Docker first — check multiple container name patterns
            for container_name in ("biznode-qdrant-1", "qdrant", "biznode_qdrant_1"):
                result = subprocess.run(
                    ["docker", "start", container_name],
                    capture_output=True, text=True, timeout=15,
                    creationflags=_NO_WINDOW,
                )
                if result.returncode == 0:
                    time.sleep(8)  # Qdrant needs time to initialize collections
                    return f"restarted via Docker ({container_name})"

            # Try docker-compose up (only qdrant service)
            compose_file = _PROJECT_ROOT / "docker-compose.yml"
            if compose_file.exists():
                result = subprocess.run(
                    ["docker", "compose", "-f", str(compose_file), "up", "-d", "qdrant"],
                    capture_output=True, text=True, timeout=30,
                    cwd=str(_PROJECT_ROOT), creationflags=_NO_WINDOW,
                )
                if result.returncode == 0:
                    time.sleep(5)
                    verify = check_qdrant()
                    if verify["status"] == "ok":
                        return "restarted via docker-compose"

        # Try local binary
        qdrant_exe = Path("C:/qdrant/qdrant.exe")
        if qdrant_exe.exists():
            storage_path = str(_PROJECT_ROOT / "memory" / "qdrant" / "storage")
            env = os.environ.copy()
            env["QDRANT__STORAGE__STORAGE_PATH"] = storage_path
            env["QDRANT__SERVICE__HTTP_PORT"] = "6333"
            subprocess.Popen(
                [str(qdrant_exe)],
                env=env,
                stdout=open(str(_LOG_DIR / "qdrant.log"), "a"),
                stderr=subprocess.STDOUT,
                creationflags=subprocess.CREATE_NO_WINDOW if sys.platform == "win32" else 0,
            )
            time.sleep(3)
            # Verify it started
            verify = check_qdrant()
            if verify["status"] == "ok":
                return "restarted via local binary"
            return "binary launched but not responding yet"

        return "restart failed: no Docker or qdrant.exe found"
    except Exception as e:
        return f"restart failed: {e}"


def restart_ollama() -> str:
    """Attempt to restart Ollama."""
    try:
        # Ollama typically runs as a service on Windows
        result = subprocess.run(
            ["ollama", "serve"],
            capture_output=True, text=True, timeout=5,
            creationflags=_NO_WINDOW,
        )
        # If it returns quickly, it might already be running or failed
        # Try starting via the Windows app
        ollama_exe = Path(os.getenv("LOCALAPPDATA", ""), "Programs", "Ollama", "ollama app.exe")
        if ollama_exe.exists():
            subprocess.Popen(
                [str(ollama_exe)],
                creationflags=subprocess.CREATE_NO_WINDOW if sys.platform == "win32" else 0,
            )
            time.sleep(5)
            verify = check_ollama()
            if verify["status"] == "ok":
                return "restarted via Ollama app"

        # Fallback: try 'ollama serve' in background
        subprocess.Popen(
            ["ollama", "serve"],
            stdout=open(str(_LOG_DIR / "ollama.log"), "a"),
            stderr=subprocess.STDOUT,
            creationflags=subprocess.CREATE_NO_WINDOW if sys.platform == "win32" else 0,
        )
        time.sleep(5)
        verify = check_ollama()
        if verify["status"] == "ok":
            return "restarted via ollama serve"

        return "restart attempted but service not responding"
    except Exception as e:
        return f"restart failed: {e}"


def restart_bot() -> str:
    """Attempt to restart the Telegram bot via bot_runner.py.

    IMPORTANT: Kills ALL existing bot processes first to avoid
    Telegram 409 Conflict errors from duplicate polling instances.
    """
    try:
        bot_script = _PROJECT_ROOT / "bot_runner.py"
        if not bot_script.exists():
            return "restart failed: bot_runner.py not found"

        # Kill any existing bot processes to prevent 409 Conflict
        killed = 0
        if sys.platform == "win32":
            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,
                creationflags=_NO_WINDOW,
            )
            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 "bot_runner.py" in cmdline or "run_bot.py" in cmdline:
                    try:
                        pid = int(pid_str.strip())
                        subprocess.run(
                            ["taskkill", "/PID", str(pid), "/T", "/F"],
                            capture_output=True, timeout=10,
                            creationflags=_NO_WINDOW,
                        )
                        killed += 1
                    except (ValueError, Exception):
                        pass
        else:
            for script in ("bot_runner.py", "run_bot.py"):
                r = subprocess.run(
                    ["pkill", "-f", script],
                    capture_output=True, text=True, timeout=5,
                    creationflags=_NO_WINDOW,
                )
                if r.returncode == 0:
                    killed += 1

        if killed:
            log.info("  BOT: Killed %d existing bot process(es)", killed)
            time.sleep(3)  # Let Telegram release the polling session

        subprocess.Popen(
            [sys.executable, str(bot_script)],
            cwd=str(_PROJECT_ROOT),
            stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
            creationflags=subprocess.CREATE_NEW_CONSOLE if sys.platform == "win32" else 0,
        )
        time.sleep(5)
        verify = check_bot()
        if verify["status"] == "ok":
            return f"restarted via bot_runner.py (killed {killed} old process(es))"
        return "restart attempted but process not confirmed"
    except Exception as e:
        return f"restart failed: {e}"


# ── Main Monitor ─────────────────────────────────────────────────────────────

RESTART_MAP = {
    "postgresql": restart_postgresql,
    "qdrant": restart_qdrant,
    "ollama": restart_ollama,
    "bot": restart_bot,
}


def run_health_check(auto_restart: bool = True) -> list:
    """
    Run health checks on all services.
    If auto_restart is True, attempts to restart any down services.

    Returns list of result dicts.
    """
    now = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M:%S UTC")
    log.info("=" * 50)
    log.info("Health check started at %s", now)

    results = []
    checks = [check_postgresql, check_qdrant, check_ollama, check_bot]

    for check_fn in checks:
        result = check_fn()
        service = result["service"]
        status = result["status"]

        if status == "ok":
            log.info("  %s: OK%s", service.upper(),
                     f" ({result.get('collections', result.get('models', ''))} loaded)"
                     if result.get("collections") or result.get("models") else "")
        elif status == "skipped":
            log.info("  %s: SKIPPED (%s)", service.upper(), result.get("reason", ""))
        else:
            log.warning("  %s: DOWN — %s", service.upper(), result.get("error", "unknown"))

            if auto_restart and service in RESTART_MAP:
                log.info("  %s: Attempting restart...", service.upper())
                restart_result = RESTART_MAP[service]()
                result["restart"] = restart_result
                log.info("  %s: %s", service.upper(), restart_result)

                # Re-check after restart
                time.sleep(2)
                recheck = check_fn()
                result["status_after_restart"] = recheck["status"]
                if recheck["status"] == "ok":
                    log.info("  %s: Recovery confirmed", service.upper())
                else:
                    log.error("  %s: Still down after restart", service.upper())

        results.append(result)

    summary = {s["service"]: s["status"] for s in results}
    down = [k for k, v in summary.items() if v == "down"]
    if down:
        log.warning("Health check complete: %s DOWN", ", ".join(down))
    else:
        log.info("Health check complete: all services operational")

    return results


if __name__ == "__main__":
    output_json = "--json" in sys.argv
    results = run_health_check(auto_restart=True)
    if output_json:
        print(json.dumps(results, indent=2))
