///|
/// Physical model representation, independent of source language and backend.
#derive(Eq, Debug)
pub(all) enum ModelFormat {
  JSON
  YAML
  IonText
  IonBinary
}

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

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

///|
pub fn ModelFormat::from_control(
  value : String,
) -> ModelFormat raise BoundaryError {
  match value {
    "json" | "morphir-json" => JSON
    "yaml" | "yml" | "morphir-yaml" => YAML
    "ion" | "ion-text" => IonText
    "ionb" | "ion-binary" => IonBinary
    _ => raise Invalid("data.unknown_codec", value)
  }
}

///|
pub fn ModelFormat::infer_path(path : String) -> ModelFormat? {
  let path = path.to_lower()
  if path.has_suffix(".yaml") || path.has_suffix(".yml") {
    Some(YAML)
  } else if path.has_suffix(".json") {
    Some(JSON)
  } else if path.has_suffix(".ionb") {
    Some(IonBinary)
  } else if path.has_suffix(".ion") {
    Some(IonText)
  } else {
    None
  }
}

///|
pub fn ModelFormat::codec_id(self : ModelFormat) -> String {
  match self {
    JSON => "morphir-json"
    YAML => "morphir-yaml"
    IonText => "ion-text"
    IonBinary => "ion-binary"
  }
}

///|
pub fn ModelFormat::is_binary(self : ModelFormat) -> Bool {
  self is IonBinary
}

///|
/// Decode strict UTF-8 or binary at the shared boundary. The caller retains the
/// original bytes for exact export; normalization never replaces those bytes.
pub fn decode_model(
  format : ModelFormat,
  bytes : Bytes,
  unit_id~ : String = "workbench",
  limits~ : Limits = Limits::default(),
) -> IRUnit raise BoundaryError {
  inspect_model_source(format, bytes, unit_id~, limits~).unit
}

///|
/// Filesystem and picker adapters supply only relative paths and captured bytes.
/// All format, version, tree ownership and assembly decisions stay shared.
pub fn inspect_model_tree(
  files : Map[String, Bytes],
  unit_id~ : String = "workbench",
  limits~ : Limits = Limits::default(),
) -> CheckpointInspection raise BoundaryError {
  guard files.length() <= model_tree_file_limit else {
    raise Invalid("data.byte_limit", "Tree exceeds the 4096-file budget")
  }
  let mut total = 0
  for _, bytes in files {
    guard bytes.length() <= limits.max_bytes - total else {
      raise Invalid("data.byte_limit", "Tree exceeds the source byte budget")
    }
    total += bytes.length()
  }
  let text : Map[String, String] = Map([])
  for path, bytes in files {
    let relevant = (
        path == "manifest.yaml" ||
        path == "manifest.yml" ||
        path == "manifest.json" ||
        path.has_prefix("pkg/") ||
        path.has_prefix("deps/")
      ) &&
      (
        path.has_suffix(".yaml") ||
        path.has_suffix(".yml") ||
        path.has_suffix(".json")
      )
    text[path] = if relevant {
      @utf8.decode(bytes[:], ignore_bom=true) catch {
        _ => raise Invalid("data.invalid_utf8", "Tree text must be valid UTF-8")
      }
    } else {
      ""
    }
  }
  try {
    let source = @yaml.read_tree_versioned(text, limits={
      ..@yaml.Limits::default(),
      max_bytes: limits.max_bytes,
      max_depth: limits.max_depth,
      max_nodes: limits.max_values,
      max_assembly_nodes: limits.max_values,
    })
    let unit = IRUnit::new(unit_id, @migration.to_current(source))
    unit.validate(limits~)
    { unit, original_format_version: source.format_version(), }
  } catch {
    @wire.Invalid(code, detail) => raise Invalid(code, detail)
    Invalid(code, detail) => raise Invalid(code, detail)
    error => raise Invalid("data.decode_failed", error.to_string())
  }
}

///|
/// Maximum captured files, including ignored sidecars. Adapters check before insert.
pub let model_tree_file_limit : Int32 = 4096

///|
pub fn decode_model_tree(
  files : Map[String, Bytes],
  unit_id~ : String = "workbench",
  limits~ : Limits = Limits::default(),
) -> IRUnit raise BoundaryError {
  inspect_model_tree(files, unit_id~, limits~).unit
}