"""Collector: host metrics — memory (/proc/meminfo), disk (shutil.disk_usage).

stdlib-only, the same primitives ``disk_watchdog`` uses (D1). Every reader is
never-raise: a missing path / unreadable proc-file degrades to ``None`` (one
signal skipped), never a tick crash (D8). CPU "hung agent" liveness is computed
from the ``/metrics`` envelope (cpu_ticks), not here.
"""
from __future__ import annotations

import logging
import shutil

logger = logging.getLogger("watchdog.collectors.host")


def read_mem_used_pct(meminfo_path: str = "/proc/meminfo") -> float | None:
    """Host memory used-% from ``/proc/meminfo`` (``MemTotal`` / ``MemAvailable``).

    ``used_pct = (1 - MemAvailable/MemTotal) * 100``. Returns ``None`` on a
    missing file / unparseable content / non-Linux (never raises).
    """
    try:
        fields: dict[str, int] = {}
        with open(meminfo_path, "r") as f:
            for line in f:
                parts = line.split(":")
                if len(parts) != 2:
                    continue
                key = parts[0].strip()
                val = parts[1].strip().split()
                if val:
                    try:
                        fields[key] = int(val[0])  # value is in kB
                    except ValueError:
                        continue
        total = fields.get("MemTotal")
        avail = fields.get("MemAvailable")
        if not total or avail is None:
            return None
        used_pct = (1.0 - (avail / total)) * 100.0
        return round(used_pct, 1)
    except Exception as e:  # noqa: BLE001 - degrade one signal, keep the tick
        logger.warning("watchdog: cannot read memory: %s", e)
        return None


def read_disk_used_pct(path: str) -> float | None:
    """Disk used-% for one path via ``shutil.disk_usage`` (1:1 with disk_watchdog).

    Returns ``None`` if the path is missing / unreadable (never raises).
    """
    try:
        usage = shutil.disk_usage(path)
        total = int(usage.total)
        if total <= 0:
            return None
        return round(int(usage.used) / total * 100.0, 1)
    except Exception as e:  # noqa: BLE001 - skip this path, keep the tick
        logger.warning("watchdog: cannot measure disk %s: %s", path, e)
        return None


def max_disk_used_pct(paths: list[str]) -> tuple[str, float] | None:
    """The fullest of ``paths`` as ``(path, used_pct)`` — the worst-case ceiling.

    A path that cannot be measured is skipped; ``None`` if none could be read.
    """
    worst: tuple[str, float] | None = None
    for p in paths:
        pct = read_disk_used_pct(p)
        if pct is None:
            continue
        if worst is None or pct > worst[1]:
            worst = (p, pct)
    return worst
