"""Follow-live — the In-progress lens' live activity view (spec §4.9, M6).

A traced op streams fed-session events to the target's ``live/sessions/<id>.ndjson``
(append-only, ephemeral, **gitignored, never gated**). This composes the *current*
session (or a named past one) into a view-model: current step, live progress, the
last-event age, the latest log line, and the full session timeline — each value
**derived** from the stream, never asserted. ``live/`` is telemetry ("what's
happening"), categorically separate from the gated ``generated/`` run-records ("what
happened"), so this read is outside the freshness gate by construction (D-LIVE-1:
follow-only for v1 — a snapshot per request, no watch daemon).

Absence is first-class: no ``live/CURRENT`` → an honest "nothing running", never a
fabricated or blank pane; an expired/unreadable session says so.
"""

from __future__ import annotations

import json
from datetime import UTC, datetime
from typing import TYPE_CHECKING, Any

from mgf.fedres import current_session, read_session

if TYPE_CHECKING:
    from mgf.fedres import DiskProvider

_TS = "%Y-%m-%dT%H:%M:%SZ"
_ENVELOPE = ("v", "session", "seq", "ts", "kind")
_LEVEL_CLASS = {
    "error": "high",
    "critical": "high",
    "warn": "medium",
    "warning": "medium",
    "info": "low",
    "debug": "effort",
}


def _parse(ts: Any) -> datetime | None:
    if not isinstance(ts, str):
        return None
    try:
        return datetime.strptime(ts, _TS).replace(tzinfo=UTC)
    except ValueError:
        return None


def _humanize(seconds: float) -> str:
    """A derived, coarse age string (``12s`` / ``3m`` / ``1h`` / ``2d``); ``—`` if unknown."""
    if seconds < 0:
        return "—"
    if seconds < 60:
        return f"{int(seconds)}s"
    if seconds < 3600:
        return f"{int(seconds // 60)}m"
    if seconds < 86400:
        return f"{int(seconds // 3600)}h"
    return f"{int(seconds // 86400)}d"


def _pct(value: Any) -> int | None:
    """Normalize a progress telemetry value to a 0..100 int (``0.6`` → 60, ``42`` → 42)."""
    if isinstance(value, bool) or not isinstance(value, int | float):
        return None
    return round(value * 100) if value <= 1 else round(value)


def _last(events: list[dict[str, Any]], kind: str) -> dict[str, Any] | None:
    return next((e for e in reversed(events) if e.get("kind") == kind), None)


def _event_text(event: dict[str, Any]) -> str:
    """One honest line for the timeline — the event's payload, by kind."""
    kind = event.get("kind")
    if kind == "session_start":
        text = f"started {event.get('op', 'session')}"
        return f"{text} · {event['task']}" if event.get("task") else text
    if kind == "session_end":
        return f"ended {event.get('result', '?')} in {event.get('duration_s', '?')}s"
    if kind == "progress":
        pct = _pct(event.get("progress"))
        return f"{pct}%" if pct is not None else "progress"
    if kind == "step":
        return str(event.get("name") or event.get("label") or event.get("msg") or "step")
    if kind in ("log", "error"):
        return str(event.get("msg") or "")
    # An unrecognized kind is shown, not dropped (honest-absence's inverse: honest-presence).
    extra = {k: v for k, v in event.items() if k not in _ENVELOPE}
    return json.dumps(extra, ensure_ascii=False) if extra else str(kind)


def _timeline_row(event: dict[str, Any]) -> dict[str, Any]:
    kind = str(event.get("kind", "log"))
    ts = event.get("ts") or ""
    return {
        "seq": event.get("seq") or 0,
        "ts": ts,
        # HH:MM:SS off the ISO stamp — the timeline shows wall-clock, not the full ts.
        "clock": ts[11:19] if isinstance(ts, str) and len(ts) >= 19 else ts,
        "kind": kind,
        "kind_class": kind.replace("_", "-"),  # → the brand's .kind--session-start etc.
        "text": _event_text(event),
    }


def _absent(title: str, reason: str, *, session: str | None = None) -> dict[str, Any]:
    return {
        "telemetry": True,
        "active": False,
        "ended": False,
        "session": session,
        "timeline": [],
        "title": title,
        "reason": reason,
    }


def follow_live(
    provider: DiskProvider, session_id: str | None = None, *, now: datetime | None = None
) -> dict[str, Any]:
    """Compose the live session view-model (current session, or a named past one).

    Everything returned is **derived** from the NDJSON stream. With no ``live/CURRENT``
    and no requested session, returns an honest idle pane; a present-but-empty session
    says so rather than faking activity.
    """
    sid = session_id or current_session(provider)
    if sid is None:
        return _absent(
            "Nothing running",
            "No live session right now — when a traced op runs, its activity streams here "
            "(live/ is ephemeral telemetry, never part of the freshness gate).",
        )
    events = read_session(provider, sid)
    if not events:
        return _absent(
            "That session has no readable events",
            "The live log is empty or has expired — live/ is append-only, ephemeral, and "
            "gitignored, so a finished session's stream is not kept.",
            session=sid,
        )

    now = now or datetime.now(UTC)
    last = events[-1]
    start = _last(events, "session_start") or events[0]
    end = _last(events, "session_end")
    step = _last(events, "step")
    progress = _last(events, "progress")
    log = _last(events, "log") or _last(events, "error")

    last_dt = _parse(last.get("ts"))
    age = _humanize((now - last_dt).total_seconds()) if last_dt else "—"
    is_current = current_session(provider) == sid
    ended = end is not None

    latest_log = None
    if log is not None:
        level = str(log.get("level") or ("error" if log.get("kind") == "error" else "info"))
        latest_log = {
            "level": level,
            "level_class": _LEVEL_CLASS.get(level, "effort"),
            "msg": str(log.get("msg") or ""),
        }

    return {
        "telemetry": True,
        "active": is_current and not ended,
        "ended": ended,
        "is_current": is_current,
        "session": sid,
        "op": start.get("op"),
        "task": start.get("task"),
        "started_at": start.get("ts"),
        "step": (step.get("name") or step.get("label") or step.get("msg")) if step else None,
        "progress": _pct(progress.get("progress")) if progress else None,
        "latest_log": latest_log,
        "last_event": {"kind": last.get("kind"), "ts": last.get("ts"), "seq": last.get("seq") or 0},
        "age": age,
        "result": end.get("result") if end else None,
        "duration_s": end.get("duration_s") if end else None,
        "run_record": end.get("run_record") if end else None,
        "event_count": len(events),
        "timeline": [_timeline_row(e) for e in events],
    }
