"""Medium-agnostic formatting helpers (PR-06 reference implementation).

This module is the **only** intentional concession the SDK makes toward
"presentation" concerns. Every helper here is a small, pure-Python
function whose contract is ``primitive in → str out``. The point is to
give every UI consumer (the in-tree CLI today; tomorrow's
hrb-app/ui/qt + hrb-app/ui/cli + hypothetical web/TUI adapters) a
single source of truth for the formatting decisions that humans expect
to look the same everywhere — uptime as ``"Hh Mm Ss"``, durations as
``"4.5ms"`` / ``"1.2s"``, byte sizes as ``"42.3 MiB"``, severity
indicators as a stable 5-char glyph.

Why this module instead of free-floating functions:

* **Single-source-of-truth.** Pre-fmt, each UI invented its own
  uptime formatter. A GUI showing "1h 2m" while the CLI showed "62m"
  for the same broker is a real bug class. Centralising eliminates
  it by construction.
* **Discoverable surface.** Consumers can ``from mgf.hrb.fmt import
  format_uptime`` instead of grepping for "uptime" across modules.
* **Forward-compatible.** Adding a new helper here (e.g.
  ``format_audit_outcome_color`` once GUIs want palette hints) is
  additive — won't break the existing CLI.

Strict invariants (enforced by ``.importlinter`` — see PR-06):

* **Stdlib only.** No ``import PySide6``, no ``import rich``, no
  ``mgf.common``. The SDK cannot drag UI deps in through fmt.
* **Pure functions.** No I/O, no state, no logging. Every call is
  deterministic for its inputs.
* **Bounded output.** Every helper returns a short, finite string.
  No unbounded growth as input grows (durations / bytes saturate
  at higher units). ASCII-only — no control chars, no
  ANSI escapes, no locale-dependent decimal separators.
* **Thread-safe / re-entrant.** No module-level mutable state; no
  caches; no lazy-init globals. Safe to call concurrently from any
  number of threads / async tasks without coordination. Adding any
  per-call cache here would break this invariant — don't.
* **Strict input types.** The three numeric helpers
  (``format_uptime`` / ``format_duration`` / ``format_rss``) reject
  ``bool`` (always a programmer bug — broker never emits booleans
  as a numeric quantity) and ``float`` (wire format is int; a float
  at this boundary is a translation defect) with ``TypeError``.
  Negative ints clamp to ``0`` (defensive — broker shouldn't emit
  negative, but don't crash). ``format_severity_glyph`` keeps the
  ``"  ?  "`` defensive fallback for unknown values.
* **Reference implementation of Magogi Foundation rule PR-06.** The
  formal rule lives in ``mgf-common/docs/standards/PRESENTATION_LAYER.md``;
  this file is what "compliant SDK fmt" looks like.
"""

from __future__ import annotations

from typing import TYPE_CHECKING

if TYPE_CHECKING:
    # Import only for type-checking so we don't create a runtime
    # cycle (diagnostics imports from us nowhere, but defensively
    # gate the import).
    from mgf.hrb.diagnostics import Severity


# ---------------------------------------------------------------------------
# Severity glyph
# ---------------------------------------------------------------------------

# Canonical 5-character glyph for each severity. Width is fixed at 5
# so a column of glyphs aligns in any monospace renderer (CLI, log
# viewers, TUI). GUIs that want their own visuals (Qt icons, web SVGs)
# do their own mapping — these glyphs are the TEXT consensus.
#
# The 5-char shape predates PR-06 (it's how the CLI's `_doctor.py`
# rendered findings); preserving it here means every consumer's
# text-mode view stays visually identical post-extraction.
_SEVERITY_GLYPHS: dict[str, str] = {
    "ok": "  OK ",
    "warn": " WARN",
    "fail": " FAIL",
    "deferred": " ... ",
}


def format_severity_glyph(severity: Severity | str) -> str:
    """Return the canonical 5-character glyph for a Severity value.

    Accepts either a :class:`mgf.hrb.diagnostics.Severity` enum
    member or its underlying string value (so callers that received
    a wire-shape ``"ok"`` / ``"warn"`` etc. don't need to enum-wrap
    before formatting). Unknown values render as ``"  ?  "`` —
    honest "I don't know how to show this" rather than crashing or
    falsely picking a wrong glyph.

    Examples:
        >>> format_severity_glyph("ok")
        '  OK '
        >>> format_severity_glyph("fail")
        ' FAIL'
        >>> format_severity_glyph("unknown_new_severity")
        '  ?  '
    """
    # Coerce enum to its value if needed — StrEnum members compare
    # equal to their string but iteration / dict-key forms benefit
    # from explicit conversion.
    key = str(severity.value if hasattr(severity, "value") else severity)
    return _SEVERITY_GLYPHS.get(key, "  ?  ")


# ---------------------------------------------------------------------------
# Uptime — milliseconds → "Hh Mm Ss" / "Mm Ss" / "Ss"
# ---------------------------------------------------------------------------


