"""Node health checking for Citadel lifecycle management.

Evaluates Kubernetes node conditions (DiskPressure, MemoryPressure,
PIDPressure, Ready) and maps them to health states compatible with
Apollo's Witchcraft-style health model.

Also integrates custom probe results from the CustomProbeChecker
for detecting failure modes beyond standard K8s conditions (stuck
volumes, GPU errors, NVMe degradation, etc.).
"""

from __future__ import annotations

import logging
from dataclasses import dataclass
from enum import Enum

from citadel_operator.k3s.custom_probes import CustomProbeReport, ProbeResult
from citadel_operator.k3s.node import NodeInfo
from citadel_operator.k3s.probe_discovery import EntityHealth

logger = logging.getLogger(__name__)


class HealthState(Enum):
    """Node health states, ordered by severity.

    Compatible with Apollo's Witchcraft health model:
    - HEALTHY: Node is fully operational
    - WARNING: Node shows early signs of trouble
    - ERROR: Node is operationally unhealthy
    - TERMINAL: Node should be replaced immediately
    """

    HEALTHY = "healthy"
    WARNING = "warning"
    ERROR = "error"
    TERMINAL = "terminal"


@dataclass(frozen=True)
class NodeCondition:
    """A single evaluated node condition."""

    condition_type: str
    status: str
    healthy: bool
    message: str


@dataclass(frozen=True)
class NodeHealthReport:
    """Complete health assessment for a node."""

    node_name: str
    state: HealthState
    conditions: list[NodeCondition]
    age_hours: float
    max_age_hours: float
    age_exceeded: bool
    custom_probe_report: CustomProbeReport | None = None
    entity_health: list[EntityHealth] | None = None

    @property
    def needs_termination(self) -> bool:
        """Node should be terminated if aged out, in terminal state, or custom probes failed."""
        return (
            self.age_exceeded
            or self.state == HealthState.TERMINAL
            or self.has_failed_custom_probes
        )

    @property
    def has_failed_custom_probes(self) -> bool:
        """True if any custom probe has exceeded its failure threshold."""
        if self.custom_probe_report is None:
            return False
        return self.custom_probe_report.any_failed

    @property
    def failed_custom_probes(self) -> list[ProbeResult]:
        """Return the list of custom probes that have exceeded their threshold."""
        if self.custom_probe_report is None:
            return []
        return self.custom_probe_report.failed_probes

    @property
    def unhealthy_entities(self) -> list[EntityHealth]:
        """Return entities where at least one pod is unhealthy."""
        if self.entity_health is None:
            return []
        return [e for e in self.entity_health if not e.fully_healthy]


# Conditions where True indicates a problem
_PRESSURE_CONDITIONS = {"DiskPressure", "MemoryPressure", "PIDPressure"}


