"""Sidecar health checks for heartbeat monitoring (Issue #107, Issue #318)."""

from __future__ import annotations

import logging
import subprocess
import time
from pathlib import Path

logger = logging.getLogger("health-heartbeat")

_DEAD_LETTER_DIR = Path("/tmp/opendeviationbar-dead-letter")


def check_dead_letters(results: dict[str, bool | str]) -> None:
    """Count dead-letter files and report oldest age.

    Dead letters accumulate when ClickHouse writes fail. A growing pile
    indicates a persistent write problem. Charon replays them every 5 min,
    so a healthy system should have 0 files most of the time.
    """
    if not _DEAD_LETTER_DIR.exists():
        results["dead_letter_count"] = "0"
        return

    files = list(_DEAD_LETTER_DIR.glob("*.parquet"))
    results["dead_letter_count"] = str(len(files))

    if not files:
        return

    # Oldest file age
    oldest_mtime = min(f.stat().st_mtime for f in files)
    age_h = (time.time() - oldest_mtime) / 3600
    if age_h < 1:
        results["dead_letter_oldest"] = f"{age_h * 60:.0f}m"
    elif age_h < 24:
        results["dead_letter_oldest"] = f"{age_h:.1f}h"
    else:
        results["dead_letter_oldest"] = f"{age_h / 24:.1f}d"


def _classify_ws_log_line(line: str) -> str | None:
    """Classify a single sidecar log line for WS error type.

    Returns:
        "fatal_ban" if IP ban detected
        "fatal_429" if 429 rate limit detected
        "transient" if normal disconnect
        None if not a WS error line
    """
    if 'signal = "BAN_DETECTED"' in line or "IP banned" in line:
        return "fatal_ban"
    if "RateLimited" in line or ("429" in line and "terminal" in line.lower()):
        return "fatal_429"
    if 'signal = "DISCONNECT"' in line or "WebSocket disconnected, reconnecting" in line:
        return "transient"
    return None


def check_ws_errors(results: dict[str, bool | str], window_minutes: int = 10) -> None:
    """Classify recent WebSocket errors from sidecar journal logs.

    Scans the last ``window_minutes`` of sidecar logs for:
    - 429 / rate limit errors (FATAL -- means we're violating Binance limits)
    - IP ban detection (FATAL -- sidecar sleeping until ban expires)
    - Transient disconnects (NORMAL -- handled by auto-reconnection)

    Results keys:
        ws_errors_healthy: bool -- False if any fatal (429/ban) errors found
        ws_error_summary: str -- Human-readable summary
        ws_fatal_count: str -- Count of fatal errors (429 + ban)
        ws_transient_count: str -- Count of transient disconnects
    """
    results["ws_errors_healthy"] = True
    results["ws_error_summary"] = "clean"
    results["ws_fatal_count"] = "0"
    results["ws_transient_count"] = "0"

    try:
        cmd = [
            "journalctl",
            "--user-unit=opendeviationbar-sidecar.service",
            f"--since={window_minutes} min ago",
            "--no-pager",
            "-o",
            "cat",
        ]
        proc = subprocess.run(cmd, capture_output=True, text=True, timeout=10, check=False)
        if proc.returncode != 0:
            results["ws_error_summary"] = "journal unavailable"
            return

        lines = proc.stdout.splitlines()

        fatal_count = 0
        transient_count = 0
        ban_detected = False
        rate_limited = False

        for line in lines:
            classification = _classify_ws_log_line(line)
            if classification == "fatal_ban":
                fatal_count += 1
                ban_detected = True
            elif classification == "fatal_429":
                fatal_count += 1
                rate_limited = True
            elif classification == "transient":
                transient_count += 1

        results["ws_fatal_count"] = str(fatal_count)
        results["ws_transient_count"] = str(transient_count)

        if fatal_count > 0:
            results["ws_errors_healthy"] = False
            parts = []
            if ban_detected:
                parts.append("IP BAN")
            if rate_limited:
                parts.append("429 RATE LIMIT")
            results["ws_error_summary"] = (
                f"FATAL: {' + '.join(parts)} ({fatal_count} events in {window_minutes}min)"
            )
        elif transient_count > 0:
            results["ws_error_summary"] = (
                f"transient: {transient_count} reconnects in {window_minutes}min"
            )
        else:
            results["ws_error_summary"] = "clean"

    except (subprocess.TimeoutExpired, FileNotFoundError, OSError) as exc:
        logger.warning("check_ws_errors failed: %s", exc)
        results["ws_error_summary"] = f"check failed: {exc}"


def check_ch_writer_metrics(results: dict[str, bool | str]) -> None:
    """Check Rust CH writer metrics from sidecar /health endpoint (Issue #318).

    Queries the sidecar health endpoint for ch_writer_* metrics exposed by
    the Rust ClickHouse writer. If the writer is not active (bars_flushed=0
    and dead_letters=0), no keys are added to results.

    Dead letters from the Rust writer mean bars were lost -- same severity
    as Python dead letters (CRITICAL in config.py).
    """
    import json
    import urllib.request

    try:
        with urllib.request.urlopen("http://localhost:8081/health", timeout=5) as resp:
            data = json.loads(resp.read())
    except (OSError, json.JSONDecodeError, ValueError):
        return  # Sidecar not running or health endpoint unavailable

    metrics = data.get("metrics", {})
    bars_flushed = metrics.get("ch_writer_bars_flushed", 0)
    dead_letters = metrics.get("ch_writer_dead_letter_count", 0)

    if bars_flushed == 0 and dead_letters == 0:
        return  # Writer not active

    results["ch_writer_bars_flushed"] = str(bars_flushed)
    results["ch_writer_bars_failed"] = str(metrics.get("ch_writer_bars_failed", 0))
    results["ch_writer_flush_count"] = str(metrics.get("ch_writer_flush_count", 0))
    results["ch_writer_dead_letter_count"] = str(dead_letters)
    latency_us = metrics.get("ch_writer_last_flush_latency_us", 0)
    results["ch_writer_latency_ms"] = f"{latency_us / 1000:.1f}"
    results["ch_writer_dead_letters_ok"] = dead_letters == 0