def format_uptime(uptime_ms: int) -> str:
    """Format a millisecond count as a compact uptime string.

    Truncates to whole seconds (sub-second uptimes are uninteresting
    for a daemon; "0s" is the right answer for the first 999ms). The
    output omits leading zero components: a 30-second uptime is
    ``"30s"``, a 2-minute one is ``"2m 0s"``, a 3-hour one is
    ``"3h 0m 0s"``. The "Hh Mm Ss" shape was already established by
    `mgf-hrb doctor` and is preserved here so the post-extraction CLI
    output stays visually identical.

    Negative values clamp to 0 (broker shouldn't emit negative
    uptime; clamp prevents crash on a malformed response). ``bool``
    and ``float`` raise ``TypeError`` — a bool here is always a
    programmer bug and a float means an upstream translation
    defect; both should fail loud rather than silently produce
    nonsense.

    Examples:
        >>> format_uptime(500)
        '0s'
        >>> format_uptime(30_000)
        '30s'
        >>> format_uptime(125_000)
        '2m 5s'
        >>> format_uptime(3_725_000)
        '1h 2m 5s'
        >>> format_uptime(-1)
        '0s'
    """
    # bool first — bool IS int subclass, so isinstance(x, int)
    # returns True for True/False. Check bool explicitly so we
    # don't silently coerce True→1 into "0s".
    if isinstance(uptime_ms, bool):
        raise TypeError(f"format_uptime expects int, got bool ({uptime_ms!r})")
    if not isinstance(uptime_ms, int):
        raise TypeError(
            f"format_uptime expects int, got {type(uptime_ms).__name__} ({uptime_ms!r})"
        )
    total_seconds = max(0, uptime_ms) // 1000
    hours, rem = divmod(total_seconds, 3600)
    minutes, seconds = divmod(rem, 60)
    if hours:
        return f"{hours}h {minutes}m {seconds}s"
    if minutes:
        return f"{minutes}m {seconds}s"
    return f"{seconds}s"


# ---------------------------------------------------------------------------
# Duration — microseconds → "123µs" / "4.5ms" / "1.2s" / "5m12s"
# ---------------------------------------------------------------------------


def format_duration(duration_us: int) -> str:
    """Format a microsecond count as a compact duration string.

    Picks the LARGEST unit that keeps the integer part to ≤4 digits
    so the rendered width stays bounded (~6 chars worst-case). Units
    cascade: µs → ms → s → m+s. Sub-second precision uses one decimal
    place so ``999_000`` (= 999ms) doesn't round into "1s".

    Used by AuditRecord.duration_human and any other "how long did
    this take?" display. Audit records carry microseconds (the
    broker measures via ``kabi.nowMs()`` then converts); ms / s / m
    scales are the operator-facing brackets that matter.

    Negative values clamp to 0. ``bool`` and ``float`` raise
    ``TypeError`` (same shape as ``format_uptime``).

    Examples:
        >>> format_duration(500)
        '500µs'
        >>> format_duration(4_500)
        '4.5ms'
        >>> format_duration(999_000)
        '999.0ms'
        >>> format_duration(1_200_000)
        '1.2s'
        >>> format_duration(312_000_000)
        '5m12s'
        >>> format_duration(-1)
        '0µs'
    """
    if isinstance(duration_us, bool):
        raise TypeError(f"format_duration expects int, got bool ({duration_us!r})")
    if not isinstance(duration_us, int):
        raise TypeError(
            f"format_duration expects int, got {type(duration_us).__name__} ({duration_us!r})"
        )
    us = max(0, duration_us)

    # µs band: 0..999 µs.
    if us < 1_000:
        return f"{us}µs"

    # ms band: 1..999 ms (with one decimal).
    if us < 1_000_000:
        ms = us / 1_000.0
        return f"{ms:.1f}ms"

    # s band: 1..59 s (with one decimal).
    if us < 60_000_000:
        s = us / 1_000_000.0
        return f"{s:.1f}s"

    # m+s band: ≥1 minute. "Xm Ys" — no decimals at this scale; an
    # operator looking at a multi-minute audit record cares about
    # whole-second resolution at most.
    total_seconds = us // 1_000_000
    minutes, seconds = divmod(total_seconds, 60)
    return f"{minutes}m{seconds}s"


# ---------------------------------------------------------------------------
# RSS / byte size — bytes → "42.3 MiB" / "1.2 GiB"
# ---------------------------------------------------------------------------


def format_rss(num_bytes: int) -> str:
    """Format a byte count as a binary-prefixed size string.

    Uses BINARY (1024-based) prefixes: ``KiB`` / ``MiB`` / ``GiB`` /
    ``TiB``. Memory consumption is always reported by Linux in
    binary units (``/proc/<pid>/status`` reports `VmRSS: NNNN kB`
    but the `kB` actually means KiB per Linux convention), so
    matching the convention prevents the classic "42 MiB on screen,
    44 MB on `free -h`" operator confusion.

    Picks the LARGEST unit that keeps the integer part to ≤4 digits.
    Sub-KiB renders as a bare byte count.

    Negative values clamp to 0. ``bool`` and ``float`` raise
    ``TypeError`` (same shape as ``format_uptime`` /
    ``format_duration``).

    Examples:
        >>> format_rss(512)
        '512 B'
        >>> format_rss(1_500)
        '1.5 KiB'
        >>> format_rss(44_400_000)
        '42.3 MiB'
        >>> format_rss(1_500_000_000)
        '1.4 GiB'
        >>> format_rss(-1)
        '0 B'
    """
    if isinstance(num_bytes, bool):
        raise TypeError(f"format_rss expects int, got bool ({num_bytes!r})")
    if not isinstance(num_bytes, int):
        raise TypeError(f"format_rss expects int, got {type(num_bytes).__name__} ({num_bytes!r})")
    b = max(0, num_bytes)

    if b < 1024:
        return f"{b} B"
    units = ("KiB", "MiB", "GiB", "TiB", "PiB")
    value = float(b) / 1024.0
    for unit in units:
        if value < 1024.0 or unit == units[-1]:
            return f"{value:.1f} {unit}"
        value /= 1024.0
    # Unreachable — the last-unit branch above always returns.
    return f"{value:.1f} {units[-1]}"
