///|
/// A typed keep-alive handle for reading computed values from outside the
/// dependency graph.
///
/// Observer is NOT a cell -- it has no SoA slot, no CellRef variant, and
/// no subscriber set. It is a lightweight ref-counted handle that:
/// 1. Increments gc_root_counts for the target cell (keeps it alive during gc)
/// 2. Provides a typed getter for reading the cell's value
/// 3. Decrements gc_root_counts on dispose (allows gc to collect)
pub struct Observer[T] {
  priv runtime : Runtime
  priv target_id : CellId
  priv getter : () -> T
  priv mut disposed : Bool
} derive(Debug(ignore=[Runtime, Fn, CellId]))

///|
fn Runtime::add_read_root(self : Runtime, id : CellId) -> Unit {
  let prev = self.add_gc_root(id)
  if prev == 0 {
    self.cell_lifecycle[id.id].on_observe(self, id)
  }
}

///|
fn Runtime::remove_read_root(self : Runtime, id : CellId) -> Unit {
  if self.is_cell_disposed(id) {
    self.core.gc_root_counts.remove(id)
    return
  }
  let remaining = self.remove_gc_root(id)
  if remaining == 0 {
    self.cell_lifecycle[id.id].on_unobserve(self, id)
  }
}

///|
/// Returns the current value of the observed cell.
///
/// For Memo and HybridMemo targets, this triggers pull verification
/// if the cell is stale. For Reactive targets, returns the eagerly
/// computed cached value.
pub fn[T] Observer::get(self : Observer[T]) -> T {
  guard !self.disposed else { abort("Observer: already disposed") }
  let value = (self.getter)()
  if self.runtime.has_pending_events() {
    self.runtime.drain_pending_events_if_idle()
  }
  value
}

///|
/// Releases this observer's keep-alive hold on the target cell.
///
/// Decrements gc_root_counts. When the last observer for a cell is
/// disposed (count reaches 0), calls on_unobserve via CellLifecycle
/// (no-op in Layer 4a; Layer 4b adds push suspension).
///
/// Idempotent: disposing an already-disposed observer is a no-op.
pub fn[T] Observer::dispose(self : Observer[T]) -> Unit {
  guard !self.disposed else { return }
  self.disposed = true
  self.runtime.remove_read_root(self.target_id)
}

///|
/// Returns true if this observer has been disposed.
pub fn[T] Observer::is_disposed(self : Observer[T]) -> Bool {
  self.disposed
}

///|
/// A typed keep-alive handle that reads `Result` values from outside the graph.
///
/// `Watch` has the same GC-rooting semantics as `Observer`, but preserves
/// read errors as values for target-facade read APIs.
pub struct Watch[T] {
  priv runtime : Runtime
  priv target_id : CellId
  priv getter : () -> Result[T, ReadError]
  priv mut disposed : Bool
} derive(Debug(ignore=[Runtime, Fn, CellId]))

///|
/// Returns the current value or a mechanism error (cycle / disposed) from the
/// watched cell.
pub fn[T] Watch::read(self : Watch[T]) -> Result[T, ReadError] {
  guard !self.disposed else { abort("Watch: already disposed") }
  let value = (self.getter)()
  if self.runtime.has_pending_events() {
    self.runtime.drain_pending_events_if_idle()
  }
  value
}

///|
/// Returns the current value, aborting if a read error is detected.
pub fn[T] Watch::read_or_abort(self : Watch[T]) -> T {
  match self.read() {
    Ok(value) => value
    Err(e) => abort(e.format_path())
  }
}

///|
/// Releases this watch's keep-alive hold on the target cell.
///
/// Idempotent: disposing an already-disposed watch is a no-op.
pub fn[T] Watch::dispose(self : Watch[T]) -> Unit {
  guard !self.disposed else { return }
  self.disposed = true
  self.runtime.remove_read_root(self.target_id)
}

///|
/// Returns true if this watch has been disposed.
pub fn[T] Watch::is_disposed(self : Watch[T]) -> Bool {
  self.disposed
}

///|
/// Creates an observer for a Memo cell.
pub fn[T] Memo::observe(self : Memo[T]) -> Observer[T] {
  guard !self.rt.is_cell_disposed(self.cell_id) else {
    abort("Memo::observe called on a disposed memo")
  }
  let rt = self.rt
  rt.add_read_root(self.cell_id)
  {
    runtime: rt,
    target_id: self.cell_id,
    getter: fn() { self.read_permissive() },
    disposed: false,
  }
}

