"""
BizNode Watchdog Daemon
========================
Continuous background process that monitors all services, auto-heals
failures using learned strategies, and reports status upstream.

Runs forever with a configurable poll interval. Designed to be started
by service_manager.py or Windows Task Scheduler at boot.

Usage:
    python watchdog/daemon.py                   # Run forever (foreground)
    python watchdog/daemon.py --once             # Single check, then exit
    python watchdog/daemon.py --interval 60      # Custom poll interval (seconds)
    python watchdog/daemon.py --json             # Output JSON per check
    pythonw watchdog/daemon.py                   # Run without console (background)
"""

from __future__ import annotations

import argparse
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
if str(_PROJECT_ROOT) not in sys.path:
    sys.path.insert(0, str(_PROJECT_ROOT))

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

from watchdog.failure_intelligence import (
    categorize_error, get_best_recovery, record_failure,
    record_snapshot, should_escalate, get_health_report,
)
from watchdog.alerting import (
    alert_service_down, alert_all_down, alert_recovered,
)

# ---------------------------------------------------------------------------
# Config
# ---------------------------------------------------------------------------

POLL_INTERVAL = int(os.getenv("WATCHDOG_INTERVAL", "30"))
STATUS_FILE = _PROJECT_ROOT / "watchdog" / "service_status.json"
PID_FILE = _PROJECT_ROOT / "watchdog" / "daemon.pid"
LOG_DIR = _PROJECT_ROOT / "logs"
LOG_DIR.mkdir(exist_ok=True)

logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
    handlers=[
        logging.FileHandler(str(LOG_DIR / "watchdog.log"), encoding="utf-8"),
        logging.StreamHandler(sys.stdout),
    ],
)
logger = logging.getLogger("watchdog.daemon")

# Reuse existing health checks from restart/health_monitor.py
sys.path.insert(0, str(_PROJECT_ROOT / "restart"))
try:
    from health_monitor import (
        check_postgresql, check_qdrant, check_ollama, check_bot,
        restart_postgresql, restart_qdrant, restart_ollama, restart_bot,
    )
except ImportError as exc:
    logger.error("Cannot import health_monitor: %s", exc)
    sys.exit(1)


# ---------------------------------------------------------------------------
# Service registry
# ---------------------------------------------------------------------------

SERVICES = {
    "postgresql": {"check": check_postgresql, "restart": restart_postgresql, "critical": True},
    "qdrant":     {"check": check_qdrant,     "restart": restart_qdrant,     "critical": False},
    "ollama":     {"check": check_ollama,      "restart": restart_ollama,     "critical": False},
    "bot":        {"check": check_bot,         "restart": restart_bot,        "critical": True},
}

# Track consecutive failures per service
_consecutive: dict[str, int] = {s: 0 for s in SERVICES}
# Track when service went down (for downtime calculation)
_down_since: dict[str, float] = {}
# Track last status for recovery detection
_last_status: dict[str, str] = {s: "unknown" for s in SERVICES}


# ---------------------------------------------------------------------------
# Recovery actions
# ---------------------------------------------------------------------------

def _kill_port_process(port: int):
    """Kill whatever is using a specific port."""
    if sys.platform == "win32":
        try:
            r = subprocess.run(
                f'netstat -ano | findstr ":{port}"',
                shell=True, capture_output=True, text=True, timeout=5,
                creationflags=_NO_WINDOW,
            )
            for line in r.stdout.strip().split("\n"):
                parts = line.split()
                if parts:
                    pid = parts[-1]
                    if pid.isdigit():
                        subprocess.run(f"taskkill /PID {pid} /F",
                                       shell=True, capture_output=True, timeout=5,
                                       creationflags=_NO_WINDOW)
        except Exception:
            pass


def _start_docker():
    """Attempt to start Docker Desktop."""
    if sys.platform == "win32":
        try:
            subprocess.Popen(
                ["C:\\Program Files\\Docker\\Docker\\Docker Desktop.exe"],
                creationflags=subprocess.DETACHED_PROCESS,
            )
            logger.info("Docker Desktop launch requested, waiting 30s...")
            time.sleep(30)
        except FileNotFoundError:
            logger.error("Docker Desktop not found at default path")


