// The refusal channel: what the runtime decided and never said.
//
// `Path::update` returns the very same root for an unresolvable path, for a
// handler nothing answers, for a handler that declined, and for a handler that
// ran and changed nothing. The success path and the failure path are
// byte-identical, which is why this framework is hard to break and why a click
// that did nothing is indistinguishable from a click that was refused. The
// distinction exists at the moment it is decided; this is where it goes.
//
// A DECLINE is not a refusal. An `update` arm answering `None` and a generated
// mutator handed a value it cannot use are the intended design — the whole
// vocabulary of "nothing happened" would be noise if every one of them were
// reported. What is reported is the cases where something was ASKED FOR and
// nothing claimed it, or where a rule the author wrote said no.
//
// Off unless a host switches it on, and that is the design rather than an
// omission: a record carries the rejected STATE, so building one is not free,
// and nothing here should pay for a listener that does not exist. Every
// producer asks `refusing()` first and takes the old `warn` path when the
// answer is no, so a page that installs nothing behaves exactly as before.

///|
/// Why a dispatch produced nothing.
///
/// Each case names a producer that exists in this repo. The design this comes
/// from has a longer vocabulary — `NO_REQUEST_FN`, `DECODE_FAILED`,
/// `COERCED_TO_DEFAULT`, `OUT_OF_RANGE`, `TELEPORT_MISSING` — and none of those
/// is here, because a code nothing raises is a promise the runtime does not
/// keep: a host filtering on it would conclude the failure never happens. A new
/// case arrives with the site that raises it, in the same change.
///
/// (`@emit_mbt.Refusal` is a compile-time namesake and unrelated: that one is a
/// declaration the MoonBit backend will not compile, and it is answered to the
/// generator rather than to a running page.)
pub(all) enum RefusalCode {
  /// The path named a leaf that is not there, so there was nothing to ask.
  PathUnresolved
  /// The leaf is there and nothing answers the name: no `update` arm, no
  /// generated mutator, no bucket. Raised by `Path::update` for a dispatch and
  /// by the 404 handler a view's unanswered name resolves to.
  NoHandler
  /// A `requires` did not hold, so the transition never started.
  Precondition
  /// An `ensures` did not hold, so the transition was abandoned whole — state
  /// and effects together.
  Postcondition
  /// An `invariant` did not hold after a transition, so it was abandoned. Same
  /// abandonment as a postcondition, different owner: the rule is the
  /// component's rather than this handler's.
  Invariant
} derive(Eq, Debug)

///|
/// The screaming-snake spelling — the one an author reads in a test failure.
///
/// One vocabulary learned once: a build-time hint about a missing arm and the
/// runtime record for the same failure say the same word.
pub fn RefusalCode::word(self : RefusalCode) -> String {
  match self {
    PathUnresolved => "PATH_UNRESOLVED"
    NoHandler => "NO_HANDLER"
    Precondition => "PRECONDITION"
    Postcondition => "POSTCONDITION"
    Invariant => "INVARIANT"
  }
}

///|
/// One refusal: where it happened, what was asked for, which rule said no, the
/// sentence that rule produced, and the state that was rejected.
pub(all) struct Refusal {
  code : RefusalCode
  /// The handler or message name that was asked for.
  asked : String
  /// The `pred` that said no. `""` when no rule was involved — nothing claimed
  /// the name, or the path did not resolve.
  rule : String
  /// What the rule's `format` said about THIS failure, with the values that
  /// made it fail. `""` when the rule declares no `format`, and `""` for every
  /// code that is not a rule.
  ///
  /// The doc comment on a `pred` and its `format` are different jobs: the
  /// comment says what the rule is, statically; the sentence says what went
  /// wrong this time.
  sentence : String
  /// The state that was rejected — the thing you actually want to look at, and
  /// the thing nothing could reach before this existed.
  ///
  /// WHICH state depends on the moment, and the moment is the code: a
  /// `Precondition` rejected the state as it arrived, a `Postcondition` and an
  /// `Invariant` rejected the successor the body built and the framework then
  /// threw away. `Null` where the producer has no state in hand at all, which
  /// today is only the render-time 404 handler.
  state : Value
  /// Where it happened, in the runtime's own addressing. Empty is the root and
  /// is a real answer, not a missing one.
  path : Path
} derive(Eq, Debug)

