///|
fn review_fields(keys : Array[String]) -> Array[String] raise TraceError {
  if keys.length() > 256 {
    reject("field_request_limit", "", "Request at most 256 metadata fields.")
  }
  let seen : Map[String, Bool] = Map([])
  for key in keys {
    if seen.contains(key) {
      reject("duplicate_field_request", key, "Request each field only once.")
    }
    seen[key] = true
  }
  keys.copy()
}

///|
fn observation_value(i : Int32, path : String) -> Json {
  Json::object({
    "query_index": Json::number(i.to_double()),
    "data_path": Json::string(path),
  })
}

///|
fn history_value(history : Array[FieldOrigin]) -> Json {
  Json::array(
    history.map(origin => {
      Json::object({ "path": Json::string(origin.path), "value": origin.value })
    }),
  )
}

///|
/// Exact whole-value groups over an audit, retaining duplicate requests and errors.
pub struct FieldSummaryReport {
  priv document : Json
  priv errors : Int32
}

///|
pub fn FieldSummaryReport::error_count(self : FieldSummaryReport) -> Int32 {
  self.errors
}

///|
/// A defensive structured view; numeric source tokens remain intact.
pub fn FieldSummaryReport::to_json_value(self : FieldSummaryReport) -> Json {
  copy_json(self.document)
}

///|
pub fn FieldSummaryReport::to_json(self : FieldSummaryReport) -> String {
  canonical_json(self.document)
}

///|
/// Missing means resolved and absent; unresolved queries are reported separately.
/// Group identity is exact canonical JSON, not numeric/scientific equivalence.
pub fn AuditReport::summarize_fields(
  self : AuditReport,
  keys : Array[String],
) -> FieldSummaryReport raise TraceError {
  let fields = review_fields(keys)
  let errors : Array[Json] = []
  for i, entry in self.entries {
    if entry.error is Some(error) {
      errors.push(
        Json::object({
          "query_index": Json::number(i.to_double()),
          "data_path": Json::string(entry.path),
          "diagnostic": error.to_json(),
        }),
      )
    }
  }
  let summaries = fields.map(key => {
    let values : Map[String, Json] = Map([])
    let groups : Map[String, Array[Json]] = Map([])
    let missing : Array[Json] = []
    for i, entry in self.entries {
      if entry.result is Some(result) {
        match result.values.get(key) {
          None => missing.push(observation_value(i, entry.path))
          Some(value) => {
            let token = canonical_json(value)
            if !groups.contains(token) {
              groups[token] = []
              values[token] = value
            }
            let history = result.origins[key]
            groups[token].push(
              Json::object({
                "query_index": Json::number(i.to_double()),
                "data_path": Json::string(entry.path),
                "winning_source": Json::string(
                  history[history.length() - 1].path,
                ),
                "trace": history_value(history),
              }),
            )
          }
        }
      }
    }
    let tokens = sorted_keys(values)
    Json::object({
      "field": Json::string(key),
      "missing": Json::array(missing),
      "groups": Json::array(
        tokens.map(token => {
          Json::object({
            "value": values[token],
            "observations": Json::array(groups[token]),
          })
        }),
      ),
    })
  })
  {
    errors: errors.length(),
    document: Json::object({
      "query_count": Json::number(self.entries.length().to_double()),
      "resolved_count": Json::number(
        (self.entries.length() - errors.length()).to_double(),
      ),
      "error_count": Json::number(errors.length().to_double()),
      "errors": Json::array(errors),
      "fields": Json::array(summaries),
    }),
  }
}