///|
/// Stable boundary diagnostics. IR codec codes are retained unchanged.
#derive(Eq, Debug)
pub(all) suberror BoundaryError {
  Invalid(String, String)
}

///|
pub extend BoundaryError with Eq::{equal, not_equal}

///|
pub extend BoundaryError with @debug.Debug::{to_repr}

///|
/// Binary content is never implicitly interpreted as UTF-8 text.
#derive(Eq, Debug)
pub(all) enum Content {
  Text(String)
  Binary(Bytes)
}

///|
pub extend Content with Eq::{equal, not_equal}

///|
pub extend Content with @debug.Debug::{to_repr}

///|
/// IDs are logical identities supplied by a caller, not host filesystem paths.
#derive(Eq, Debug)
pub(all) struct SourceSnapshot {
  unit_id : String
  content : Content
}

///|
pub extend SourceSnapshot with Eq::{equal, not_equal}

///|
pub extend SourceSnapshot with @debug.Debug::{to_repr}

///|
#derive(Debug)
pub(all) struct NodeMetadata {
  node_id : String
  data : @ion.IonValue
}

///|
pub extend NodeMetadata with @debug.Debug::{to_repr}

///|
/// Rich metadata stays outside the JSON-shaped semantic IR extension model.
/// Structural transforms need a separate lineage contract; identity processing
/// can return this value unchanged.
#derive(Debug)
pub(all) struct IRUnit {
  id : String
  distribution : @ir.IRFile
  metadata : Array[NodeMetadata]
  annotations : Array[@ion.SymbolToken]
  provenance : Array[@ion.IonValue]
}

///|
pub extend IRUnit with @debug.Debug::{to_repr}

///|
/// The decoded checkpoint's format is independent of its migrated semantic IR.
/// This result carries no file bytes, retained model handle or host authority.
#derive(Debug)
pub struct CheckpointInspection {
  unit : IRUnit
  original_format_version : @version.FormatVersion
}

///|
pub extend CheckpointInspection with @debug.Debug::{to_repr}

///|
pub fn IRUnit::new(
  id : String,
  distribution : @ir.IRFile,
  metadata~ : Array[NodeMetadata] = [],
) -> IRUnit {
  {
    id,
    distribution,
    metadata: metadata.copy(),
    annotations: [@ion.symbol("morphir_pipeline")],
    provenance: [],
  }
}

///|
/// Validate a source frontend's rich output before components or serialization.
pub fn IRUnit::validate(
  self : IRUnit,
  limits~ : Limits = Limits::default(),
) -> Unit raise {
  if limits.max_bytes <= 0 || limits.max_depth < 0 || limits.max_values <= 0 {
    raise Invalid("data.invalid_limits", "Limits must be positive")
  }
  let envelope = write_envelope(self, limits)
  let owned = @identities.nodes(self.id, self.distribution)
  for entry in self.metadata {
    if !owned.contains(entry.node_id) {
      raise Invalid(
        "data.invalid_identity",
        "Unowned metadata node: " + entry.node_id,
      )
    }
  }
  check_content(Binary(@ion_binary.write_binary_value(envelope)), limits)
}

///|
/// These are acceptance/traversal budgets, not a streaming-parser guarantee.
#derive(Eq, Debug)
pub(all) struct Limits {
  max_bytes : Int32
  max_depth : Int32
  max_values : Int32
}

///|
pub extend Limits with Eq::{equal, not_equal}

///|
pub extend Limits with @debug.Debug::{to_repr}

///|
pub fn Limits::default() -> Limits {
  { max_bytes: 4 * 1024 * 1024, max_depth: 128, max_values: 100000, }
}

///|
pub(all) struct Codec {
  decode : (SourceSnapshot, Limits) -> IRUnit raise
  encode : (IRUnit, Limits) -> Content raise
}

///|
pub struct Registry {
  priv codecs : Map[String, Codec]
  priv limits : Limits
}

///|
pub const PROFILE : String = "morphir-pipeline-v1"