///|
/// `RuntimeControl` — the host-side control handle. A narrow, identity-guarded
/// facade over the run's control plane: follow-up submission and abort,
/// consumed by Posoco at safe boundaries. Stale targets are rejected at
/// enqueue time; abort is idempotent. The run loop itself stays internal —
/// this handle never exposes phases, journals, or the mailbox.

///|
/// Default max follow-up queue depth, aligned with the internal control
/// mailbox bound. Beyond this, enqueue returns `RejectedQueueFull`.
let default_max_follow_up_depth : Int = 64

///|
/// Result of a `RuntimeControl` submission.
pub(all) enum EnqueueOutcome {
  /// Command accepted; carries the command id for diagnostics/correlation.
  Accepted(command_id~ : String)
  /// Rejected because there is no active run, or the target went stale.
  RejectedStale(reason~ : String)
  /// Rejected because the follow-up queue is full.
  RejectedQueueFull(depth~ : Int)
  /// Abort arrived after a previous abort already fired for the active run.
  AbortAlreadyRequested
} derive(Eq, Debug)

///|
pub impl Show for EnqueueOutcome with fn to_string(self : EnqueueOutcome) -> String {
  match self {
    Accepted(command_id~) => "Accepted(\{command_id})"
    RejectedStale(reason~) => "RejectedStale(\{reason})"
    RejectedQueueFull(depth~) => "RejectedQueueFull(depth=\{depth})"
    AbortAlreadyRequested => "AbortAlreadyRequested"
  }
}

///|
/// The control handle. One instance per Agent; obtained via
/// `Agent::control()`. Holds a borrowed internal mailbox for identity
/// checks and abort signalling, plus its own follow-up queue drained by the
/// Agent at turn boundaries.
pub struct RuntimeControl {
  priv mailbox : &@puppetry.Mailbox
  priv follow_ups : @aqueue.Queue[@kernel.Message]
  priv mut follow_up_depth : Int
  /// Monotonic counter for command ids, so submissions correlate in
  /// diagnostics without wall-clock time.
  priv mut seq : Int
  priv max_depth : Int
}

///|
/// Construct the control handle over the Agent's shared mailbox. Framework
/// entry point — hosts obtain the handle from `Agent::control()`, they do
/// not construct it.
pub fn RuntimeControl::new(mailbox : &@puppetry.Mailbox) -> RuntimeControl {
  {
    mailbox,
    follow_ups: @aqueue.Queue::Queue(kind=Unbounded),
    follow_up_depth: 0,
    seq: 0,
    max_depth: default_max_follow_up_depth,
  }
}

///|
fn RuntimeControl::next_command_id(
  self : RuntimeControl,
  prefix : String,
) -> @puppetry.CommandId {
  self.seq = self.seq + 1
  @puppetry.CommandId::unchecked("\{prefix}_\{self.seq}")
}

///|
/// Currently-active run id, `None` when the Agent is idle. Hosts use this
/// (with `active_turn_id`) to target submissions and to detect staleness.
pub fn RuntimeControl::active_run_id(self : RuntimeControl) -> @kernel.RunId? {
  self.mailbox.active_run_id()
}

///|
/// Currently-active turn id, `None` when the Agent is idle.
pub fn RuntimeControl::active_turn_id(self : RuntimeControl) -> @kernel.TurnId? {
  self.mailbox.active_turn_id()
}

///|
/// Submit a follow-up message. The Agent consumes queued follow-ups one at
/// a time at turn boundaries (OneAtATime, matching the internal follow-up
/// mailbox policy) and drives a subsequent turn with each as a new user
/// message. Rejected with `RejectedStale` when no run is active — a
/// background task that finishes after its turn ended cannot sneak a
/// message into a later run.
pub fn RuntimeControl::enqueue_follow_up(
  self : RuntimeControl,
  message : @kernel.Message,
) -> EnqueueOutcome {
  let command_id = self.next_command_id("follow_up")
  match (self.mailbox.active_run_id(), self.mailbox.active_turn_id()) {
    (Some(_), Some(_)) => {
      if self.follow_up_depth >= self.max_depth {
        return RejectedQueueFull(depth=self.follow_up_depth)
      }
      let put_ok : Bool = self.follow_ups.try_put(message) catch { _ => false }
      if !put_ok {
        return RejectedQueueFull(depth=self.follow_up_depth)
      }
      self.follow_up_depth = self.follow_up_depth + 1
      Accepted(command_id=command_id.to_string())
    }
    _ => RejectedStale(reason="follow_up requires an active run")
  }
}

///|
/// Take the oldest queued follow-up (OneAtATime). Consumed by the Agent at
/// turn boundaries; hosts observe depth via `pending_follow_ups` instead.
pub fn RuntimeControl::take_follow_up(
  self : RuntimeControl,
) -> @kernel.Message? {
  let next : @kernel.Message? = self.follow_ups.try_get() catch { _ => None }
  match next {
    Some(message) => {
      self.follow_up_depth = self.follow_up_depth - 1
      Some(message)
    }
    None => None
  }
}

///|
/// Number of follow-ups currently queued.
pub fn RuntimeControl::pending_follow_ups(self : RuntimeControl) -> Int {
  self.follow_up_depth
}

///|
/// Abort the active run. Idempotent: the first call is accepted and the
/// loop observes it at its next safe point (in-flight effects receive a
/// best-effort `cancel_effects`); subsequent calls return
/// `AbortAlreadyRequested`. Rejected with `RejectedStale` when no run is
/// active.
pub fn RuntimeControl::abort_active(
  self : RuntimeControl,
  detail : String?,
) -> EnqueueOutcome {
  let command_id = self.next_command_id("abort")
  let cmd : @puppetry.AbortCommand = {
    command_id,
    target_run_id: match self.mailbox.active_run_id() {
      Some(run) => run
      None => return RejectedStale(reason="no active run to abort")
    },
    detail,
  }
  match self.mailbox.enqueue_abort(cmd) {
    @puppetry.Accepted(command_id~) =>
      Accepted(command_id=command_id.to_string())
    @puppetry.RejectedStale(reason~, ..) => RejectedStale(reason~)
    @puppetry.RejectedQueueFull(depth~, ..) => RejectedQueueFull(depth~)
    @puppetry.AbortAlreadyRequested(..) => AbortAlreadyRequested
  }
}