"""
vita.coaching — the presentation layer

Turns model numbers into something a lifter can read and act on:

  * classify()  — a proximity-to-failure reading (how close a set is to failure,
                  the RPE it maps to, what it means, and what to do next)
  * status()    — just the short label from classify() (kept for compatibility)
  * formatting helpers for the printed report blocks

This module is pure presentation. It holds no fatigue math (that lives in
vita.core) and no side effects beyond building strings.
"""

from __future__ import annotations

from dataclasses import dataclass
from typing import Optional

__all__ = [
    "classify",
    "status",
    "RIRClass",
    "set_output",
    "get_output",
    "render",
    "MODES",
]


# --------------------------------------------------------------------------
# proximity-to-failure classification
# --------------------------------------------------------------------------
@dataclass(frozen=True)
class RIRClass:
    """A proximity-to-failure reading for a set.

    Reps-in-reserve and RPE are two sides of the same coin (RPE = 10 - RIR), so
    we report both -- RIR is what the model estimates, RPE is what most coaches
    say out loud.
    """

    label: str  # short category, e.g. "Hard working set"
    rpe: float  # RPE equivalent, 1..10 (10 = failure)
    zone: str  # warm-up | light | working | maximal | failure | past failure
    note: str  # one line: what this set is
    advice: str  # one line: what to do about it

    def __str__(self) -> str:
        return f"{self.label} (RPE {self.rpe:g})"


def _rpe_from_rir(rir: float) -> float:
    """RPE = 10 - RIR, clamped to [1, 10] and rounded to the nearest 0.5."""
    rpe = max(1.0, min(10.0, 10.0 - rir))
    return round(rpe * 2) / 2.0


def classify(rir: float) -> RIRClass:
    """Classify a set by its reps-in-reserve.

    Thresholds follow the RIR/RPE proximity-to-failure framework: failure at
    0 RIR (RPE 10), with each rep of reserve stepping the RPE down. Most
    productive working sets land in the 1-3 RIR band; 4+ RIR is closer to a
    warm-up than a growth stimulus.
    """
    rpe = _rpe_from_rir(rir)
    if rir < -0.5:
        return RIRClass(
            "Past failure",
            rpe,
            "past failure",
            "Beyond your predicted max for this load.",
            "Expect a grind or a missed rep — drop the weight or end the set.",
        )
    if rir < 0.5:
        return RIRClass(
            "Failure",
            rpe,
            "failure",
            "All-out — no reps left in the tank.",
            "Great as a finisher, but costly — don't stack many of these.",
        )
    if rir < 1.5:
        return RIRClass(
            "Near failure",
            rpe,
            "maximal",
            "About one rep from the limit.",
            "Strong stimulus — one or two sets like this per exercise is plenty.",
        )
    if rir < 2.5:
        return RIRClass(
            "Hard working set",
            rpe,
            "working",
            "Hard, with a rep or two held back.",
            "The sweet spot for most working sets — hold here.",
        )
    if rir < 3.5:
        return RIRClass(
            "Moderate working set",
            rpe,
            "working",
            "Productive, with a little left in reserve.",
            "Sustainable across sets — add load if you want more stimulus.",
        )
    if rir < 4.5:
        return RIRClass(
            "Easy working set",
            rpe,
            "light",
            "On the light side for a working set.",
            "Several reps in reserve — consider a bit more weight.",
        )
    return RIRClass(
        "Warm-up",
        rpe,
        "warm-up",
        "Well short of failure.",
        "Good for warming up; too easy to drive much growth on its own.",
    )


def _status(rir: float) -> str:
    """Short proximity-to-failure label (the classify() label)."""
    return classify(rir).label


# public alias (the underscore name is kept for backward compatibility)
status = _status


# --------------------------------------------------------------------------
# small formatting helpers
# --------------------------------------------------------------------------
def _pct(x: float) -> str:
    return f"{round(x * 100)}%"


def _lb(x: Optional[float]) -> str:
    return "bodyweight" if x is None else f"{round(x)} lb"


def _leader(label: str, value: str, width: int = 40) -> str:
    """A dotted-leader line:  'Weight ........... 225 lb'."""
    base = f"{label} "
    pad = width - len(base) - len(value)
    if pad < 1:
        pad = 1
    return "  " + base + "." * pad + " " + value


def _rule(width: int = 42) -> str:
    return "  " + "-" * width


# --------------------------------------------------------------------------
# output modes — how result objects display themselves
# --------------------------------------------------------------------------
# Controls what you see when a VITA result is echoed on its own (in a REPL or
# notebook). print(obj) always shows the full summary, regardless of mode.
#
#   quiet      one concise line
#   summary    the coaching summary block            (default)
#   verbose    the summary plus extra detail
#   developer  the raw object with full-precision numbers (old behavior)
MODES = ("quiet", "summary", "verbose", "developer")
_MODE = "summary"


def set_output(mode: str) -> None:
    """Set the global output mode (one of: quiet, summary, verbose, developer)."""
    global _MODE
    if mode not in MODES:
        raise ValueError(f"mode must be one of {MODES}, got {mode!r}")
    _MODE = mode


def get_output() -> str:
    """The current global output mode."""
    return _MODE


def _developer_repr(obj) -> str:
    """Raw, full-precision dump of a result object's public fields."""
    fields = {k: v for k, v in vars(obj).items() if not k.startswith("_")}
    inner = ", ".join(f"{k}={v!r}" for k, v in fields.items())
    return f"{type(obj).__name__}({inner})"


def render(obj, mode: Optional[str] = None) -> str:
    """Render a result object according to the active output mode.

    The object must provide ._oneline(), .summary(), and .verbose().
    """
    mode = mode or _MODE
    if mode == "quiet":
        return obj._oneline()
    if mode == "verbose":
        return obj.verbose()
    if mode == "developer":
        return _developer_repr(obj)
    return obj.summary()
