///|
/// Lifecycle phase of a Fuwaroid's loop. Phases
/// only move FORWARD: `Running` → `Closing` (a `close` was requested) →
/// `Stopped` (the unified stop/cleanup path completed). An abnormal stop —
/// host cancellation observed at a yield/`get` boundary, or another
/// terminal mailbox error — may jump straight from `Running` or `Closing`
/// to `Stopped`, carrying the recorded `StopReason`. The phase never moves
/// backwards: a late `close` cannot un-stop an instance, and a second
/// `close` cannot leave `Closing` once `Stopped` was reached.
pub(all) enum Lifecycle {
  Running
  Closing
  Stopped(StopReason)
} derive(Debug)

///|
/// Synchronous, read-only diagnostic snapshot of one Fuwaroid. Every
/// field is a copied value: a snapshot never leaks
/// mutable internal state, the business `State`, nor `Cmd`/`Query`/`Reply`
/// values.
///
/// Field semantics:
/// - `lifecycle`: the forward-only phase, see `Lifecycle`.
/// - `accepted`: admissions ACCEPTED by the mailbox — commands and queries
///   alike, one count per successful `tell`/`ask` admission.
/// - `processed`: messages whose handler RETURNED and whose state was
///   committed on the loop. This is NOT background-work completion: work a
///   handler spawned on the host group may still be running (or have been
///   refused by the closing mailbox) long after `processed` was bumped.
/// - `rejected_full` / `rejected_closed`: admission attempts refused
///   because the bounded mailbox was full / because the mailbox was
///   closed — one count per refused `tell` or `ask`.
/// - `abandoned`: messages accepted but never served when the loop stopped
///   abnormally — commands and queries alike, counted by the unified
///   cleanup path over exactly the envelopes its stranded-ask drain
///   removes. Once the loop has terminated the conservation law closes:
///   `abandoned == accepted − processed` for an abnormal stop and
///   `abandoned == 0` for a graceful one.
/// - `outstanding`: derived at snapshot time as
///   `accepted − processed − abandoned` — accepted messages whose fate
///   (served or abandoned) is not decided yet. It counts REQUESTS in
///   flight; it does NOT pretend to be the underlying buffer length (a
///   parked-reader handoff can hold one message beyond the configured
///   capacity, and this field says nothing about buffer occupancy).
///
/// Counter policy: every counter is `Int64` and only ever increases (each
/// counts one monotone event kind); they never silently wrap or reset.
/// Overflow handling is by design "unreachable, documented": at 2^63
/// admissions the cooperative runtime would have exhausted every
/// realizable resource long before, and per-increment checked arithmetic
/// on the message hot path is not worth its cost.
pub(all) struct Snapshot {
  lifecycle : Lifecycle
  accepted : Int64
  processed : Int64
  rejected_full : Int64
  rejected_closed : Int64
  abandoned : Int64
  outstanding : Int64
} derive(Debug)

///|
/// Mutable per-instance diagnostic state backing `Snapshot`. It lives
/// inside the instance's `LoopStop` cell, which is shared between the
/// handle (admission side: `tell`/`ask`/`close`) and the loop task
/// (serving side: `processed`; cleanup: `abandoned` and the final phase).
/// All updates happen inside synchronous, non-suspending sections, so the
/// cooperative runtime never observes a torn or half-counted state.
priv struct Diag {
  mut lifecycle : Lifecycle
  mut accepted : Int64
  mut processed : Int64
  mut rejected_full : Int64
  mut rejected_closed : Int64
  mut abandoned : Int64
}

///|
fn Diag::fresh() -> Diag {
  {
    lifecycle: Running,
    accepted: 0L,
    processed: 0L,
    rejected_full: 0L,
    rejected_closed: 0L,
    abandoned: 0L,
  }
}

///|
/// Rank used by the forward-only phase move (see `Diag::transition`).
fn lifecycle_rank(lifecycle : Lifecycle) -> Int {
  match lifecycle {
    Running => 0
    Closing => 1
    Stopped(_) => 2
  }
}

///|
/// Forward-only phase move. A backwards request (e.g. a `close` arriving
/// after the loop already stopped) is ignored.
fn Diag::transition(self : Diag, next : Lifecycle) -> Unit {
  if lifecycle_rank(next) > lifecycle_rank(self.lifecycle) {
    self.lifecycle = next
  }
}

///|
/// Synchronous, read-only diagnostic snapshot (field criteria on
/// `Snapshot`). Never raises, never suspends: the read happens in one
/// uninterrupted scheduling turn, so the counters are mutually consistent
/// and `outstanding = accepted − processed − abandoned` holds for every
/// observed snapshot.
pub fn[Cmd, Query, Reply] Fuwaroid::snapshot(
  self : Fuwaroid[Cmd, Query, Reply],
) -> Snapshot {
  match self.stop {
    Some(cell) => {
      let d = cell.diag
      {
        lifecycle: d.lifecycle,
        accepted: d.accepted,
        processed: d.processed,
        rejected_full: d.rejected_full,
        rejected_closed: d.rejected_closed,
        abandoned: d.abandoned,
        outstanding: d.accepted - d.processed - d.abandoned,
      }
    }
    // A handle not obtained from `spawn` (white-box construction) has no
    // loop and no recorded diagnostics: report the zero-valued Running view.
    None =>
      {
        lifecycle: Running,
        accepted: 0L,
        processed: 0L,
        rejected_full: 0L,
        rejected_closed: 0L,
        abandoned: 0L,
        outstanding: 0L,
      }
  }
}