def execute_recovery(service: str, strategy: dict) -> bool:
    """Execute a recovery action and return success."""
    action = strategy.get("action", "restart_service")
    wait = strategy.get("wait_seconds", 3)

    svc = SERVICES.get(service)
    if not svc:
        return False

    logger.info("Recovery for %s: action=%s, wait=%ds", service, action, wait)

    try:
        if action == "escalate":
            return False  # Can't auto-fix

        if action == "kill_all_restart":
            # Kill existing processes (especially for bot 409 conflicts)
            if service == "bot":
                # Kill all bot-related python processes
                if sys.platform == "win32":
                    subprocess.run(
                        'powershell -Command "Get-Process python -EA SilentlyContinue | '
                        'ForEach-Object { $c=(Get-CimInstance Win32_Process -Filter '
                        '\\"ProcessId=$($_.Id)\\").CommandLine; '
                        'if($c -match \'bot_runner|run_bot\'){Stop-Process -Id $_.Id -Force}}"',
                        shell=True, capture_output=True, timeout=10,
                        creationflags=_NO_WINDOW,
                    )
            time.sleep(wait)
            result = svc["restart"]()
            return "ok" in str(result).lower() or "restart" in str(result).lower()

        if action == "kill_port_restart":
            port_map = {"postgresql": 5432, "qdrant": 6333, "ollama": 11434, "dashboard": 7777}
            port = port_map.get(service)
            if port:
                _kill_port_process(port)
            time.sleep(wait)
            result = svc["restart"]()
            return "ok" in str(result).lower() or "restart" in str(result).lower()

        if action == "start_docker":
            _start_docker()
            result = svc["restart"]()
            return "ok" in str(result).lower() or "restart" in str(result).lower()

        if action == "wait_retry":
            time.sleep(wait)
            check = svc["check"]()
            return check.get("status") == "ok"

        if action == "restart_service":
            time.sleep(wait)
            result = svc["restart"]()
            return "ok" in str(result).lower() or "restart" in str(result).lower()

        # Unknown action, try default restart
        time.sleep(wait)
        result = svc["restart"]()
        return "ok" in str(result).lower() or "restart" in str(result).lower()

    except Exception as exc:
        logger.error("Recovery action %s for %s failed: %s", action, service, exc)
        return False


# ---------------------------------------------------------------------------
# Main check cycle
# ---------------------------------------------------------------------------

def run_check_cycle() -> dict:
    """Run one complete health check cycle across all services."""
    results = {}
    all_healthy = True

    for name, svc in SERVICES.items():
        try:
            check = svc["check"]()
            status = check.get("status", "unknown")
            error = check.get("error", "")

            results[name] = {
                "status": status,
                "error": error,
                "checked_at": datetime.now(timezone.utc).isoformat(),
            }

            if status == "ok":
                # Service is up
                if _last_status[name] != "ok" and _last_status[name] != "unknown":
                    # Just recovered!
                    downtime = 0
                    if name in _down_since:
                        downtime = int(time.time() - _down_since[name])
                        del _down_since[name]
                    alert_recovered(name, downtime)
                    logger.info("%s recovered (was down for %ds)", name, downtime)

                _consecutive[name] = 0
                _last_status[name] = "ok"
                continue

            if status == "skipped":
                # Service not needed for this install (e.g. PostgreSQL when using SQLite)
                _consecutive[name] = 0
                _last_status[name] = "skipped"
                results[name]["status"] = "skipped"
                results[name]["reason"] = check.get("reason", "not configured")
                continue

            # Service is DOWN
            all_healthy = False
            _consecutive[name] += 1
            if name not in _down_since:
                _down_since[name] = time.time()

            error_type = categorize_error(name, error)
            logger.warning("%s DOWN (x%d): %s — %s",
                           name, _consecutive[name], error_type, error[:200])

            # Get recovery strategy
            strategy = get_best_recovery(name, error_type)
            logger.info("Strategy for %s: %s (source: %s)",
                        name, strategy["action"], strategy.get("source", "?"))

            # Check if we should escalate instead of auto-recovering
            if should_escalate(name, _consecutive[name]):
                logger.warning("Escalating %s — auto-recovery exhausted", name)
                alert_service_down(
                    name, error_type, error,
                    _consecutive[name], "escalate", False)
                record_failure(name, error_type, error, "escalate", False,
                               0, _consecutive[name])
                results[name]["recovery"] = "escalated"
                _last_status[name] = "down"
                continue

            # Attempt recovery
            start_time = time.time()
            success = execute_recovery(name, strategy)
            duration_ms = int((time.time() - start_time) * 1000)

            # Verify (some services need extra time after restart)
            if success:
                verify_wait = 10 if name in ("qdrant", "postgresql") else 3
                time.sleep(verify_wait)
                verify = svc["check"]()
                success = verify.get("status") == "ok"
                if not success:
                    # One more try with extra wait
                    time.sleep(5)
                    verify = svc["check"]()
                    success = verify.get("status") == "ok"

            # Record
            record_failure(
                name, error_type, error,
                strategy["action"], success, duration_ms,
                _consecutive[name])

            if success:
                logger.info("%s recovered via %s (%dms)", name, strategy["action"], duration_ms)
                _consecutive[name] = 0
                if name in _down_since:
                    del _down_since[name]
                results[name]["status"] = "recovered"
                results[name]["recovery"] = strategy["action"]
                _last_status[name] = "ok"
            else:
                # Alert based on escalation level
                alert_service_down(
                    name, error_type, error,
                    _consecutive[name], strategy["action"], False)
                results[name]["recovery"] = f"failed:{strategy['action']}"
                _last_status[name] = "down"

        except Exception as exc:
            logger.error("Check cycle error for %s: %s", name, exc)
            results[name] = {"status": "error", "error": str(exc)}
            all_healthy = False

    # Check if ALL critical services are down
    critical_down = [
        name for name, svc in SERVICES.items()
        if svc["critical"] and results.get(name, {}).get("status") in ("down", "error")
    ]
    if len(critical_down) >= 2:
        alert_all_down(critical_down)

    # Record snapshot
    record_snapshot(results, all_healthy)

    # Write status file (for bot to read for graceful degradation)
    _write_status_file(results, all_healthy)

    return results


