///|
/// Schema inference over fact samples.
///
/// The infer operation walks fact records and derives a dotted-path to
/// type-name mapping usable by the check operation's optional schema.
/// Paths observed with more than one type are reported separately as
/// `mixed` instead of a single type, so downstream checks never receive
/// an ambiguous schema entry.

///|
fn walk_schema(
  value : Value,
  prefix : String,
  observed : Map[String, Map[String, Bool]],
) -> Unit {
  match value {
    Object(fields) =>
      for key, item in fields {
        let path = if prefix == "" { key } else { prefix + "." + key }
        match item {
          Object(_) => {
            note_schema_type(observed, path, "object")
            walk_schema(item, path, observed)
          }
          Array(_) => note_schema_type(observed, path, "array")
          _ => note_schema_type(observed, path, type_name(item))
        }
      }
    _ => ()
  }
}

///|
fn note_schema_type(
  observed : Map[String, Map[String, Bool]],
  path : String,
  kind : String,
) -> Unit {
  match observed.get(path) {
    Some(kinds) => kinds[kind] = true
    None => {
      let kinds : Map[String, Bool] = Map([])
      kinds[kind] = true
      observed[path] = kinds
    }
  }
}

///|
fn infer_request(request : Value) -> Value {
  let facts = match get(request, "facts") {
    Array(items) => items
    other => [other]
  }
  let observed : Map[String, Map[String, Bool]] = Map([])
  for fact in facts {
    walk_schema(fact, "", observed)
  }
  let paths : Array[String] = []
  for path, _ in observed {
    paths.push(path)
  }
  paths.sort_by(fn(a, b) { stable_string_compare(a, b) })
  let schema : Map[String, Value] = Map([])
  let mixed : Array[Value] = []
  for path in paths {
    let kinds = observed[path]
    let names : Array[String] = []
    for kind, _ in kinds {
      names.push(kind)
    }
    names.sort_by(fn(a, b) { stable_string_compare(a, b) })
    if names.length() == 1 {
      schema[path] = String(names[0])
    } else {
      mixed.push(record([("path", String(path)), ("types", strings(names))]))
    }
  }
  record([
    ("schema", Object(schema)),
    ("mixed", Array(mixed)),
    ("scanned", Number(facts.length().to_double())),
  ])
}