///|
priv struct ActiveProgramSubscription {
  key : String
  mut kind : String
  mut label : String
  mut descriptor : @core.SubscriptionDescriptor
  active : Ref[Bool]
  cancel : (() -> Unit)?
  mut reused : Bool
}

///|
priv struct ActiveProgramEffectTask {
  token : Int
  descriptor : @core.EffectDescriptor
  active : Ref[Bool]
  cancel : Ref[(() -> Unit)?]
}

///|
priv struct ProgramRuntimeDiagnostics {
  mut dispatched_count : Int
  mut update_count : Int
  mut ignored_program_dispatch_count : Int
  mut message_enqueue_count : Int
  mut message_drain_count : Int
  mut pending_message_count : Int
  mut max_pending_message_count : Int
  mut effect_plan_count : Int
  mut effect_count : Int
  mut effect_none_count : Int
  mut effect_batch_count : Int
  mut effect_send_count : Int
  mut effect_dispatch_count : Int
  mut effect_run_count : Int
  mut effect_task_count : Int
  mut duplicate_effect_descriptor_key_count : Int
  duplicate_effect_descriptor_keys : Array[String]
  mut superseded_effect_task_key_count : Int
  superseded_effect_task_keys : Array[String]
  mut last_effect_summary : EffectPlanSummary
  mut last_scheduled_effect_summary : EffectPlanSummary
  mut next_effect_task_value : Int
  mut started_effect_task_count : Int
  mut completed_effect_task_count : Int
  mut cancelled_effect_task_count : Int
  mut ignored_effect_task_dispatch_count : Int
  mut active_effect_tasks : Array[ActiveProgramEffectTask]
  effect_task_lifecycle : Array[EffectTaskLifecycleSnapshot]
  mut subscription_plan_count : Int
  mut last_subscription_plan_summary : SubscriptionPlanSummary
  mut started_subscription_count : Int
  mut reused_subscription_count : Int
  mut cancelled_subscription_count : Int
  mut duplicate_subscription_key_count : Int
  duplicate_subscription_keys : Array[String]
  mut ignored_subscription_dispatch_count : Int
  mut active_subscriptions : Array[ActiveProgramSubscription]
  subscription_lifecycle : Array[SubscriptionLifecycleSnapshot]
}

///|
fn ProgramRuntimeDiagnostics::new() -> ProgramRuntimeDiagnostics {
  {
    dispatched_count: 0,
    update_count: 0,
    ignored_program_dispatch_count: 0,
    message_enqueue_count: 0,
    message_drain_count: 0,
    pending_message_count: 0,
    max_pending_message_count: 0,
    effect_plan_count: 0,
    effect_count: 0,
    effect_none_count: 0,
    effect_batch_count: 0,
    effect_send_count: 0,
    effect_dispatch_count: 0,
    effect_run_count: 0,
    effect_task_count: 0,
    duplicate_effect_descriptor_key_count: 0,
    duplicate_effect_descriptor_keys: [],
    superseded_effect_task_key_count: 0,
    superseded_effect_task_keys: [],
    last_effect_summary: EffectPlanSummary::empty(),
    last_scheduled_effect_summary: EffectPlanSummary::empty(),
    next_effect_task_value: 1,
    started_effect_task_count: 0,
    completed_effect_task_count: 0,
    cancelled_effect_task_count: 0,
    ignored_effect_task_dispatch_count: 0,
    active_effect_tasks: [],
    effect_task_lifecycle: [],
    subscription_plan_count: 0,
    last_subscription_plan_summary: SubscriptionPlanSummary::empty(),
    started_subscription_count: 0,
    reused_subscription_count: 0,
    cancelled_subscription_count: 0,
    duplicate_subscription_key_count: 0,
    duplicate_subscription_keys: [],
    ignored_subscription_dispatch_count: 0,
    active_subscriptions: [],
    subscription_lifecycle: [],
  }
}

///|
fn ProgramRuntimeDiagnostics::record_dispatch(
  self : ProgramRuntimeDiagnostics,
) -> Unit {
  self.dispatched_count = self.dispatched_count + 1
}

///|
fn ProgramRuntimeDiagnostics::record_update(
  self : ProgramRuntimeDiagnostics,
) -> Unit {
  self.update_count = self.update_count + 1
}

///|
fn ProgramRuntimeDiagnostics::record_ignored_program_dispatch(
  self : ProgramRuntimeDiagnostics,
) -> Unit {
  self.ignored_program_dispatch_count = self.ignored_program_dispatch_count + 1
}

///|
fn ProgramRuntimeDiagnostics::record_message_enqueued(
  self : ProgramRuntimeDiagnostics,
  pending_count : Int,
) -> Unit {
  self.message_enqueue_count = self.message_enqueue_count + 1
  self.record_pending_message_count(pending_count)
}

///|
fn ProgramRuntimeDiagnostics::record_message_drain_started(
  self : ProgramRuntimeDiagnostics,
  pending_count : Int,
) -> Unit {
  if pending_count > 0 {
    self.message_drain_count = self.message_drain_count + 1
  }
  self.record_pending_message_count(pending_count)
}

///|
fn ProgramRuntimeDiagnostics::record_message_processed(
  self : ProgramRuntimeDiagnostics,
  pending_count : Int,
) -> Unit {
  self.record_pending_message_count(pending_count)
}

///|
fn ProgramRuntimeDiagnostics::record_message_queue_cleared(
  self : ProgramRuntimeDiagnostics,
) -> Unit {
  self.record_pending_message_count(0)
}

