///|
pub struct ControlBinding {
  key : String
  node_index : Int
  slot : GraphParamSlot
} derive(Debug, Eq)

///|
#alias(new)
pub fn ControlBinding::ControlBinding(
  key~ : String,
  node_index~ : Int,
  slot~ : GraphParamSlot,
) -> ControlBinding {
  { key, node_index, slot }
}

///|
pub(all) enum ControlBindingError {
  InvalidNodeIndex(Int)
  InvalidSlotForNode(Int, GraphParamSlot)
  DuplicateKey(String)
  /// Binding (key, node_index) targets a template node that
  /// optimize_graph eliminated. Prevents silent set_param no-ops.
  OrphanBinding(String, Int)
} derive(Debug, Eq)

///|
pub struct ControlBindingBuilder {
  priv bindings : Array[ControlBinding]
} derive(Debug)

///|
#alias(new)
pub fn ControlBindingBuilder::ControlBindingBuilder() -> ControlBindingBuilder {
  { bindings: [] }
}

///|
/// Add a binding. Mutates internal array, returns self for chaining.
pub fn ControlBindingBuilder::bind(
  self : ControlBindingBuilder,
  key~ : String,
  node_index~ : Int,
  slot~ : GraphParamSlot,
) -> ControlBindingBuilder {
  self.bindings.push(ControlBinding::new(key~, node_index~, slot~))
  self
}

///|
/// Proven-valid control bindings. Validated against a specific
/// CompiledTemplate at build time (bounds + slot compatibility +
/// orphan detection + key uniqueness).
///
/// No public constructor — only reachable through ControlBindingBuilder::build().
///
/// WARNING: A ControlBindingMap's validity is tied to the template it was
/// built against. After VoicePool::set_template swaps to a new template,
/// bindings validated against the prior template remain type-level valid
/// but may silently retarget the wrong kind of node or no-op against
/// nodes the new template's optimize_graph eliminated. Rebuild the
/// ControlBindingMap whenever the template changes. Structural staleness
/// detection is tracked as a follow-up.
pub struct ControlBindingMap {
  priv bindings : Array[ControlBinding]
} derive(Debug, Eq)

///|
/// Number of validated bindings in this map.
pub fn ControlBindingMap::length(self : ControlBindingMap) -> Int {
  self.bindings.length()
}

///|
/// Validate all bindings against the compiled template and transition
/// to the proven-valid ControlBindingMap. Per-binding checks in order:
/// node index bounds, slot compatibility with the authoring node kind,
/// post-optimization liveness (rejects bindings on nodes eliminated by
/// optimize_graph), and key uniqueness. Returns the first error found.
pub fn ControlBindingBuilder::build(
  self : ControlBindingBuilder,
  compiled_template : CompiledTemplate,
) -> Result[ControlBindingMap, ControlBindingError] {
  let seen_keys : Map[String, Bool] = {}
  for i in 0..= compiled_template.length() {
      return Err(ControlBindingError::InvalidNodeIndex(binding.node_index))
    }
    if !node_accepts_slot(
        compiled_template.node_at(binding.node_index),
        binding.slot,
      ) {
      return Err(
        ControlBindingError::InvalidSlotForNode(
          binding.node_index,
          binding.slot,
        ),
      )
    }
    if !compiled_template.is_node_live(binding.node_index) {
      return Err(
        ControlBindingError::OrphanBinding(binding.key, binding.node_index),
      )
    }
    if seen_keys.contains(binding.key) {
      return Err(ControlBindingError::DuplicateKey(binding.key))
    }
    seen_keys[binding.key] = true
  }
  // WHY copy: self.bindings is a mutable Array shared with the builder.
  // Without copying, post-build bind() calls would mutate the validated map,
  // breaking the proven-valid invariant.
  Ok({ bindings: self.bindings.copy() })
}

///|
/// Convert pattern controls to graph controls using the validated bindings.
/// Emits GraphControl::set_param for each bound key found in the input map,
/// in binding insertion order. Missing keys are skipped; unrecognized keys
/// are ignored. Values are passed through without domain validation.
pub fn ControlBindingMap::resolve_controls(
  self : ControlBindingMap,
  controls : Map[String, Double],
) -> Array[GraphControl] {
  let result = Array::new(capacity=self.bindings.length())
  for i in 0..
        result.push(
          GraphControl::set_param(binding.node_index, binding.slot, value),
        )
      None => ()
    }
  }
  result
}

///|
pub impl Show for ControlBinding with output(self, logger) {
  logger.write_string(@debug.to_string(self))
}

///|
pub impl Show for ControlBindingError with output(self, logger) {
  logger.write_string(@debug.to_string(self))
}

///|
pub impl Show for ControlBindingBuilder with output(self, logger) {
  logger.write_string(@debug.to_string(self))
}

///|
pub impl Show for ControlBindingMap with output(self, logger) {
  logger.write_string(@debug.to_string(self))
}