"""Cluster health monitoring for GreyMatter nodes.

Provides node-level and cluster-level health checks:

  Node health (local):
    - Database accessible and writable
    - Sync version advancing (not stuck)
    - Certificate valid and not near expiry
    - Disk space adequate

  Cluster health (hub aggregates from spokes):
    - Which spokes are reachable
    - Sync lag per spoke (how far behind hub)
    - Last successful sync timestamp per spoke
    - Overall cluster convergence status

Usage:
    checker = HealthChecker(db)
    report = checker.check_node()    # NodeHealthReport
    cluster = checker.check_cluster(spoke_reports)  # ClusterHealthReport
"""
from __future__ import annotations

import logging
import os
import time
from dataclasses import dataclass, field
from datetime import datetime, timezone
from enum import Enum
from pathlib import Path
from typing import Any

logger = logging.getLogger(__name__)


class HealthStatus(str, Enum):
    """Health check result."""
    HEALTHY = "healthy"
    DEGRADED = "degraded"  # Functional but needs attention
    UNHEALTHY = "unhealthy"  # Broken, needs intervention


@dataclass(frozen=True)
class HealthCheck:
    """Single health check result."""
    name: str
    status: HealthStatus
    message: str = ""
    value: Any = None


@dataclass
class NodeHealthReport:
    """Aggregate health report for a single node."""
    node_id: str
    status: HealthStatus
    checks: list[HealthCheck] = field(default_factory=list)
    sync_version: int = 0
    timestamp: str = ""

    def to_dict(self) -> dict[str, Any]:
        return {
            "node_id": self.node_id,
            "status": self.status.value,
            "sync_version": self.sync_version,
            "timestamp": self.timestamp,
            "checks": [
                {"name": c.name, "status": c.status.value, "message": c.message}
                for c in self.checks
            ],
        }


@dataclass
class ClusterHealthReport:
    """Aggregate health of the entire cluster."""
    hub_node_id: str
    status: HealthStatus
    total_nodes: int = 0
    healthy_nodes: int = 0
    degraded_nodes: int = 0
    unhealthy_nodes: int = 0
    max_sync_lag: int = 0
    node_reports: list[NodeHealthReport] = field(default_factory=list)
    timestamp: str = ""

    def to_dict(self) -> dict[str, Any]:
        return {
            "hub_node_id": self.hub_node_id,
            "status": self.status.value,
            "total_nodes": self.total_nodes,
            "healthy": self.healthy_nodes,
            "degraded": self.degraded_nodes,
            "unhealthy": self.unhealthy_nodes,
            "max_sync_lag": self.max_sync_lag,
            "timestamp": self.timestamp,
            "nodes": [n.to_dict() for n in self.node_reports],
        }


# --- Health Checker ---

# Thresholds
MIN_DISK_MB = 100  # Warn below 100MB free
CERT_EXPIRY_WARNING_DAYS = 14  # Warn if cert expires within 2 weeks
MAX_SYNC_LAG_WARNING = 100  # Warn if spoke is >100 versions behind hub


