///|
/// A location inside a CBOR document.
pub(all) enum CborPathSegment {
  Key(String)
  Index(Int)
} derive(Eq, Debug)

///|
/// Return a readable path fragment for diagnostics and logs.
pub fn cbor_path_segment_to_string(segment : CborPathSegment) -> String {
  match segment {
    Key(name) => "." + name
    Index(index) => "[" + index.to_string() + "]"
  }
}

///|
/// Format a path without introducing a dependency on a JSON pointer package.
pub fn cbor_path_to_string(path : Array[CborPathSegment]) -> String {
  let result = StringBuilder::new()
  if path.length() == 0 {
    result.write_string("$")
  } else {
    result.write_string("$")
    for segment in path {
      result.write_string(cbor_path_segment_to_string(segment))
    }
  }
  result.to_string()
}

///|
fn query_failure(path : Array[CborPathSegment], message : String) -> CborError {
  SemanticError("query " + cbor_path_to_string(path) + ": " + message)
}

///|
fn query_failure_at(
  path : Array[CborPathSegment],
  segment : CborPathSegment,
  message : String,
) -> CborError {
  let extended = []
  for item in path {
    extended.push(item)
  }
  extended.push(segment)
  query_failure(extended, message)
}

///|
fn map_value(value : CborValue, key : String) -> CborValue? {
  match value {
    Map(entries) => {
      for entry in entries {
        match entry.0 {
          Text(name) => if name == key { return Some(entry.1) }
          _ => ()
        }
      }
      None
    }
    _ => None
  }
}

///|
fn map_value_required(
  value : CborValue,
  key : String,
  path : Array[CborPathSegment],
) -> CborValue raise CborError {
  match map_value(value, key) {
    Some(found) => found
    None => raise query_failure_at(path, Key(key), "object field is missing")
  }
}

///|
fn array_value(
  value : CborValue,
  index : Int,
  path : Array[CborPathSegment],
) -> CborValue raise CborError {
  match value {
    Array(items) => {
      if index < 0 || index >= items.length() {
        raise query_failure_at(
          path,
          Index(index),
          "array index is out of range",
        )
      }
      items[index]
    }
    _ => raise query_failure_at(path, Index(index), "value is not an array")
  }
}

///|
/// Resolve a path one segment at a time and return the selected value.
pub fn cbor_query_at(
  value : CborValue,
  path : Array[CborPathSegment],
) -> CborValue raise CborError {
  let mut current = value
  let consumed = []
  for segment in path {
    match segment {
      Key(name) => current = map_value_required(current, name, consumed)
      Index(index) => current = array_value(current, index, consumed)
    }
    consumed.push(segment)
  }
  current
}

///|
/// Resolve a path, returning `None` only when a field is absent.
pub fn cbor_query_optional(
  value : CborValue,
  path : Array[CborPathSegment],
) -> CborValue? raise CborError {
  if path.length() == 0 {
    return Some(value)
  }
  let mut current = value
  let consumed = []
  for segment in path {
    match segment {
      Key(name) =>
        match current {
          Map(entries) => {
            let mut found = None
            for entry in entries {
              match entry.0 {
                Text(key) => if key == name { found = Some(entry.1) }
                _ => ()
              }
            }
            match found {
              Some(next) => current = next
              None => return None
            }
          }
          _ =>
            raise query_failure_at(
              consumed,
              Key(name),
              "value is not an object",
            )
        }
      Index(index) => current = array_value(current, index, consumed)
    }
    consumed.push(segment)
  }
  Some(current)
}

///|
/// Read a text field at a nested path.
pub fn cbor_query_text(
  value : CborValue,
  path : Array[CborPathSegment],
) -> String raise CborError {
  match cbor_query_at(value, path) {
    Text(text) => text
    _ => raise query_failure(path, "expected text")
  }
}

