///|
/// Structured metadata about a cell in the dependency graph.
///
/// This structure provides a uniform view of both Input and Derived cells.
/// For inputs, the `dependencies` array will be empty.
///
/// # Fields
///
/// - `label`: Optional human-readable name for debugging and introspection
/// - `id`: The unique identifier for this cell
/// - `changed_at`: When this cell's value last actually changed
/// - `verified_at`: When this cell was last confirmed up-to-date
/// - `durability`: How often this cell is expected to change
/// - `dependencies`: Cell IDs this cell depends on (empty for inputs)
pub(all) struct CellInfo {
  label : String?
  id : CellId
  changed_at : Revision
  verified_at : Revision
  durability : Durability
  dependencies : Array[CellId]
  subscribers : Array[CellId]
}

///|
/// Snapshot a HashSet[CellId] into an Array[CellId].
fn snapshot_subscribers(
  subscribers : @hashset.HashSet[CellId],
) -> Array[CellId] {
  let result : Array[CellId] = []
  for sub in subscribers {
    result.push(sub)
  }
  result
}

///|
/// Returns structured metadata for any cell (input or derived).
///
/// This method provides uniform introspection access to cell metadata regardless of
/// whether the cell is an Input or Memo. It returns CellInfo containing the cell's
/// ID, type, revision information, durability, and dependency list.
///
/// # Parameters
///
/// - `id`: The CellId to query (obtained via Input::id() or Memo::id())
///
/// # Returns
///
/// - `Some(CellInfo)`: If the cell ID is valid and points to an active cell
/// - `None`: If the cell ID is out of bounds or points to an unused slot
///
/// # Usage
///
/// ```moonbit nocheck
/// let rt = Runtime()
/// let sig = Input(rt, 42)
/// let derived = Derived(rt, fn() { sig.get() * 2 })
/// let _ = derived.read_or_abort()  // Force computation
///
/// // Query input metadata
/// match rt.cell_info(sig.id()) {
///   Some(info) => {
///     println("Input changed at: \{info.changed_at}")
///     println("Dependencies: \{info.dependencies.length()}")
///   }
///   None => println("Cell not found")
/// }
///
/// // Query derived metadata
/// match rt.cell_info(derived.id()) {
///   Some(info) => {
///     println("Derived durability: \{info.durability}")
///     println("Depends on \{info.dependencies.length()} cells")
///   }
///   None => println("Cell not found")
/// }
/// ```
///
/// # Notes
///
/// - The dependency array is a copy; modifying it does not affect the runtime
/// - For inputs, the dependencies array is always empty
/// - For derived cells, dependencies are populated after the first computation
/// - The method performs bounds checking and returns None for invalid IDs
pub fn Runtime::cell_info(self : Runtime, id : CellId) -> CellInfo? {
  if !self.validate_cell_soft(id) {
    return None
  }
  let ops = self.core.cell_ops[id.id]
  match self.core.cell_index[id.id] {
    PullInput(_) => {
      let ca = ops.changed_at()
      Some(CellInfo::{
        label: ops.label(),
        id,
        changed_at: ca,
        // Inputs are always up-to-date after a set; verified_at == changed_at
        verified_at: ca,
        durability: ops.durability(),
        dependencies: [],
        subscribers: snapshot_subscribers(ops.subscribers()),
      })
    }
    PullMemo(idx) | HybridMemo(idx) => {
      let memo = self.pull.memos[idx]
      Some(CellInfo::{
        label: ops.label(),
        id,
        changed_at: ops.changed_at(),
        verified_at: memo.verified_at,
        durability: ops.durability(),
        dependencies: memo.dependencies.copy(),
        subscribers: snapshot_subscribers(ops.subscribers()),
      })
    }
    _ => None
  }
}

///|
/// Returns the cell IDs that depend on the given cell (reverse edges).
///
/// This enables introspection of the dependency graph in both directions.
/// The returned array is a snapshot; modifying it does not affect the runtime.
///
/// Returns an empty array if the cell ID is invalid, disposed, out of bounds,
/// or belongs to a different runtime — matching `cell_info` semantics.
///
/// # Parameters
///
/// - `id`: The cell to query
///
/// # Returns
///
/// Array of CellIds that have `id` in their dependency list, or empty
/// array if the cell ID is not valid for this runtime or has been disposed
pub fn Runtime::dependents(self : Runtime, id : CellId) -> Array[CellId] {
  if !self.validate_cell_soft(id) {
    return []
  }
  if self.is_cell_disposed(id) {
    return []
  }
  snapshot_subscribers(self.core.cell_ops[id.id].subscribers())
}

///|
/// Number of GC roots currently anchoring `id` — i.e. how many `Watch`
/// handles or `gc_root = Root` cells reference it. Returns
/// `0` for unknown or disposed cells (matches the `Runtime::dependents`
/// soft-fail idiom).
///
/// Phase 1 coordinator use: registration-time mode-1 check that each
/// protected cell is watch-rooted before the editor is published.
pub fn Runtime::gc_root_count(self : Runtime, id : CellId) -> Int {
  match self.core.gc_root_counts.get(id) {
    Some(n) => n
    None => 0
  }
}