///|
fn[T] Memo::watch_result(self : Memo[T]) -> Watch[T] {
  guard !self.rt.is_cell_disposed(self.cell_id) else {
    abort("Memo::watch called on a disposed memo")
  }
  let rt = self.rt
  rt.add_read_root(self.cell_id)
  {
    runtime: rt,
    target_id: self.cell_id,
    getter: fn() { self.read_honest() },
    disposed: false,
  }
}

///|
/// Creates an observer for a HybridMemo cell.
pub fn[T : Eq] HybridMemo::observe(self : HybridMemo[T]) -> Observer[T] {
  guard !self.rt.is_cell_disposed(self.cell_id) else {
    abort("HybridMemo::observe called on a disposed hybrid memo")
  }
  let rt = self.rt
  rt.add_read_root(self.cell_id)
  {
    runtime: rt,
    target_id: self.cell_id,
    getter: fn() { self.read_permissive() },
    disposed: false,
  }
}

///|
fn[T : Eq] HybridMemo::watch_result(self : HybridMemo[T]) -> Watch[T] {
  guard !self.rt.is_cell_disposed(self.cell_id) else {
    abort("HybridMemo::watch called on a disposed hybrid memo")
  }
  let rt = self.rt
  rt.add_read_root(self.cell_id)
  {
    runtime: rt,
    target_id: self.cell_id,
    getter: fn() { self.read_honest() },
    disposed: false,
  }
}

///|
/// Creates an observer for a Reactive cell.
pub fn[T] Reactive::observe(self : Reactive[T]) -> Observer[T] {
  let cell_id = self.cell_id.id
  guard !self.rt.is_cell_disposed(cell_id) else {
    abort("Reactive::observe called on a disposed reactive")
  }
  let rt = self.rt
  rt.add_read_root(cell_id)
  {
    runtime: rt,
    target_id: cell_id,
    getter: fn() { self.read_permissive() },
    disposed: false,
  }
}

///|
fn[T] Reactive::watch_result(self : Reactive[T]) -> Watch[T] {
  let cell_id = self.cell_id.id
  guard !self.rt.is_cell_disposed(cell_id) else {
    abort("Reactive::watch called on a disposed reactive")
  }
  let rt = self.rt
  rt.add_read_root(cell_id)
  {
    runtime: rt,
    target_id: cell_id,
    // EagerDerived has no cycle/raise channel (its compute is `() -> T`), but
    // the `Watch` contract is uniformly honest about disposal: a read after the
    // target is disposed returns `Err(Disposed(_))` rather than aborting.
    getter: fn() {
      if self.rt.is_cell_disposed(cell_id) {
        Err(ReadError::disposed(cell_id))
      } else {
        Ok(self.read_permissive())
      }
    },
    disposed: false,
  }
}

///|
/// One-shot observe: observe a memo, read its value, dispose the observer.
///
/// Convenience for cases where you need a single outside-the-graph read.
///
/// # Panics
///
/// Aborts if the memo has been disposed.
#deprecated("Use target facade read_or_abort()/watch() methods; Runtime::read is legacy compatibility", skip_current_package=true)
pub fn[T] Runtime::read(self : Runtime, memo : Memo[T]) -> T {
  let obs = memo.observe()
  let val = (obs.getter)()
  obs.dispose()
  if self.has_pending_events() {
    self.drain_pending_events_if_idle()
  }
  val
}

///|
/// One-shot observe for HybridMemo.
#deprecated("Use target facade read_or_abort()/watch() methods; Runtime::read_hybrid is legacy compatibility", skip_current_package=true)
pub fn[T : Eq] Runtime::read_hybrid(self : Runtime, memo : HybridMemo[T]) -> T {
  let obs = memo.observe()
  let val = (obs.getter)()
  obs.dispose()
  if self.has_pending_events() {
    self.drain_pending_events_if_idle()
  }
  val
}

///|
/// One-shot observe for Reactive.
#deprecated("Use target facade read()/watch() methods; Runtime::read_reactive is legacy compatibility", skip_current_package=true)
pub fn[T] Runtime::read_reactive(self : Runtime, reactive : Reactive[T]) -> T {
  let obs = reactive.observe()
  let val = (obs.getter)()
  obs.dispose()
  if self.has_pending_events() {
    self.drain_pending_events_if_idle()
  }
  val
}