///|
/// Read an integer field at a nested path without narrowing unsafely.
pub fn cbor_query_int64(
  value : CborValue,
  path : Array[CborPathSegment],
) -> Int64 raise CborError {
  match cbor_query_at(value, path) {
    Integer(number) => number
    Unsigned(number) =>
      if number > 0x7FFFFFFFFFFFFFFFUL {
        raise query_failure(path, "unsigned integer exceeds Int64 range")
      } else {
        number.reinterpret_as_int64()
      }
    _ => raise query_failure(path, "expected integer")
  }
}

///|
/// Read a full-width unsigned integer field.
pub fn cbor_query_uint64(
  value : CborValue,
  path : Array[CborPathSegment],
) -> UInt64 raise CborError {
  match cbor_query_at(value, path) {
    Unsigned(number) => number
    Integer(number) =>
      if number < 0L {
        raise query_failure(path, "negative integer cannot be UInt64")
      } else {
        number.reinterpret_as_uint64()
      }
    _ => raise query_failure(path, "expected unsigned integer")
  }
}

///|
/// Read a boolean field at a nested path.
pub fn cbor_query_bool(
  value : CborValue,
  path : Array[CborPathSegment],
) -> Bool raise CborError {
  match cbor_query_at(value, path) {
    Simple(20) => false
    Simple(21) => true
    _ => raise query_failure(path, "expected bool")
  }
}

///|
/// Read a byte string field at a nested path.
pub fn cbor_query_bytes(
  value : CborValue,
  path : Array[CborPathSegment],
) -> Bytes raise CborError {
  match cbor_query_at(value, path) {
    Bytes(bytes) => bytes
    _ => raise query_failure(path, "expected byte string")
  }
}

///|
/// Read a floating-point field, accepting integer CBOR numbers as well.
pub fn cbor_query_double(
  value : CborValue,
  path : Array[CborPathSegment],
) -> Double raise CborError {
  match cbor_query_at(value, path) {
    Float64(number) => number
    Integer(number) => number.to_double()
    Unsigned(number) => number.to_double()
    _ => raise query_failure(path, "expected number")
  }
}

///|
/// Return whether an object contains a text key.
pub fn cbor_object_has(value : CborValue, key : String) -> Bool {
  match map_value(value, key) {
    Some(_) => true
    None => false
  }
}

///|
/// Read a top-level object field without constructing a path.
pub fn cbor_object_get(value : CborValue, key : String) -> CborValue? {
  map_value(value, key)
}

///|
/// Return all text keys in insertion order.
pub fn cbor_object_keys(value : CborValue) -> Array[String] {
  let keys = []
  match value {
    Map(entries) =>
      for entry in entries {
        match entry.0 {
          Text(name) => keys.push(name)
          _ => ()
        }
      }
    _ => ()
  }
  keys
}

///|
/// Project an object onto a list of keys, preserving the requested order.
pub fn cbor_object_project(
  value : CborValue,
  keys : Array[String],
) -> CborValue raise CborError {
  match value {
    Map(_) => {
      let result = []
      for key in keys {
        match map_value(value, key) {
          Some(found) => result.push((Text(key), found))
          None =>
            raise SemanticError("query projection field is missing: " + key)
        }
      }
      Map(result)
    }
    _ => raise SemanticError("query projection requires an object")
  }
}

///|
/// Return a copy of an object with a text field inserted or replaced.
pub fn cbor_object_set(
  value : CborValue,
  key : String,
  replacement : CborValue,
) -> CborValue raise CborError {
  match value {
    Map(entries) => {
      let result = []
      let mut replaced = false
      for entry in entries {
        match entry.0 {
          Text(name) if name == key => {
            result.push((Text(key), replacement))
            replaced = true
          }
          _ => result.push(entry)
        }
      }
      if !replaced {
        result.push((Text(key), replacement))
      }
      Map(result)
    }
    _ => raise SemanticError("object set requires an object")
  }
}

