///|
/// Counts and shape measurements for one JSON document.
pub(all) struct JsonStats {
  mut value_count : Int
  mut object_count : Int
  mut array_count : Int
  mut string_count : Int
  mut number_count : Int
  mut boolean_count : Int
  mut null_count : Int
  mut max_depth : Int
  mut max_array_length : Int
  mut max_object_members : Int
}

///|
/// The result of inspecting a JSON document. A failed report still contains
/// the zero stats value so callers can render one stable response shape.
pub(all) struct AnalysisReport {
  valid : Bool
  stats : JsonStats
  canonical : String?
  digest : String?
  error : String?
}

///|
pub fn JsonStats::empty() -> JsonStats {
  {
    value_count: 0,
    object_count: 0,
    array_count: 0,
    string_count: 0,
    number_count: 0,
    boolean_count: 0,
    null_count: 0,
    max_depth: 0,
    max_array_length: 0,
    max_object_members: 0,
  }
}

///|
/// Parse, analyze, canonicalize, and hash one JSON document in one call.
pub fn inspect_json(input : String) -> AnalysisReport {
  let value = match parse(input) {
    Ok(value) => value
    Err(error) =>
      return {
        valid: false,
        stats: JsonStats::empty(),
        canonical: None,
        digest: None,
        error: Some(error.message()),
      }
  }
  let stats = JsonStats::empty()
  collect_stats(value, 0, stats)
  let canonical = match canonicalize(value) {
    Ok(serialized) => serialized
    Err(error) =>
      return {
        valid: false,
        stats,
        canonical: None,
        digest: None,
        error: Some(error.message()),
      }
  }
  let digest = match canonical_digest(value) {
    Ok(digest) => digest
    Err(error) =>
      return {
        valid: false,
        stats,
        canonical: Some(canonical),
        digest: None,
        error: Some(error.message()),
      }
  }
  {
    valid: true,
    stats,
    canonical: Some(canonical),
    digest: Some(digest),
    error: None,
  }
}

///|
fn collect_stats(value : JsonValue, depth : Int, stats : JsonStats) -> Unit {
  stats.value_count = stats.value_count + 1
  if depth > stats.max_depth {
    stats.max_depth = depth
  }
  match value {
    Null => stats.null_count = stats.null_count + 1
    Bool(_) => stats.boolean_count = stats.boolean_count + 1
    Number(_) => stats.number_count = stats.number_count + 1
    String(_) => stats.string_count = stats.string_count + 1
    Array(values) => {
      stats.array_count = stats.array_count + 1
      if values.length() > stats.max_array_length {
        stats.max_array_length = values.length()
      }
      for child in values {
        collect_stats(child, depth + 1, stats)
      }
    }
    Object(entries) => {
      stats.object_count = stats.object_count + 1
      if entries.length() > stats.max_object_members {
        stats.max_object_members = entries.length()
      }
      for entry in entries {
        collect_stats(entry.1, depth + 1, stats)
      }
    }
  }
}

///|
/// Render the metrics as a deterministic JSON object for scripts and CI.
pub fn JsonStats::to_json(self : JsonStats) -> String {
  "{\"value_count\":" +
  self.value_count.to_string() +
  ",\"object_count\":" +
  self.object_count.to_string() +
  ",\"array_count\":" +
  self.array_count.to_string() +
  ",\"string_count\":" +
  self.string_count.to_string() +
  ",\"number_count\":" +
  self.number_count.to_string() +
  ",\"boolean_count\":" +
  self.boolean_count.to_string() +
  ",\"null_count\":" +
  self.null_count.to_string() +
  ",\"max_depth\":" +
  self.max_depth.to_string() +
  ",\"max_array_length\":" +
  self.max_array_length.to_string() +
  ",\"max_object_members\":" +
  self.max_object_members.to_string() +
  "}"
}

///|
/// Render a report as a stable, line-oriented summary suitable for terminals.
pub fn AnalysisReport::to_text(self : AnalysisReport) -> String {
  let status = if self.valid { "valid" } else { "invalid" }
  let mut output = "status: " + status + "\n"
  output = output + "stats: " + self.stats.to_json() + "\n"
  match self.digest {
    Some(digest) => output = output + "sha256: " + digest + "\n"
    None => ()
  }
  match self.error {
    Some(error) => output = output + "error: " + error + "\n"
    None => ()
  }
  output
}

///|
/// Render a report as JSON without relying on a host JSON implementation.
pub fn AnalysisReport::to_json(self : AnalysisReport) -> String {
  let canonical = match self.canonical {
    Some(value) => canonical_string(value)
    None => "null"
  }
  let digest = match self.digest {
    Some(value) => canonical_string(value)
    None => "null"
  }
  let error = match self.error {
    Some(value) => canonical_string(value)
    None => "null"
  }
  "{\"valid\":" +
  (if self.valid { "true" } else { "false" }) +
  ",\"stats\":" +
  self.stats.to_json() +
  ",\"canonical\":" +
  canonical +
  ",\"digest\":" +
  digest +
  ",\"error\":" +
  error +
  "}"
}