///|
/// Structural metrics for logging, admission control, and telemetry.
pub(all) struct CborDocumentStats {
  nodes : Int
  containers : Int
  max_depth : Int
  text_bytes : Int
  binary_bytes : Int
  encoded_bytes : Int
} derive(Eq, Debug)

///|
fn add_document_stats(
  left : CborDocumentStats,
  right : CborDocumentStats,
) -> CborDocumentStats {
  {
    nodes: left.nodes + right.nodes,
    containers: left.containers + right.containers,
    max_depth: if left.max_depth > right.max_depth {
      left.max_depth
    } else {
      right.max_depth
    },
    text_bytes: left.text_bytes + right.text_bytes,
    binary_bytes: left.binary_bytes + right.binary_bytes,
    encoded_bytes: left.encoded_bytes + right.encoded_bytes,
  }
}

///|
fn document_stats_at(value : CborValue, depth : Int) -> CborDocumentStats {
  let base = {
    nodes: 1,
    containers: 0,
    max_depth: depth,
    text_bytes: 0,
    binary_bytes: 0,
    encoded_bytes: encode(value).length(),
  }
  match value {
    Text(text) => { ..base, text_bytes: text.length() }
    Bytes(bytes) => { ..base, binary_bytes: bytes.length() }
    Array(items) => {
      let mut result = { ..base, containers: 1 }
      for item in items {
        result = add_document_stats(result, document_stats_at(item, depth + 1))
      }
      result
    }
    Map(entries) => {
      let mut result = { ..base, containers: 1 }
      for entry in entries {
        result = add_document_stats(
          result,
          document_stats_at(entry.0, depth + 1),
        )
        result = add_document_stats(
          result,
          document_stats_at(entry.1, depth + 1),
        )
      }
      result
    }
    Tag(_, item) => {
      let child = document_stats_at(item, depth + 1)
      { ..add_document_stats(base, child), containers: 1 }
    }
    _ => base
  }
}

///|
/// Compute node, container, depth, payload, and encoded-size metrics.
pub fn cbor_document_stats(value : CborValue) -> CborDocumentStats {
  document_stats_at(value, 1)
}

///|
/// Return the encoded byte size of a document.
pub fn cbor_document_encoded_size(value : CborValue) -> Int {
  encode(value).length()
}

///|
/// Return true when the document fits both structural budgets.
pub fn cbor_document_within_budget(
  value : CborValue,
  max_nodes : Int,
  max_encoded_bytes : Int,
  max_depth : Int,
) -> Bool {
  let stats = cbor_document_stats(value)
  stats.nodes <= max_nodes &&
  stats.encoded_bytes <= max_encoded_bytes &&
  stats.max_depth <= max_depth
}

///|
fn document_contains_text_at(value : CborValue, target : String) -> Bool {
  match value {
    Text(text) => text == target
    Array(items) => {
      let mut found = false
      for item in items {
        if document_contains_text_at(item, target) {
          found = true
        }
      }
      found
    }
    Map(entries) => {
      let mut found = false
      for entry in entries {
        if document_contains_text_at(entry.0, target) ||
          document_contains_text_at(entry.1, target) {
          found = true
        }
      }
      found
    }
    Tag(_, item) => document_contains_text_at(item, target)
    _ => false
  }
}

///|
/// Search both object keys and values for an exact text value.
pub fn cbor_document_contains_text(value : CborValue, target : String) -> Bool {
  document_contains_text_at(value, target)
}

///|
fn redact_at(
  value : CborValue,
  fields : Array[String],
  replacement : CborValue,
) -> CborValue {
  match value {
    Array(items) => {
      let result = []
      for item in items {
        result.push(redact_at(item, fields, replacement))
      }
      Array(result)
    }
    Map(entries) => {
      let result = []
      for entry in entries {
        match entry.0 {
          Text(name) => {
            let mut should_redact = false
            for field in fields {
              if field == name {
                should_redact = true
              }
            }
            result.push(
              (
                entry.0,
                if should_redact {
                  replacement
                } else {
                  redact_at(entry.1, fields, replacement)
                },
              ),
            )
          }
          _ =>
            result.push(
              (
                redact_at(entry.0, fields, replacement),
                redact_at(entry.1, fields, replacement),
              ),
            )
        }
      }
      Map(result)
    }
    Tag(tag, item) => Tag(tag, redact_at(item, fields, replacement))
    _ => value
  }
}

///|
/// Recursively replace values whose object key is in a sensitive-field list.
pub fn cbor_redact_fields(
  value : CborValue,
  fields : Array[String],
  replacement : CborValue,
) -> CborValue {
  redact_at(value, fields, replacement)
}

///|
fn collect_leaf_paths_at(
  value : CborValue,
  path : String,
  max_depth : Int,
  result : Array[String],
) -> Unit {
  if cbor_value_depth(value) > max_depth {
    result.push(path + "[depth-limit]")
    return
  }
  match value {
    Array(items) =>
      for i = 0; i < items.length(); i = i + 1 {
        collect_leaf_paths_at(
          items[i],
          path + "[" + i.to_string() + "]",
          max_depth,
          result,
        )
      }
    Map(entries) =>
      for entry in entries {
        match entry.0 {
          Text(name) =>
            collect_leaf_paths_at(entry.1, path + "." + name, max_depth, result)
          _ =>
            collect_leaf_paths_at(entry.1, path + ".[key]", max_depth, result)
        }
      }
    Tag(_, item) =>
      collect_leaf_paths_at(item, path + ".tag", max_depth, result)
    _ => result.push(path)
  }
}