class HealthChecker:
    """Evaluates node health against Citadel lifecycle policies.

    This is the read-only assessment layer. It determines *what state*
    a node is in but does not take action. Apollo's policy engine
    consumes these reports to make termination decisions.

    When custom probe reports are provided, they are factored into the
    overall health state: any failed custom probe escalates the state
    to at least ERROR.
    """

    def __init__(self, max_age_hours: float = 48.0) -> None:
        self.max_age_hours = max_age_hours

    def check(
        self,
        node: NodeInfo,
        custom_probe_report: CustomProbeReport | None = None,
        entity_health: list[EntityHealth] | None = None,
    ) -> NodeHealthReport:
        """Produce a full health report for a node.

        Args:
            node: The node's lifecycle-relevant state.
            custom_probe_report: Optional results from custom probe execution.
                When provided, failed probes escalate the health state.
            entity_health: Optional aggregated health from Apollo probe
                discovery. Informational -- does not affect the computed
                health state (entity-level decisions are made by Apollo).
        """
        conditions = self._evaluate_conditions(node)
        age_exceeded = node.age_hours >= self.max_age_hours

        # Determine overall health state (including custom probes)
        state = self._compute_state(
            conditions, node, age_exceeded, custom_probe_report
        )

        return NodeHealthReport(
            node_name=node.name,
            state=state,
            conditions=conditions,
            age_hours=node.age_hours,
            max_age_hours=self.max_age_hours,
            age_exceeded=age_exceeded,
            custom_probe_report=custom_probe_report,
            entity_health=entity_health,
        )

    def check_all(
        self,
        nodes: list[NodeInfo],
        custom_probe_reports: dict[str, CustomProbeReport] | None = None,
        entity_health_by_node: dict[str, list[EntityHealth]] | None = None,
    ) -> list[NodeHealthReport]:
        """Produce health reports for all nodes.

        Args:
            nodes: List of node snapshots.
            custom_probe_reports: Optional dict mapping node names to their
                custom probe reports. Nodes without an entry are checked
                without custom probe data.
            entity_health_by_node: Optional dict mapping node names to their
                discovered entity health data from Apollo probe discovery.
        """
        reports = custom_probe_reports or {}
        entity_health = entity_health_by_node or {}
        return [
            self.check(
                n,
                reports.get(n.name),
                entity_health.get(n.name),
            )
            for n in nodes
        ]

    def _evaluate_conditions(self, node: NodeInfo) -> list[NodeCondition]:
        """Evaluate each node condition."""
        results: list[NodeCondition] = []

        # Check Ready condition
        ready_status = node.conditions.get("Ready", "Unknown")
        results.append(
            NodeCondition(
                condition_type="Ready",
                status=ready_status,
                healthy=ready_status == "True",
                message="Node is ready"
                if ready_status == "True"
                else f"Node not ready (status={ready_status})",
            )
        )

        # Check pressure conditions (True = bad)
        for cond_type in _PRESSURE_CONDITIONS:
            status = node.conditions.get(cond_type, "False")
            is_healthy = status != "True"
            results.append(
                NodeCondition(
                    condition_type=cond_type,
                    status=status,
                    healthy=is_healthy,
                    message=f"{cond_type} is nominal"
                    if is_healthy
                    else f"{cond_type} detected",
                )
            )

        return results

    def _compute_state(
        self,
        conditions: list[NodeCondition],
        node: NodeInfo,
        age_exceeded: bool,
        custom_probe_report: CustomProbeReport | None = None,
    ) -> HealthState:
        """Compute the aggregate health state.

        Custom probe failures escalate the state to at least ERROR,
        since they represent hardware/infrastructure-level problems
        that K8s conditions do not capture.
        """
        not_ready = any(
            not c.healthy for c in conditions if c.condition_type == "Ready"
        )
        has_pressure = any(
            not c.healthy
            for c in conditions
            if c.condition_type in _PRESSURE_CONDITIONS
        )

        # Terminal: not ready (kubelet is down or node is unreachable)
        if not_ready and not node.schedulable:
            return HealthState.TERMINAL

        # Error: not ready but still schedulable, or multiple pressures
        if not_ready:
            return HealthState.ERROR

        pressure_count = sum(
            1
            for c in conditions
            if c.condition_type in _PRESSURE_CONDITIONS and not c.healthy
        )
        if pressure_count >= 2:
            return HealthState.ERROR

        # Error: any custom probe has exceeded its failure threshold
        if custom_probe_report is not None and custom_probe_report.any_failed:
            failed = custom_probe_report.failed_probes
            probe_names = ", ".join(p.probe_name for p in failed)
            logger.warning(
                "Node %s has %d failed custom probe(s): %s",
                node.name,
                len(failed),
                probe_names,
            )
            return HealthState.ERROR

        # Warning: single pressure or approaching max age (>80%)
        if has_pressure:
            return HealthState.WARNING
        if node.age_hours >= self.max_age_hours * 0.8:
            return HealthState.WARNING

        return HealthState.HEALTHY