///|
/// Return a copy of an object with a field removed.
pub fn cbor_object_remove(
  value : CborValue,
  key : String,
) -> CborValue raise CborError {
  match value {
    Map(entries) => {
      let result = []
      for entry in entries {
        match entry.0 {
          Text(name) if name == key => ()
          _ => result.push(entry)
        }
      }
      Map(result)
    }
    _ => raise SemanticError("object remove requires an object")
  }
}

///|
/// Replace a nested path while preserving all untouched values.
pub fn cbor_set_path(
  value : CborValue,
  path : Array[CborPathSegment],
  replacement : CborValue,
) -> CborValue raise CborError {
  if path.length() == 0 {
    return replacement
  }
  let head = path[0]
  let tail = path[1:]
  match head {
    Key(name) => {
      let child = map_value_required(value, name, [])
      let updated = cbor_set_path(child, tail.to_owned(), replacement)
      cbor_object_set(value, name, updated)
    }
    Index(index) =>
      match value {
        Array(items) => {
          if index < 0 || index >= items.length() {
            raise query_failure(path, "array index is out of range")
          }
          let updated = cbor_set_path(
            items[index],
            tail.to_owned(),
            replacement,
          )
          let result = []
          for item in items {
            result.push(item)
          }
          result[index] = updated
          Array(result)
        }
        _ => raise query_failure(path, "value is not an array")
      }
  }
}

///|
/// Merge two objects; fields from `overlay` replace fields from `base`.
pub fn cbor_object_merge(
  base : CborValue,
  overlay : CborValue,
) -> CborValue raise CborError {
  match (base, overlay) {
    (Map(base_entries), Map(overlay_entries)) => {
      let result = []
      for entry in base_entries {
        result.push(entry)
      }
      for overlay_entry in overlay_entries {
        match overlay_entry.0 {
          Text(name) => {
            let mut replaced = false
            for i = 0; i < result.length(); i = i + 1 {
              match result[i].0 {
                Text(existing) if existing == name => {
                  result[i] = (Text(name), overlay_entry.1)
                  replaced = true
                }
                _ => ()
              }
            }
            if !replaced {
              result.push(overlay_entry)
            }
          }
          _ => raise SemanticError("object merge requires text keys")
        }
      }
      Map(result)
    }
    _ => raise SemanticError("object merge requires two objects")
  }
}

///|
/// Convert a value to a compact log-safe diagnostic with a length limit.
pub fn cbor_query_preview(value : CborValue, max_chars : Int) -> String {
  let rendered = diagnostic(value)
  if max_chars <= 0 {
    return ""
  }
  if rendered.length() <= max_chars {
    rendered
  } else {
    rendered[0:max_chars].to_owned() + "..."
  }
}

///|
/// Count descendants in a document for lightweight telemetry.
pub fn cbor_value_node_count(value : CborValue) -> Int {
  match value {
    Array(items) => {
      let mut total = 1
      for item in items {
        total = total + cbor_value_node_count(item)
      }
      total
    }
    Map(entries) => {
      let mut total = 1
      for entry in entries {
        total = total + cbor_value_node_count(entry.0)
        total = total + cbor_value_node_count(entry.1)
      }
      total
    }
    Tag(_, item) => 1 + cbor_value_node_count(item)
    _ => 1
  }
}

///|
/// Return the maximum nesting depth of a document.
pub fn cbor_value_depth(value : CborValue) -> Int {
  match value {
    Array(items) => {
      let mut deepest = 0
      for item in items {
        let depth = cbor_value_depth(item)
        if depth > deepest {
          deepest = depth
        }
      }
      deepest + 1
    }
    Map(entries) => {
      let mut deepest = 0
      for entry in entries {
        let key_depth = cbor_value_depth(entry.0)
        let value_depth = cbor_value_depth(entry.1)
        if key_depth > deepest {
          deepest = key_depth
        }
        if value_depth > deepest {
          deepest = value_depth
        }
      }
      deepest + 1
    }
    Tag(_, item) => 1 + cbor_value_depth(item)
    _ => 1
  }
}