///|
/// Stable, reviewable identity for one validated behavior tree asset.
pub(all) struct AssetIdentity {
  root : String
  node_count : Int
  blackboard_entries : Int
  canonical_dsl : String
  fingerprint : String
} derive(Debug, Eq)

///|
pub fn AssetIdentity::summary(self : AssetIdentity) -> String {
  "root=" +
  self.root +
  ", nodes=" +
  self.node_count.to_string() +
  ", blackboard=" +
  self.blackboard_entries.to_string() +
  ", fingerprint=" +
  self.fingerprint
}

///|
pub fn AssetIdentity::markdown(self : AssetIdentity) -> String {
  "# MoonBTKit Asset Identity\n\n" +
  "- " +
  self.summary() +
  "\n\n## Canonical DSL\n\n```text\n" +
  self.canonical_dsl +
  "\n```"
}

///|
/// Normalize comments, whitespace, blackboard order, and declaration order.
/// Child order remains unchanged because it affects behavior tree semantics.
pub fn canonicalize_dsl(source : String) -> Result[String, BtError] {
  match parse_dsl(source) {
    Ok(doc) => Ok(canonicalize_document(doc))
    Err(err) => Err(err)
  }
}

///|
/// Produce a deterministic, non-cryptographic fingerprint for cache keys,
/// version comparisons, and regression asset identity.
pub fn asset_fingerprint(source : String) -> Result[String, BtError] {
  match canonicalize_dsl(source) {
    Ok(canonical) =>
      Ok("asset-v2-" + stable_digest("moonbtkit-asset-v2\n" + canonical))
    Err(err) => Err(err)
  }
}

///|
pub fn inspect_asset(source : String) -> Result[AssetIdentity, BtError] {
  match parse_dsl(source) {
    Ok(doc) => {
      let canonical = canonicalize_document(doc)
      Ok({
        root: doc.tree.root,
        node_count: doc.tree.node_count(),
        blackboard_entries: doc.blackboard.len(),
        canonical_dsl: canonical,
        fingerprint: "asset-v2-" +
        stable_digest("moonbtkit-asset-v2\n" + canonical),
      })
    }
    Err(err) => Err(err)
  }
}

///|
fn canonicalize_document(doc : DslDocument) -> String {
  let lines = Array::new()
  lines.push("root " + doc.tree.root)
  let board_lines = named_blackboard_lines(doc.blackboard)
  sort_named_lines(board_lines)
  let mut b = 0
  while b < board_lines.length() {
    lines.push(board_lines[b].1)
    b = b + 1
  }
  let node_lines = named_node_lines(doc.tree)
  sort_named_lines(node_lines)
  let mut n = 0
  while n < node_lines.length() {
    lines.push(node_lines[n].1)
    n = n + 1
  }
  join_strings(lines, "\n")
}

///|
fn named_blackboard_lines(board : Blackboard) -> Array[(String, String)] {
  let values = board.snapshot()
  let serialized = serialize_blackboard(board)
  let out = Array::new()
  let mut i = 0
  while i < values.length() {
    out.push((values[i].0, serialized[i]))
    i = i + 1
  }
  out
}

///|
fn named_node_lines(tree : BehaviorTree) -> Array[(String, String)] {
  let serialized = serialize_tree(tree)
  let out = Array::new()
  let mut i = 0
  while i < tree.nodes.length() {
    out.push((tree.nodes[i].id, serialized[i + 1]))
    i = i + 1
  }
  out
}

///|
fn sort_named_lines(values : Array[(String, String)]) -> Unit {
  let mut i = 0
  while i < values.length() {
    let mut best = i
    let mut j = i + 1
    while j < values.length() {
      if string_less(values[j].0, values[best].0) {
        best = j
      }
      j = j + 1
    }
    if best != i {
      let temp = values[i]
      values[i] = values[best]
      values[best] = temp
    }
    i = i + 1
  }
}

///|
fn string_less(left : String, right : String) -> Bool {
  let limit = if left.length() < right.length() {
    left.length()
  } else {
    right.length()
  }
  let mut i = 0
  while i < limit {
    if left[i] < right[i] {
      return true
    }
    if left[i] > right[i] {
      return false
    }
    i = i + 1
  }
  left.length() < right.length()
}