///|
/// A one-line rendering, for a host that wants text rather than the record.
///
/// The sentence the author wrote comes LAST and unquoted, because it is the
/// half a reader is meant to read: everything before it is provenance.
pub fn Refusal::to_line(self : Refusal) -> String {
  let b = StringBuilder::new()
  b.write_string(self.code.word())
  if self.rule != "" {
    b.write_string(" \{self.rule}")
  }
  if self.asked != "" {
    b.write_string(" at `\{self.asked}`")
  }
  if self.path.steps.length() > 0 {
    b.write_string(" on \{Repr(self.path.steps)}")
  }
  if self.sentence != "" {
    b.write_string(" — \{self.sentence}")
  }
  b.to_string()
}

///|
/// Where refusals go. `None` — the default — is the channel switched off, and
/// every producer checks it before building anything.
let refusal_hook : Ref[((Refusal) -> Unit)?] = Ref(None)

///|
/// One dispatch's worth of refusal.
///
/// `held` rather than a list because at most one record escapes a dispatch: a
/// refusal is reported where the chain ENDS, and everything above it on the way
/// out is the same failure being handed back. The first one to fire is the
/// deepest one that fired, which is the one that decided.
priv struct DispatchScope {
  path : Path
  mut held : Refusal?
}

///|
let dispatch_scope : Ref[DispatchScope?] = Ref(None)

///|
/// Switch the channel on. Returns the uninstall, which puts back whatever
/// listener was there before — so a test that installs one inside another's
/// lifetime cannot silently keep it.
pub fn on_refusal(f : (Refusal) -> Unit) -> () -> Unit {
  let prev = refusal_hook.val
  refusal_hook.val = Some(f)
  fn() { refusal_hook.val = prev }
}

///|
/// Whether anything is listening.
///
/// Producers ask this before they build a record, and take the `warn` path when
/// the answer is no. It is the reason a rejected state can be carried at all:
/// nobody pays for it until somebody wants it.
pub fn refusing() -> Bool {
  refusal_hook.val is Some(_)
}

///|
/// Report a refusal. A no-op when nothing is listening.
///
/// Inside a dispatch the record is HELD rather than delivered, so the one that
/// decided is the one that escapes. A refusal raised outside any dispatch — a
/// render-time name that nothing answers — is its own chain end and goes
/// straight out.
pub fn refuse(r : Refusal) -> Unit {
  guard refusal_hook.val is Some(f) else { return }
  match dispatch_scope.val {
    Some(sc) =>
      if sc.held is None {
        // A producer deep inside a body knows the rule and the state and has no
        // idea where the dispatch landed; the scope does. A path the producer
        // stated is kept — it knew something more specific.
        sc.held = Some(
          if r.path.steps.is_empty() {
            { ..r, path: sc.path }
          } else {
            r
          },
        )
      }
    None => f(r)
  }
}

///|
/// Open a dispatch scope, answering whether THIS call opened it.
///
/// A handler that dispatches again nests, and the inner call answers `false`
/// so it cannot close the outer one's scope: one dispatch, one record, however
/// many layers it walked.
fn begin_dispatch(path : Path) -> Bool {
  guard refusal_hook.val is Some(_) else { return false }
  guard dispatch_scope.val is None else { return false }
  dispatch_scope.val = Some({ path, held: None })
  true
}

///|
/// Close the scope `opened` reported opening, delivering the record it held.
fn end_dispatch(opened : Bool) -> Unit {
  guard opened else { return }
  let held = match dispatch_scope.val {
    Some(sc) => sc.held
    None => None
  }
  dispatch_scope.val = None
  guard held is Some(r) else { return }
  guard refusal_hook.val is Some(f) else { return }
  f(r)
}