# FILE-SIZE-OK: health check probes are cohesive
"""Health check implementations for opendeviationbar-py.

GitHub Issue: https://github.com/terrylica/opendeviationbar-py/issues/109
"""

from __future__ import annotations

import logging
import subprocess
from typing import TYPE_CHECKING

import clickhouse_connect
import psutil
from clickhouse_connect.driver.exceptions import DatabaseError

if TYPE_CHECKING:
    from opendeviationbar.config.network import NetworkConfig

logger = logging.getLogger(__name__)


def _net_cfg() -> NetworkConfig:
    from opendeviationbar.config.network import NetworkConfig
    return NetworkConfig()


def check_clickhouse(host: str = "localhost", port: int = 8123) -> bool:
    """Check ClickHouse connectivity.

    Parameters
    ----------
    host : str
        ClickHouse host address.
    port : int
        ClickHouse HTTP port.

    Returns
    -------
    bool
        True if ClickHouse is reachable and responds.
    """
    try:
        client = clickhouse_connect.get_client(host=host, port=port)
        result = client.query("SELECT 1")
    except (DatabaseError, OSError):
        logger.warning("ClickHouse health check failed", exc_info=True)
        return False
    else:
        return result.result_rows[0][0] == 1


def check_memory(threshold_mb: int = 3072) -> bool:
    """Check memory usage is below threshold.

    Parameters
    ----------
    threshold_mb : int
        Maximum allowed RSS in MB.

    Returns
    -------
    bool
        True if memory usage is below threshold.
    """
    try:
        process = psutil.Process()
        rss_mb = process.memory_info().rss / (1024 * 1024)
    except (psutil.NoSuchProcess, psutil.AccessDenied, OSError):
        logger.warning("Memory health check failed", exc_info=True)
        return False
    else:
        return rss_mb < threshold_mb


def check_disk_space(min_free_gb: float = 10.0) -> bool:
    """Check minimum free disk space.

    Parameters
    ----------
    min_free_gb : float
        Minimum required free space in GB.

    Returns
    -------
    bool
        True if sufficient free space exists.
    """
    try:
        usage = psutil.disk_usage("/")
        free_gb = usage.free / (1024**3)
    except OSError:
        logger.warning("Disk space health check failed", exc_info=True)
        return False
    else:
        return free_gb > min_free_gb


def check_websocket(service_name: str = "opendeviationbar-sidecar") -> bool:
    """Check if the opendeviationbar-sidecar service is running.

    Parameters
    ----------
    service_name : str
        Name of the systemd user service to check.

    Returns
    -------
    bool
        True if the service is active.
    """
    try:
        result = subprocess.run(
            ["systemctl", "--user", "is-active", service_name],
            capture_output=True,
            text=True,
            timeout=_net_cfg().http_probe_timeout_s,
            check=False,
        )
    except (FileNotFoundError, OSError):
        logger.warning("systemctl not available, checking process")
        return check_process_running(service_name)
    except subprocess.TimeoutExpired:
        logger.warning("WebSocket health check timeout", exc_info=True)
        return False
    else:
        is_active = result.returncode == 0 and result.stdout.strip() == "active"
        if not is_active:
            logger.warning(
                "WebSocket service '%s' is not active: %s",
                service_name,
                result.stdout.strip(),
            )
        return is_active


def check_process_running(process_name: str) -> bool:
    """Check if a process is running by name.

    Parameters
    ----------
    process_name : str
        Name of the process to check.

    Returns
    -------
    bool
        True if process is found running.
    """
    try:
        found = any(
            process_name in proc.info["name"] for proc in psutil.process_iter(["name"])
        )
    except (psutil.NoSuchProcess, psutil.AccessDenied, OSError):
        logger.warning("Process check failed", exc_info=True)
        return False
    else:
        return found


def run_all_checks(
    ch_host: str = "localhost",
    ch_port: int = 8123,
    memory_threshold_mb: int = 3072,
    min_free_gb: float = 10.0,
    service_name: str = "opendeviationbar-sidecar",
) -> dict[str, bool | str]:
    """Run all health checks and return results.

    Parameters
    ----------
    ch_host : str
        ClickHouse host.
    ch_port : int
        ClickHouse port.
    memory_threshold_mb : int
        Memory threshold in MB.
    min_free_gb : float
        Minimum free disk space in GB.
    service_name : str
        WebSocket service name.

    Returns
    -------
    dict[str, bool]
        Dictionary mapping check names to their results.
    """
    # Issue #108: Include circuit breaker state from fatal_cache_write
    circuit_breaker_state = "unknown"
    try:
        from opendeviationbar.orchestration.open_deviation_bars_cache import (
            get_fatal_write_breaker,
        )

        circuit_breaker_state = get_fatal_write_breaker().state.value
    except ImportError:
        pass

    return {
        "clickhouse": check_clickhouse(ch_host, ch_port),
        "memory": check_memory(memory_threshold_mb),
        "disk_space": check_disk_space(min_free_gb),
        "websocket": check_websocket(service_name),
        "circuit_breaker": circuit_breaker_state,
    }
