#!/usr/bin/env python3
"""jarvis-health — vérification complète de l'état de la machine Jarvis.

Sections : Système, CPU, Mémoire, Stockage, GPU/ROCm, Ollama,
          Hermes Gateway, Open WebUI, Home Assistant, UniFi, Réseau, Synthèse.
"""

import json
import os
import re
import socket
import subprocess
import sys
import urllib.request
from pathlib import Path

try:
    import psutil  # type: ignore[import-untyped]
except ImportError:
    print("Erreur : package 'psutil' requis.", file=sys.stderr)
    print("  pip3 install --user psutil", file=sys.stderr)
    sys.exit(1)


HERMES_HOME = Path(
    os.environ.get("HERMES_HOME", str(Path.home() / ".hermes"))
).expanduser()


# ═══ helpers couleurs et statut ═══

def color_text(text, code):
    if hasattr(sys.stdout, "isatty") and sys.stdout.isatty():
        return "\033[%dm%s\033[0m" % (code, text)
    return text


_STATUS_COLORS = {"OK": 32, "INFO": 94, "WARN": 33, "FAIL": 31}
_LEVEL_MAP = {"OK": 0, "INFO": 1, "WARN": 2, "FAIL": 3}


class HealthState(object):
    max_level = 0


_hs = HealthState()


def _emit(label, msg):
    lval = _LEVEL_MAP.get(label, 1)
    if label in ("WARN", "FAIL"):
        _hs.max_level = max(_hs.max_level, lval)
    elif label == "INFO":
        _hs.max_level = max(_hs.max_level, 1)
    return color_text("[%s]" % label, _STATUS_COLORS[label]) + " %s" % msg


def ok(msg):
    return _emit("OK", msg)

def warn(msg):
    return _emit("WARN", msg)

def info(msg):
    _hs.max_level = max(_hs.max_level, 1)
    t = color_text("[INFO]", _STATUS_COLORS["INFO"])
    return "%s %s" % (t, msg)

def fail(msg):
    _hs.max_level = max(_hs.max_level, 3)
    t = color_text("[FAIL]", _STATUS_COLORS["FAIL"])
    return "%s %s" % (t, msg)


# ═══ utilitaires commandes ═══

def run(cmd, timeout=10):
    try:
        r = subprocess.run(cmd, capture_output=True, text=True, timeout=timeout)
        return r.returncode == 0, r.stdout.strip()
    except Exception as e:
        return False, str(e)


def fmt_gb(b):
    return "%.1f Go" % (b / (1024 ** 3))


# ═══ temperature CPU ═══

def _cpu_temperature():
    """Retourne température CPU en °C ou None."""
    for hwmon in sorted(Path("/sys/class/hwmon").glob("hwmon*")):
        nm_file = hwmon / "name"
        if not nm_file.exists():
            continue
        nm = nm_file.read_text().strip()
        if nm != "k10temp":
            continue
        tf = hwmon / "temp1_input"
        if not tf.exists():
            continue
        try:
            return float(tf.read_text().strip()) / 1000.0
        except ValueError:
            return None

    for tz in sorted(Path("/sys/class/thermal").glob("thermal_zone*")):
        try:
            ttype = (tz / "type").read_text().strip().lower()
        except Exception:
            continue
        if any(k in ttype for k in ("cpu", "k10", "x86_pkg")):
            try:
                tv = float((tz / "temp").read_text().strip())
                return tv / 1000.0 if tv > 0 else None
            except ValueError:
                continue
    return None


# ═══ SECTION 1 : SYSTÈME ═══