def _write_status_file(results: dict, all_healthy: bool):
    """Write current status to JSON file for other processes to read."""
    try:
        STATUS_FILE.parent.mkdir(parents=True, exist_ok=True)
        data = {
            "last_check": datetime.now(timezone.utc).isoformat(),
            "all_healthy": all_healthy,
            "services": results,
            "daemon_pid": os.getpid(),
        }
        STATUS_FILE.write_text(json.dumps(data, indent=2), encoding="utf-8")
    except Exception as exc:
        logger.error("Failed to write status file: %s", exc)


# ---------------------------------------------------------------------------
# Daemon loop
# ---------------------------------------------------------------------------

def run_forever(interval: int = POLL_INTERVAL, json_output: bool = False):
    """Main daemon loop — runs health checks every `interval` seconds."""
    logger.info("=" * 60)
    logger.info("  BizNode Watchdog Daemon starting")
    logger.info("  PID: %d | Interval: %ds", os.getpid(), interval)
    logger.info("  Services: %s", ", ".join(SERVICES.keys()))
    logger.info("=" * 60)

    # Write PID file
    PID_FILE.parent.mkdir(parents=True, exist_ok=True)
    PID_FILE.write_text(str(os.getpid()))

    try:
        while True:
            try:
                results = run_check_cycle()

                if json_output:
                    print(json.dumps(results, indent=2))
                else:
                    statuses = " | ".join(
                        f"{name}:{info.get('status', '?')}"
                        for name, info in results.items()
                    )
                    logger.info("Check: %s", statuses)

            except Exception as exc:
                logger.error("Check cycle failed: %s", exc, exc_info=True)

            time.sleep(interval)

    except KeyboardInterrupt:
        logger.info("Watchdog daemon stopped (KeyboardInterrupt)")
    finally:
        if PID_FILE.exists():
            PID_FILE.unlink(missing_ok=True)


def run_once(json_output: bool = False) -> dict:
    """Run a single health check and exit."""
    results = run_check_cycle()
    if json_output:
        print(json.dumps(results, indent=2))
    else:
        for name, info in results.items():
            status = info.get("status", "?")
            error = info.get("error", "")
            recovery = info.get("recovery", "")
            line = f"  {name:15s} {status:10s}"
            if error:
                line += f" — {error[:80]}"
            if recovery:
                line += f" [{recovery}]"
            print(line)
    return results


# ---------------------------------------------------------------------------
# Entry point
# ---------------------------------------------------------------------------

def main():
    parser = argparse.ArgumentParser(description="BizNode Watchdog Daemon")
    parser.add_argument("--once", action="store_true",
                        help="Run single check and exit")
    parser.add_argument("--interval", type=int, default=POLL_INTERVAL,
                        help=f"Poll interval in seconds (default: {POLL_INTERVAL})")
    parser.add_argument("--json", action="store_true",
                        help="Output JSON per check cycle")
    parser.add_argument("--report", action="store_true",
                        help="Print health report and exit")
    args = parser.parse_args()

    if args.report:
        report = get_health_report()
        print(json.dumps(report, indent=2, default=str))
        return

    if args.once:
        run_once(json_output=args.json)
    else:
        run_forever(interval=args.interval, json_output=args.json)


if __name__ == "__main__":
    main()