///|
fn ProgramRuntimeDiagnostics::record_pending_message_count(
  self : ProgramRuntimeDiagnostics,
  pending_count : Int,
) -> Unit {
  self.pending_message_count = pending_count
  if pending_count > self.max_pending_message_count {
    self.max_pending_message_count = pending_count
  }
}

///|
fn ProgramRuntimeDiagnostics::record_effects(
  self : ProgramRuntimeDiagnostics,
  summary : EffectPlanSummary,
  executed_count : Int,
) -> Unit {
  self.effect_plan_count = self.effect_plan_count + 1
  self.effect_count = self.effect_count + executed_count
  self.effect_none_count = self.effect_none_count + summary.none_count
  self.effect_batch_count = self.effect_batch_count + summary.batch_count
  self.effect_send_count = self.effect_send_count + summary.send_count
  self.effect_dispatch_count = self.effect_dispatch_count +
    summary.dispatch_count
  self.effect_run_count = self.effect_run_count + summary.run_count
  self.effect_task_count = self.effect_task_count + summary.task_count
  self.duplicate_effect_descriptor_key_count = self.duplicate_effect_descriptor_key_count +
    summary.duplicate_descriptor_key_count
  self.record_duplicate_effect_descriptor_keys(
    summary.duplicate_descriptor_keys,
  )
  self.last_effect_summary = summary
  if summary.scheduled_count > 0 {
    self.last_scheduled_effect_summary = summary
  }
}

///|
fn ProgramRuntimeDiagnostics::record_ignored_effect_task_dispatch(
  self : ProgramRuntimeDiagnostics,
) -> Unit {
  self.ignored_effect_task_dispatch_count = self.ignored_effect_task_dispatch_count +
    1
}

///|
/// Records a live task whose key was reused by a descriptor with a different
/// kind or label. The supersession itself stays silent-cancel (last-writer
/// semantics); only the collision evidence is recorded so apps can discover
/// accidental key collisions through the diagnostics snapshot.
fn ProgramRuntimeDiagnostics::record_effect_task_key_supersession(
  self : ProgramRuntimeDiagnostics,
  replaced : @core.EffectDescriptor,
  incoming : @core.EffectDescriptor,
) -> Unit {
  self.superseded_effect_task_key_count = self.superseded_effect_task_key_count +
    1
  let detail = if replaced.kind() != incoming.kind() {
    "kind: " + replaced.kind() + " -> " + incoming.kind()
  } else {
    "label: " + replaced.label() + " -> " + incoming.label()
  }
  let record = incoming.key() + " (" + detail + ")"
  if !self.superseded_effect_task_keys.any(existing => existing == record) {
    self.superseded_effect_task_keys.push(record)
  }
}

///|
fn ProgramRuntimeDiagnostics::record_duplicate_effect_descriptor_keys(
  self : ProgramRuntimeDiagnostics,
  keys : Array[String],
) -> Unit {
  for key in keys {
    if !self.duplicate_effect_descriptor_keys.any(existing => existing == key) {
      self.duplicate_effect_descriptor_keys.push(key)
    }
  }
}

///|
fn ProgramRuntimeDiagnostics::snapshot(
  self : ProgramRuntimeDiagnostics,
) -> ProgramRuntimeSnapshot {
  {
    dispatched_count: self.dispatched_count,
    update_count: self.update_count,
    ignored_program_dispatch_count: self.ignored_program_dispatch_count,
    message_enqueue_count: self.message_enqueue_count,
    message_drain_count: self.message_drain_count,
    pending_message_count: self.pending_message_count,
    max_pending_message_count: self.max_pending_message_count,
    effect_plan_count: self.effect_plan_count,
    effect_count: self.effect_count,
    effect_none_count: self.effect_none_count,
    effect_batch_count: self.effect_batch_count,
    effect_send_count: self.effect_send_count,
    effect_dispatch_count: self.effect_dispatch_count,
    effect_run_count: self.effect_run_count,
    effect_task_count: self.effect_task_count,
    duplicate_effect_descriptor_key_count: self.duplicate_effect_descriptor_key_count,
    duplicate_effect_descriptor_keys: self.duplicate_effect_descriptor_keys.copy(),
    superseded_effect_task_key_count: self.superseded_effect_task_key_count,
    superseded_effect_task_keys: self.superseded_effect_task_keys.copy(),
    last_effect_summary: self.last_effect_summary,
    last_scheduled_effect_summary: self.last_scheduled_effect_summary,
    active_effect_task_count: self.active_effect_tasks.length(),
    started_effect_task_count: self.started_effect_task_count,
    completed_effect_task_count: self.completed_effect_task_count,
    cancelled_effect_task_count: self.cancelled_effect_task_count,
    ignored_effect_task_dispatch_count: self.ignored_effect_task_dispatch_count,
    active_effect_tasks: self.active_effect_tasks.map(task => task.descriptor),
    effect_task_lifecycle: self.effect_task_lifecycle.copy(),
    subscription_plan_count: self.subscription_plan_count,
    last_subscription_plan_summary: self.last_subscription_plan_summary,
    active_subscription_count: self.active_subscriptions.length(),
    started_subscription_count: self.started_subscription_count,
    reused_subscription_count: self.reused_subscription_count,
    cancelled_subscription_count: self.cancelled_subscription_count,
    duplicate_subscription_key_count: self.duplicate_subscription_key_count,
    duplicate_subscription_keys: self.duplicate_subscription_keys.copy(),
    ignored_subscription_dispatch_count: self.ignored_subscription_dispatch_count,
    active_subscriptions: self.active_subscriptions.map(active => {
      active.descriptor
    }),
    active_subscription_kind_counts: self.active_subscription_kind_counts(),
    subscription_lifecycle: self.subscription_lifecycle.copy(),
  }
}