class HealthChecker:
    """Runs health checks on a GreyMatter node."""

    def __init__(self, db, *, db_path: str | Path | None = None):
        """
        Args:
            db: Database instance (SQLiteDatabase or CockroachDatabase).
            db_path: Path to the SQLite database file (for disk checks).
                     Not needed for CockroachDB.
        """
        self._db = db
        self._db_path = Path(db_path) if db_path else None

    def check_node(self) -> NodeHealthReport:
        """Run all node-level health checks."""
        checks = []

        checks.append(self._check_db_accessible())
        checks.append(self._check_db_writable())
        checks.append(self._check_sync_state())

        if self._db_path:
            checks.append(self._check_disk_space())

        # Determine overall status (worst of all checks)
        statuses = [c.status for c in checks]
        if HealthStatus.UNHEALTHY in statuses:
            overall = HealthStatus.UNHEALTHY
        elif HealthStatus.DEGRADED in statuses:
            overall = HealthStatus.DEGRADED
        else:
            overall = HealthStatus.HEALTHY

        # Get node info
        node_id = ""
        sync_version = 0
        try:
            from greymatter_core.sync.tracking import get_node_id, get_sync_version
            node_id = get_node_id(self._db)
            sync_version = get_sync_version(self._db)
        except Exception:
            pass

        return NodeHealthReport(
            node_id=node_id,
            status=overall,
            checks=checks,
            sync_version=sync_version,
            timestamp=datetime.now(timezone.utc).isoformat(),
        )

    def _check_db_accessible(self) -> HealthCheck:
        """Can we read from the database?"""
        try:
            self._db.execute_one("SELECT 1 AS ok")
            return HealthCheck("db_accessible", HealthStatus.HEALTHY)
        except Exception as e:
            return HealthCheck(
                "db_accessible", HealthStatus.UNHEALTHY,
                message=f"Database not accessible: {e}",
            )

    def _check_db_writable(self) -> HealthCheck:
        """Can we write to the database?"""
        try:
            # Try updating _sync_state (a safe idempotent write)
            self._db.execute(
                "UPDATE _sync_state SET value = value WHERE key = 'node_id'"
            )
            return HealthCheck("db_writable", HealthStatus.HEALTHY)
        except Exception as e:
            return HealthCheck(
                "db_writable", HealthStatus.UNHEALTHY,
                message=f"Database not writable: {e}",
            )

    def _check_sync_state(self) -> HealthCheck:
        """Is sync state properly initialized?"""
        try:
            from greymatter_core.sync.tracking import get_node_id, get_sync_version
            node_id = get_node_id(self._db)
            version = get_sync_version(self._db)

            if not node_id:
                return HealthCheck(
                    "sync_state", HealthStatus.DEGRADED,
                    message="No node_id set in _sync_state",
                )

            return HealthCheck(
                "sync_state", HealthStatus.HEALTHY,
                message=f"node={node_id}, version={version}",
                value={"node_id": node_id, "sync_version": version},
            )
        except Exception as e:
            return HealthCheck(
                "sync_state", HealthStatus.DEGRADED,
                message=f"Sync state not initialized: {e}",
            )

    def _check_disk_space(self) -> HealthCheck:
        """Is there enough disk space for the database?"""
        try:
            if not self._db_path or not self._db_path.exists():
                return HealthCheck(
                    "disk_space", HealthStatus.HEALTHY,
                    message="In-memory database",
                )

            stat = os.statvfs(self._db_path.parent)
            free_mb = (stat.f_bavail * stat.f_frsize) / (1024 * 1024)

            if free_mb < MIN_DISK_MB:
                return HealthCheck(
                    "disk_space", HealthStatus.UNHEALTHY,
                    message=f"Only {free_mb:.0f}MB free (minimum: {MIN_DISK_MB}MB)",
                    value=free_mb,
                )

            return HealthCheck(
                "disk_space", HealthStatus.HEALTHY,
                message=f"{free_mb:.0f}MB free",
                value=free_mb,
            )
        except Exception as e:
            return HealthCheck(
                "disk_space", HealthStatus.DEGRADED,
                message=f"Cannot check disk space: {e}",
            )


def check_cluster_health(
    hub_report: NodeHealthReport,
    spoke_reports: list[NodeHealthReport],
) -> ClusterHealthReport:
    """Aggregate spoke health reports into a cluster-level view.

    Args:
        hub_report: Health report from the hub node.
        spoke_reports: Health reports received from spoke nodes.

    Returns:
        ClusterHealthReport with overall cluster status.
    """
    all_reports = [hub_report] + spoke_reports
    hub_version = hub_report.sync_version

    healthy = sum(1 for r in all_reports if r.status == HealthStatus.HEALTHY)
    degraded = sum(1 for r in all_reports if r.status == HealthStatus.DEGRADED)
    unhealthy = sum(1 for r in all_reports if r.status == HealthStatus.UNHEALTHY)

    # Calculate max sync lag (how far behind hub the worst spoke is)
    max_lag = 0
    for spoke in spoke_reports:
        lag = compute_sync_lag(hub_version, spoke.sync_version)
        if lag > max_lag:
            max_lag = lag

    # Overall cluster status
    if unhealthy > 0 or hub_report.status == HealthStatus.UNHEALTHY:
        overall = HealthStatus.UNHEALTHY
    elif degraded > 0 or max_lag > MAX_SYNC_LAG_WARNING:
        overall = HealthStatus.DEGRADED
    else:
        overall = HealthStatus.HEALTHY

    return ClusterHealthReport(
        hub_node_id=hub_report.node_id,
        status=overall,
        total_nodes=len(all_reports),
        healthy_nodes=healthy,
        degraded_nodes=degraded,
        unhealthy_nodes=unhealthy,
        max_sync_lag=max_lag,
        node_reports=all_reports,
        timestamp=datetime.now(timezone.utc).isoformat(),
    )


def compute_sync_lag(hub_version: int, spoke_version: int) -> int:
    """Calculate how far behind a spoke is from the hub.

    Returns the version difference (0 = fully converged).
    Negative values mean spoke is ahead (shouldn't happen, but handled).
    """
    return max(0, hub_version - spoke_version)