def check_system(lines):
    ok_h, hostname = run(["hostname"])
    host = hostname or "inconnu"
    lines.append(ok("Hostname : %s" % host))

    try:
        env_file = Path("/etc/os-release")
        pretty_name = ""
        ver_id = ""
        for raw in env_file.read_text().splitlines():
            if "=" not in raw:
                continue
            k, v = raw.split("=", 1)
            v = v.strip("\"'")
            if k == "PRETTY_NAME":
                pretty_name = v
            elif k == "VERSION_ID":
                ver_id = v
        label = pretty_name or "Linux"
        if ver_id:
            label += " %s" % ver_id
        lines.append(ok("Distribution : %s" % label))
    except Exception as e:
        lines.append(fail("Distribution : non lisible (%s)" % e))

    ok_k, kernel = run(["uname", "-r"])
    vers = kernel or "inconnu"
    lines.append(ok("Kernel : %s" % vers))

    try:
        seconds = float(Path("/proc/uptime").read_text().split()[0])
        days = int(seconds // 86400)
        rem1 = seconds % 86400
        hrs = int(rem1 // 3600)
        rem2 = rem1 % 3600
        mins = int(rem2 // 60)
        if days:
            ut_str = "%dj %dh %dm" % (days, hrs, mins)
        elif hrs:
            ut_str = "%dh %dm" % (hrs, mins)
        else:
            ut_str = "%dm" % mins
        lines.append(ok("Uptime : %s" % ut_str))
    except Exception as e:
        lines.append(fail("Uptime non disponible (%s)" % e))


# ═══ SECTION 2 : CPU ═══

def check_cpu(lines):
    cpu_percent = psutil.cpu_percent(interval=1)
    cpu_count = psutil.cpu_count() or 0
    load_avg = os.getloadavg()

    if load_avg[0] > cpu_count * 2:
        base_line = ok("CPU : %.1f%% — %d cœurs" % (cpu_percent, cpu_count))
        lines.append(base_line + color_text(" \u26a0 charge élevée", 33))
    else:
        lines.append(ok("CPU : %.1f%% d'utilisation, %d cœurs" % (cpu_percent, cpu_count)))

    lines.append(info(
        "Charge moyenne (1/5/15 min) : %.2f, %.2f, %.2f" % tuple(load_avg)))

    ct = _cpu_temperature()
    if ct is not None:
        lines.append(ok("Température CPU : %.1f C degrés" % ct))
    else:
        lines.append(info(
            "Température CPU : N/A (aucun capteur CPU détecté)"))


# ═══ SECTION 3 : MEMOIRE ═══

def check_memory(lines):
    mem = psutil.virtual_memory()

    total_gb = fmt_gb(mem.total)
    used_gb = fmt_gb(mem.used)
    avail_gb = fmt_gb(mem.available)
    pct = mem.percent
    total_gb_num = mem.total / (1024 ** 3)

    msg = "Mémoire : %d%% utilisé — %s / %s (%s dispo)" % (
        pct, used_gb, total_gb, avail_gb)

    if pct > 90:
        lines.append(fail(msg))
    elif pct > 75:
        lines.append(warn(msg))
    else:
        lines.append(ok(msg))

    # Sur Strix Halo, une partie de la mémoire physique peut être
    # réservée au GPU et donc ne pas apparaître comme RAM Linux.
    if 28 <= total_gb_num <= 40:
        lines.append(info(
            "Mémoire unifiée CPU/GPU : %s visibles par Linux "
            "(une partie de la mémoire physique est réservée au GPU)"
            % total_gb))


# ═══ SECTION 4 : STOCKAGE ═══

def check_storage(lines):
    disk = psutil.disk_usage("/")
    total_gb = fmt_gb(disk.total)
    used_gb = fmt_gb(disk.used)
    free_gb = fmt_gb(disk.free)
    pct = disk.percent

    if pct > 90:
        lines.append(fail(
            "Stockage (/) : %d%% utilisé — %s / %s (%s libre)"
            % (pct, used_gb, total_gb, free_gb)))
    elif pct > 80:
        lines.append(warn(
            "Stockage (/) : %d%% utilisé — %s / %s (%s libre)"
            % (pct, used_gb, total_gb, free_gb)))
    else:
        lines.append(ok(
            "Stockage (/) : %d%% utilisé — %s / %s (%s libre)"
            % (pct, used_gb, total_gb, free_gb)))


# ═══ SECTION 5 : GPU / ROCm ═══

def check_gpu_rocm(lines):
    gpu_name = None
    gfx_name = None

    # 1. ROCm : source privilégiée
    ok_ri, out_ri = run(["rocminfo"], timeout=10)
    has_rocminfo = ok_ri and bool(out_ri)

    if has_rocminfo:
        marketing_names = []

        for raw in out_ri.splitlines():
            s = raw.strip()

            m = re.search(r"Marketing Name:\s*(.+)", s, re.I)
            if m:
                name = m.group(1).strip()
                if name and name.lower() not in ("unknown", "n/a"):
                    marketing_names.append(name)

            m = re.search(r"\b(gfx\d+)\b", s, re.I)
            if m:
                candidate = m.group(1).lower()
                if candidate.startswith("gfx"):
                    gfx_name = candidate

        for name in marketing_names:
            low = name.lower()
            if "radeon" in low or "ryzen ai" in low or "amd" in low:
                gpu_name = name
                break

    # 2. Les logs Ollama donnent souvent le nom marketing exact du GPU
    ok_j, out_j = run(
        ["journalctl", "-u", "ollama", "--no-pager", "-n", "300"],
        timeout=8)

    if ok_j and out_j:
        for raw in reversed(out_j.splitlines()):
            if not gpu_name:
                m = re.search(r'description="([^"]+)"', raw)
                if m and (
                    "radeon" in m.group(1).lower()
                    or "amd" in m.group(1).lower()
                ):
                    gpu_name = m.group(1).strip()

            if not gfx_name:
                m = re.search(r"\bcompute=(gfx\d+)\b", raw, re.I)
                if m:
                    gfx_name = m.group(1).lower()

            if gpu_name and gfx_name:
                break

    # 3. Fallback PCI
    if not gpu_name:
        ok_lp, out_lp = run(["lspci", "-nn"], timeout=5)
        if ok_lp and out_lp:
            for raw in out_lp.splitlines():
                low = raw.lower()
                if (
                    ("vga compatible controller" in low or
                     "3d controller" in low or
                     "display controller" in low)
                    and ("amd" in low or "ati" in low or "radeon" in low)
                ):
                    gpu_name = raw.split(":", 2)[-1].strip()
                    break

    # rocm-smi peut fonctionner partiellement sur un iGPU Strix Halo.
    ok_rs, out_rs = run(["rocm-smi"], timeout=10)
    has_rocm_smi = ok_rs and bool(out_rs)

    if has_rocminfo:
        if gfx_name:
            lines.append(ok("ROCm : disponible (%s)" % gfx_name))
        else:
            lines.append(ok("ROCm : disponible via rocminfo"))
    elif has_rocm_smi:
        lines.append(ok("ROCm : disponible via rocm-smi"))
    else:
        ok_ls, out_ls = run(["lsmod"], timeout=5)
        if ok_ls and "amdgpu" in (out_ls or ""):
            lines.append(info(
                "ROCm : module amdgpu chargé, outils ROCm non concluants"))
        else:
            lines.append(warn(
                "ROCm : aucun outil ROCm fonctionnel détecté"))

    if gpu_name:
        if gfx_name and gfx_name.lower() not in gpu_name.lower():
            lines.append(ok(
                "GPU : %s (%s)" % (gpu_name, gfx_name)))
        else:
            lines.append(ok("GPU : %s" % gpu_name))
    elif gfx_name:
        lines.append(ok("GPU AMD : %s" % gfx_name))
    else:
        lines.append(info(
            "GPU : nom marketing non déterminé"))

    # Utilisation GPU : ne donner une valeur que si rocm-smi la fournit
    # explicitement.
    gpu_usage = None

    ok_use, out_use = run(["rocm-smi", "--showuse"], timeout=8)
    if ok_use and out_use:
        patterns = [
            r"GPU use\s*\(%\)\s*:\s*([0-9.]+)",
            r"GPU use[^0-9]*([0-9.]+)\s*%",
            r"GPU.*?use.*?([0-9.]+)\s*%",
        ]

        for raw in out_use.splitlines():
            for pat in patterns:
                m = re.search(pat, raw, re.I)
                if m:
                    try:
                        gpu_usage = float(m.group(1))
                    except ValueError:
                        pass
                    break
            if gpu_usage is not None:
                break

    if gpu_usage is not None:
        lines.append(info(
            "Utilisation GPU : %.1f%%" % gpu_usage))
    else:
        lines.append(info(
            "Utilisation GPU : N/A "
            "(métrique fiable non disponible via rocm-smi)"))


# ═══ SECTION 6 : OLLAMA ═══

def check_ollama(lines):
    api_url = "http://127.0.0.1:11434"

    ok_svc, svc_out = run(
        ["systemctl", "is-active", "ollama"], timeout=5)

    svc_state = (svc_out or "").strip()

    if svc_state == "active":
        lines.append(ok("Ollama systemd : active"))
    elif svc_state:
        lines.append(warn(
            "Ollama systemd : %s" % svc_state))
    else:
        lines.append(warn(
            "Ollama systemd : état indéterminé"))

    running_models = None
    api_error = None

    # /api/ps donne les modèles réellement chargés ainsi que
    # context_length et size_vram.
    try:
        req = urllib.request.Request(
            api_url + "/api/ps",
            headers={"Accept": "application/json"})

        with urllib.request.urlopen(req, timeout=5) as resp:
            data = json.loads(resp.read())
            running_models = data.get("models", [])

    except Exception as e:
        api_error = str(e)

    if running_models is None:
        lines.append(fail(
            "Ollama API : inaccessible (%s)" % api_error))
        return

    lines.append(ok(
        "Ollama API : en ligne sur %s" % api_url))

    if not running_models:
        lines.append(warn(
            "Ollama : aucun modèle actuellement chargé"))
        return

    for model in running_models:
        name = model.get("name") or model.get("model") or "?"
        size = model.get("size") or 0
        size_vram = model.get("size_vram") or 0
        context = model.get("context_length")

        details = model.get("details") or {}
        quant = (
            details.get("quantization_level")
            or details.get("quantization")
            or "?"
        )

        processor = "N/A"

        if size and size_vram:
            gpu_pct = int(round((size_vram / size) * 100))
            gpu_pct = max(0, min(100, gpu_pct))

            if gpu_pct >= 99:
                processor = "100% GPU"
            elif gpu_pct <= 1:
                processor = "100% CPU"
            else:
                processor = "%d%% GPU / %d%% CPU" % (
                    gpu_pct, 100 - gpu_pct)

        parts = [
            "Modèle chargé : %s" % name,
            "taille %s" % fmt_gb(size),
            processor,
        ]

        if context:
            parts.append("contexte %s" % context)

        if quant != "?":
            parts.append("quantification %s" % quant)

        lines.append(info(" | ".join(parts)))


# ═══ SECTION 7 : HERMES GATEWAY ═══

def check_hermes_gateway(lines):
    port = 8642

    ok_gs, svc_out = run(["systemctl", "--user", "is-active",
                          "hermes-gateway.service"], timeout=5)
    svc_state = svc_out.strip() if (ok_gs and svc_out) else "inconnu"

    if svc_state == "active":
        lines.append(ok("Hermes Gateway (systemd) : active"))
    elif not svc_state:
        if ok_gs:
            lines.append(warn(
                "Hermes Gateway (systemd) : inactive"))
        else:
            lines.append(info(
                "Hermes Gateway (systemd) : impossible de déterminer l'état"))
    else:
        lines.append(warn(
            "Hermes Gateway (systemd) : %s" % svc_state))

    found_list = []  # [(host_str, port_int), ...]

    ok_ss, out_ss = run(["ss", "-tlnp"], timeout=5)
    if ok_ss and out_ss:
        for raw in out_ss.splitlines():
            parts = raw.split()
            for p in parts:
                found_port_match = False
                host_candidate = None

                if ":" in p and not p.startswith("Recv") and \
                   not p.startswith("Send"):
                    addr_parts = p.rsplit(":", 1)
                    if len(addr_parts) == 2 and addr_parts[1].isdigit():
                        if int(addr_parts[1]) == port:
                            host_candidate = addr_parts[0]
                            found_port_match = True

                if found_port_match and host_candidate:
                    ip_resolved = host_candidate.replace("*", "0.0.0.0")
                    if (ip_resolved, port) not in found_list:
                        found_list.append((ip_resolved, port))

    if found_list:
        for host_val, pt in found_list[:1]:
            desc = "0.0.0.0" if host_val == "0.0.0.0" else host_val
            lines.append(
                info("Adresse réelle détectée : %s:%d (ss)" % (desc, pt)))

            test_host = host_val if host_val != "0.0.0.0" else "127.0.0.1"
            try:
                with socket.create_connection((test_host, pt), timeout=3):
                    lines.append(
                        ok("Hermes Gateway API : connecté (%s:%d)" % (test_host, pt)))
                    return
            except ConnectionRefusedError:
                lines.append(fail(
                    "Hermes Gateway : écoute trouvée sur %s:%d mais connexion refusée"
                    % (desc, pt)))

    test_ok = False
    for probe_host in ["127.0.0.1", "::1"]:
        try:
            with socket.create_connection((probe_host, port), timeout=3):
                lines.append(
                    info("Port %d détecté en écoute (%s)" % (port, probe_host)))
                test_ok = True
        except (ConnectionRefusedError, OSError):
            continue

    if not found_list and not test_ok:
        lines.append(warn("Port %d : non détecté en écoute" % port))


# ═══ SECTION 8 : OPEN WEBUI (Docker) ═══

def check_open_webui(lines):
    ok_docker, _ = run(["docker", "version"], timeout=5)

    if not ok_docker:
        lines.append(warn(
            "Open WebUI : Docker indisponible"))
        return

    ok_state, state = run(
        ["docker", "inspect", "--format",
         "{{.State.Status}}", "open-webui"],
        timeout=5)

    if not ok_state or not state:
        lines.append(info(
            "Open WebUI : non installé"))
        return

    state = state.strip()

    if state == "running":
        lines.append(ok(
            "Open WebUI (conteneur) : running"))
    else:
        lines.append(warn(
            "Open WebUI (conteneur) : %s" % state))
        return

    ok_ports, ports_json = run(
        ["docker", "inspect", "--format",
         "{{json .NetworkSettings.Ports}}", "open-webui"],
        timeout=5)

    bindings = []

    if ok_ports and ports_json:
        try:
            ports = json.loads(ports_json)

            for container_port, host_bindings in ports.items():
                if not host_bindings:
                    continue

                for binding in host_bindings:
                    host_ip = binding.get("HostIp", "")
                    host_port = binding.get("HostPort", "")

                    if host_port:
                        bindings.append(
                            (container_port, host_ip, host_port))

        except Exception:
            pass

    if not bindings:
        lines.append(info(
            "Open WebUI : aucun port publié détecté"))
        return

    # Privilégier le port HTTP interne 8080.
    selected = None

    for binding in bindings:
        if binding[0].startswith("8080/"):
            selected = binding
            break

    if selected is None:
        selected = bindings[0]

    _, host_ip, host_port = selected

    display_ip = host_ip or "0.0.0.0"

    lines.append(info(
        "Port publié : %s:%s" % (
            display_ip, host_port)))

    # Pour un bind 0.0.0.0/::, tester via localhost.
    if host_ip in ("", "0.0.0.0", "::"):
        test_host = "127.0.0.1"
    else:
        test_host = host_ip

    try:
        url = "http://%s:%s/" % (test_host, host_port)
        req = urllib.request.Request(
            url, headers={"User-Agent": "jarvis-health/1.0"})

        with urllib.request.urlopen(req, timeout=5) as resp:
            if 200 <= resp.status < 400:
                lines.append(ok(
                    "Open WebUI HTTP : accessible (%s:%s)"
                    % (test_host, host_port)))
            else:
                lines.append(warn(
                    "Open WebUI HTTP : réponse %s"
                    % resp.status))

    except Exception as e:
        lines.append(warn(
            "Open WebUI HTTP : inaccessible (%s)" % e))


# ═══ SECTION 9 : HOME ASSISTANT ═══

def check_home_assistant(lines):
    env_path = HERMES_HOME / ".env"

    hass_url = None
    hass_token = None

    if env_path.exists():
        for raw in env_path.read_text().splitlines():
            s = raw.strip()

            if not s or s.startswith("#") or "=" not in s:
                continue

            key, value = s.split("=", 1)
            key = key.strip()
            value = value.strip().strip('"').strip("'")

            if key == "HASS_URL":
                hass_url = value
            elif key == "HASS_TOKEN":
                hass_token = value

    missing = []

    if not hass_url:
        missing.append("HASS_URL")

    if not hass_token:
        missing.append("HASS_TOKEN")

    if missing:
        lines.append(info(
            "Home Assistant : non configuré (%s)"
            % ", ".join(missing)))
        return

    base_url = hass_url.rstrip("/")
    api_url = base_url + "/api/"

    try:
        req = urllib.request.Request(
            api_url,
            headers={
                "Authorization": "Bearer " + hass_token,
                "Accept": "application/json",
            },
            method="GET")

        with urllib.request.urlopen(req, timeout=6) as resp:
            if resp.status == 200:
                lines.append(ok(
                    "Home Assistant : configuré et accessible"))
            else:
                lines.append(warn(
                    "Home Assistant : HTTP %d" % resp.status))

    except urllib.error.HTTPError as e:
        if e.code in (401, 403):
            lines.append(fail(
                "Home Assistant : authentification refusée "
                "(HTTP %d)" % e.code))
        else:
            lines.append(warn(
                "Home Assistant : erreur HTTP %d" % e.code))

    except urllib.error.URLError as e:
        lines.append(fail(
            "Home Assistant : inaccessible (%s)" % e.reason))

    except Exception as e:
        lines.append(fail(
            "Home Assistant : erreur de connexion (%s)" % e))


# ═══ SECTION 10 : UNIFI ═══

def check_unifi(lines):
    env_path = HERMES_HOME / ".env"
    unifi_url = None
    unifi_key_present = False

    if env_path.exists():
        for raw in env_path.read_text().splitlines():
            line_s = raw.strip()
            if not line_s or line_s.startswith("#") or "=" not in line_s:
                continue
            k, v = line_s.split("=", 1)
            k_clean = k.strip()
            v_clean = v.strip("\"'")
            if k_clean == "UNIFI_URL":
                unifi_url = v_clean
            elif k_clean == "UNIFI_API_KEY" and v_clean:
                unifi_key_present = True

    missing_uni = []
    if not unifi_url:
        missing_uni.append("UNIFI_URL")
    if not unifi_key_present:
        missing_uni.append("UNIFI_API_KEY")

    if missing_uni:
        lines.append(info(
            "UniFi : non configuré (manque %s)" % "/ /".join(missing_uni)))
        return

    script_path = (HERMES_HOME / "skills" / "infrastructure" /
                   "infrastructure-jarvis" / "scripts" / "unifi.py")

    if script_path.exists() and \
       (script_path.stat().st_mode & 0o111):
        cr_script = run(
            [sys.executable, str(script_path), "info"], timeout=15)
        if cr_script[0] and cr_script[1]:
            lines.append(ok("UniFi : accessible via unifi.py info"))
            return

    base_url_uni = unifi_url.rstrip("/")
    for ep in ("/proxy/network/integration/v1/info", "/api/v2/sites"):
        full_url = base_url_uni + ep
        try:
            req_u = urllib.request.Request(
                full_url, headers={"Accept": "application/json"},
                method="GET")
            with urllib.request.urlopen(req_u, timeout=8) as resp_s:
                if resp_s.status == 200:
                    lines.append(ok(
                        "UniFi : accessible (%s%s)" % (base_url_uni, ep)))
                    return
        except urllib.error.HTTPError as e:
            code_h = e.code
            if code_h in (401, 403):
                lines.append(warn(
                    "UniFi : URL %s répond (auth requise, HTTP %d)" % (base_url_uni, code_h)))
                return
        except urllib.error.URLError as e:
            reason_f = str(e.reason) if hasattr(e, "reason") else str(e)
            lines.append(fail(
                "UniFi : inaccessible (%s) (erreur réseau : %s)" % (full_url, reason_f)))
            return


# ═══ SECTION 11 : RÉSEAU ═══

def check_network(lines):
    excluded_prefixes = (
        "lo",
        "docker",
        "br-",
        "veth",
        "virbr",
    )

    addresses = []

    ok_ip, out_ip = run(
        ["ip", "-o", "-4", "addr", "show", "scope", "global"],
        timeout=5)

    if ok_ip and out_ip:
        for raw in out_ip.splitlines():
            parts = raw.split()

            if len(parts) < 4:
                continue

            iface = parts[1].split("@")[0]

            if iface.startswith(excluded_prefixes):
                continue

            try:
                inet_idx = parts.index("inet")
                ip_addr = parts[inet_idx + 1].split("/")[0]
            except (ValueError, IndexError):
                continue

            addresses.append((iface, ip_addr))

    if addresses:
        lines.append(info(
            "Adresses IPv4 : %s" % ", ".join(
                "%s (%s)" % (ip, iface)
                for iface, ip in addresses)))
    else:
        lines.append(warn(
            "Aucune adresse IPv4 principale détectée"))

    ok_rt, out_rt = run(
        ["ip", "-4", "route", "show", "default"],
        timeout=5)

    default_route = None

    if ok_rt and out_rt:
        for raw in out_rt.splitlines():
            if raw.strip().startswith("default"):
                default_route = raw.strip()
                break

    if default_route:
        lines.append(info(
            "Route par défaut : %s" % default_route))

        src_match = re.search(
            r"\bsrc\s+(\d+\.\d+\.\d+\.\d+)",
            default_route)

        dev_match = re.search(
            r"\bdev\s+(\S+)",
            default_route)

        if src_match:
            src_ip = src_match.group(1)
            dev = dev_match.group(1) if dev_match else "?"

            lines.append(ok(
                "Adresse LAN principale : %s (%s)"
                % (src_ip, dev)))

    else:
        lines.append(warn(
            "Pas de route par défaut détectée"))


# ═══ SECTION 12 : SYNTHÈSE ═══

def check_synthesis(all_lines_by_section):
    global _hs
    max_l = _hs.max_level

    all_fail_lines = []
    for sect_name, sect_lines in all_lines_by_section.items():
        for sl in sect_lines:
            if "[FAIL]" in sl:
                all_fail_lines.append(sl)

    has_critical = False
    ollama_api_ok = False
    gateway_api_ok = False

    for sect_name, sect_lines in all_lines_by_section.items():
        for sl in sect_lines:
            if "ollama" in sect_name.lower() and "en ligne" in sl.lower() \
               and "[OK]" in sl:
                ollama_api_ok = True
            if "Hermes Gateway" in sect_name and "connecté" in sl.lower() \
               and "[OK]" in sl:
                gateway_api_ok = True

    fail_count = len(all_fail_lines)

    if fail_count > 2:
        has_critical = True

    if not all_fail_lines and max_l <= 1:
        st_name = "OK"
        st_color = 32
    else:
        if fail_count >= 3 or has_critical:
            st_name = "CRITIQUE"
            st_color = 31
        elif fail_count > 0 or not all([ollama_api_ok, gateway_api_ok]):
            st_name = "DEGRADE"
            st_color = 33
        else:
            st_name = "OK"
            st_color = 32

    print("")
    border_w = 50
    print(color_text("=" * border_w, st_color))
    status_full = color_text("SANTÉ: %s" % st_name, st_color)
    if fail_count > 0 or (st_name == "DEGRADE"):
        reasons = []
        for sl in all_fail_lines:
            reasons.append(sl.strip())
        if not ollama_api_ok:
            reasons.append("  -> Ollama API inaccessible")
        if not gateway_api_ok:
            reasons.append("  -> Hermes Gateway API non testée/connectée")
        if reasons:
            print(color_text(("-- ANOMALIES :", st_color)) + " ".join(
                [":"] + [r for r in reasons]) + "")
    # Re-print the separator line for display
    pass

    return st_name, fail_count, ollama_api_ok, gateway_api_ok


# ═══ MAIN ═══

if __name__ == "__main__":
    print(color_text(
        "== [1]== JARVIS HEALTH CHECK\n", 36))

    all_by_section = {}
    current_lines = []

    def start_sec(title):
        global current_lines
        current_lines = []
        print("\n" + color_text("[%s]" % title, 95) + "\n")

    def end_sec(section_name):
        global all_by_section, current_lines
        all_by_section[section_name] = list(current_lines)
        for item in current_lines:
            print(item)
        current_lines = []

    # --- Sec 1 : Système ---
    start_sec("SYSTEME")
    check_system(current_lines)
    end_sec("System")

    # --- Sec 2 : CPU ---
    start_sec("CPU")
    check_cpu(current_lines)
    end_sec("CPU")

    # --- Sec 3 : Mémoire ---
    start_sec("MEMOIRE")
    check_memory(current_lines)
    end_sec("Memory")

    # --- Sec 4 : Stockage ---
    start_sec("STOCKAGE")
    check_storage(current_lines)
    end_sec("Storage")

    # --- Sec 5 : GPU / ROCm ---
    start_sec("GPU / ROCm")
    check_gpu_rocm(current_lines)
    end_sec("GPU/ROCm")

    # --- Sec 6 : Ollama ---
    start_sec("OLLAMA")
    check_ollama(current_lines)
    end_sec("Ollama")

    # --- Sec 7 : Hermes Gateway ---
    start_sec("HERMES GATEWAY")
    check_hermes_gateway(current_lines)
    end_sec("Hermes Gateway")

    # --- Sec 8 : Open WebUI ---
    start_sec("OPEN WEBUI")
    check_open_webui(current_lines)
    end_sec("Open WebUI")

    # --- Sec 9 : Home Assistant ---
    start_sec("HOME ASSISTANT")
    check_home_assistant(current_lines)
    end_sec("Home Assistant")

    # --- Sec 10 : UniFi ---
    start_sec("UNIFI")
    check_unifi(current_lines)
    end_sec("UniFi")

    # --- Sec 11 : Réseau ---
    start_sec("RESEAU")
    check_network(current_lines)
    end_sec("Network")

    # --- Sec 12 : Synthèse ---
    syn_name, syn_fail, syn_ollama, syn_hermes = \
        check_synthesis(all_by_section)

    border_w2 = 50
    syn_color = 32 if syn_name == "OK" else (31 if syn_name == "CRITIQUE" else 33)
    print(color_text("=" * border_w2, syn_color))
    print(color_text(
        "SANTÉ: %s" % syn_name,
        syn_color))
    print(color_text("=" * border_w2, syn_color))

    sys.exit(0 if syn_name == "OK" else 1)