///|
/// Collect leaf paths for audit logs without exposing leaf contents.
pub fn cbor_document_leaf_paths(
  value : CborValue,
  max_depth : Int,
) -> Array[String] {
  let result = []
  collect_leaf_paths_at(
    value,
    "$",
    if max_depth < 1 {
      1
    } else {
      max_depth
    },
    result,
  )
  result
}

///|
/// Return the number of map entries, including nested objects.
pub fn cbor_document_field_count(value : CborValue) -> Int {
  match value {
    Map(entries) => {
      let mut count = entries.length()
      for entry in entries {
        count = count + cbor_document_field_count(entry.0)
        count = count + cbor_document_field_count(entry.1)
      }
      count
    }
    Array(items) => {
      let mut count = 0
      for item in items {
        count = count + cbor_document_field_count(item)
      }
      count
    }
    Tag(_, item) => cbor_document_field_count(item)
    _ => 0
  }
}

///|
/// Return the number of byte-string payloads in a document.
pub fn cbor_document_binary_count(value : CborValue) -> Int {
  match value {
    Bytes(_) => 1
    Array(items) => {
      let mut count = 0
      for item in items {
        count = count + cbor_document_binary_count(item)
      }
      count
    }
    Map(entries) => {
      let mut count = 0
      for entry in entries {
        count = count + cbor_document_binary_count(entry.0)
        count = count + cbor_document_binary_count(entry.1)
      }
      count
    }
    Tag(_, item) => cbor_document_binary_count(item)
    _ => 0
  }
}

///|
/// Return the number of text values in a document.
pub fn cbor_document_text_count(value : CborValue) -> Int {
  match value {
    Text(_) => 1
    Array(items) => {
      let mut count = 0
      for item in items {
        count = count + cbor_document_text_count(item)
      }
      count
    }
    Map(entries) => {
      let mut count = 0
      for entry in entries {
        count = count + cbor_document_text_count(entry.0)
        count = count + cbor_document_text_count(entry.1)
      }
      count
    }
    Tag(_, item) => cbor_document_text_count(item)
    _ => 0
  }
}

///|
/// Return the largest byte-string payload length in a document.
pub fn cbor_document_largest_binary(value : CborValue) -> Int {
  match value {
    Bytes(bytes) => bytes.length()
    Array(items) => {
      let mut largest = 0
      for item in items {
        let size = cbor_document_largest_binary(item)
        if size > largest {
          largest = size
        }
      }
      largest
    }
    Map(entries) => {
      let mut largest = 0
      for entry in entries {
        let key_size = cbor_document_largest_binary(entry.0)
        let value_size = cbor_document_largest_binary(entry.1)
        if key_size > largest {
          largest = key_size
        }
        if value_size > largest {
          largest = value_size
        }
      }
      largest
    }
    Tag(_, item) => cbor_document_largest_binary(item)
    _ => 0
  }
}

///|
/// Count numeric leaves, including integer and floating-point values.
pub fn cbor_document_numeric_count(value : CborValue) -> Int {
  match value {
    Integer(_) | Unsigned(_) | Float64(_) => 1
    Array(items) => {
      let mut count = 0
      for item in items {
        count = count + cbor_document_numeric_count(item)
      }
      count
    }
    Map(entries) => {
      let mut count = 0
      for entry in entries {
        count = count + cbor_document_numeric_count(entry.0)
        count = count + cbor_document_numeric_count(entry.1)
      }
      count
    }
    Tag(_, item) => cbor_document_numeric_count(item)
    _ => 0
  }
}

///|
/// Count tagged values to support application-specific extension audits.
pub fn cbor_document_tag_count(value : CborValue) -> Int {
  match value {
    Tag(_, item) => 1 + cbor_document_tag_count(item)
    Array(items) => {
      let mut count = 0
      for item in items {
        count = count + cbor_document_tag_count(item)
      }
      count
    }
    Map(entries) => {
      let mut count = 0
      for entry in entries {
        count = count + cbor_document_tag_count(entry.0)
        count = count + cbor_document_tag_count(entry.1)
      }
      count
    }
    _ => 0
  }
}

///|
/// Create a stable summary for an observability event.
pub fn cbor_document_summary(value : CborValue) -> String {
  let stats = cbor_document_stats(value)
  "nodes=\{stats.nodes} containers=\{stats.containers} depth=\{stats.max_depth} encoded=\{stats.encoded_bytes} text_bytes=\{stats.text_bytes} binary_bytes=\{stats.binary_bytes}"
}

///|
/// Return a redacted preview bounded by a character budget.
pub fn cbor_redacted_preview(
  value : CborValue,
  fields : Array[String],
  max_chars : Int,
) -> String {
  cbor_query_preview(
    cbor_redact_fields(value, fields, Text("")),
    max_chars,
  )
}