///|
fn object_fields(
  value : Json,
  allowed : Array[String],
) -> Map[String, Json] raise AllenError {
  guard value is Object(fields) else {
    raise InvalidFormat("expected JSON object")
  }
  for key, _ in fields {
    if !allowed.contains(key) {
      raise InvalidFormat("unknown JSON field: " + key)
    }
  }
  if fields.length() != allowed.length() {
    raise InvalidFormat("missing JSON field")
  }
  fields
}

///|
fn field(fields : Map[String, Json], name : String) -> Json raise AllenError {
  match fields.get(name) {
    Some(value) => value
    None => raise InvalidFormat("missing JSON field: " + name)
  }
}

///|
fn json_string(value : Json) -> String raise AllenError {
  match value {
    String(text) => text
    _ => raise InvalidFormat("expected JSON string")
  }
}

///|
fn json_array(value : Json) -> Array[Json] raise AllenError {
  match value {
    Array(values) => values
    _ => raise InvalidFormat("expected JSON array")
  }
}

///|
fn constraint_json(c : Constraint) -> Json {
  Json::object({
    "left": Json::string(c.left),
    "right": Json::string(c.right),
    "allowed": Json::string(c.allowed.render()),
    "label": Json::string(c.label),
  })
}

///|
/// Schema-strict value decoder. Every object field is required; unknown fields
/// and non-string relation sets are rejected. Empty relation sets are legal.
pub fn network_from_json(value : Json) -> Network raise AllenError {
  let fields = object_fields(value, ["format", "names", "constraints"])
  if json_string(field(fields, "format")) != "moonallen/1" {
    raise InvalidFormat("unsupported MoonAllen JSON format")
  }
  let names_json = json_array(field(fields, "names"))
  if names_json.length() > 64 {
    raise TooLarge(names_json.length())
  }
  let names = names_json.map(json_string)
  let rows = json_array(field(fields, "constraints"))
  if rows.length() > 4096 {
    raise TooLarge(rows.length())
  }
  let mut net = Network::new(names)
  for row in rows {
    let c = object_fields(row, ["left", "right", "allowed", "label"])
    net = net.constrain({
      left: json_string(field(c, "left")),
      right: json_string(field(c, "right")),
      allowed: parse_relset(json_string(field(c, "allowed"))),
      label: json_string(field(c, "label")),
    })
  }
  net
}

///|
/// Bounded core JSON parser (1 Mi UTF-16 units, depth 16). Duplicate object keys
/// follow core JSON last-key-wins semantics; use unique keys for interchange.
pub fn parse_network_json(text : String) -> Network raise AllenError {
  if text.length() > 1048576 {
    raise TooLarge(text.length())
  }
  let mut depth = 0
  let mut quoted = false
  let mut escaped = false
  for c in text.iter() {
    if quoted {
      if escaped {
        escaped = false
      } else if c == '\\' {
        escaped = true
      } else if c == '"' {
        quoted = false
      }
    } else if c == '"' {
      quoted = true
    } else if c == '{' || c == '[' {
      depth = depth + 1
      if depth > 16 {
        raise InvalidFormat("JSON depth exceeds 16")
      }
    } else if c == '}' || c == ']' {
      depth = depth - 1
    }
  }
  let value = @json.parse(text) catch {
    _ => raise InvalidFormat("malformed or over-deep JSON")
  }
  network_from_json(value)
}

///|
/// Current-domain snapshot. Original labels are not attributed to deductions.
pub fn Network::to_json(self : Network) -> Json {
  Json::object({
    "format": Json::string("moonallen/1"),
    "names": Json::array(self.names.map(name => Json::string(name))),
    "constraints": Json::array(
      self.domain_constraints(label="snapshot").map(constraint_json),
    ),
  })
}

///|
/// Export original evidence separately. For propagated networks, decoding this
/// reconstructs the input domains, not the narrower inferred snapshot.
pub fn Network::sources_json(self : Network) -> Json {
  Json::object({
    "format": Json::string("moonallen/1"),
    "names": Json::array(self.names.map(name => Json::string(name))),
    "constraints": Json::array(self.sources.map(constraint_json)),
  })
}