///|
pub suberror ConfigError {
  InvalidConfig(String, String)
} derive(Debug)

///|
pub extend ConfigError with @moonbitlang/core/debug.Debug::{to_repr}

///|
fn object_fields(
  value : Json,
  path : String,
) -> Map[String, Json] raise ConfigError {
  match value {
    Object(fields) => fields
    _ => raise InvalidConfig(path, "Expected an object")
  }
}

///|
fn required(
  fields : Map[String, Json],
  name : String,
  path : String,
) -> Json raise ConfigError {
  match fields.get(name) {
    Some(value) => value
    None =>
      raise InvalidConfig(path + "." + name, "Required property is missing")
  }
}

///|
fn text_value(value : Json, path : String) -> String raise ConfigError {
  match value {
    String(text) => text
    _ => raise InvalidConfig(path, "Expected a string")
  }
}

///|
fn integer_value(value : Json, path : String) -> Int raise ConfigError {
  match value {
    Number(number, ..) => {
      if number < -2147483648.0 || number > 2147483647.0 {
        raise InvalidConfig(path, "Integer outside Int32 bounds")
      }
      let result = number.to_int()
      if result.to_double() != number {
        raise InvalidConfig(path, "Expected an integer")
      }
      result
    }
    _ => raise InvalidConfig(path, "Expected a JSON number")
  }
}

///|
fn bool_value(value : Json, path : String) -> Bool raise ConfigError {
  match value {
    True => true
    False => false
    _ => raise InvalidConfig(path, "Expected a boolean")
  }
}

///|
fn array_value(value : Json, path : String) -> Array[Json] raise ConfigError {
  match value {
    Array(items) => items
    _ => raise InvalidConfig(path, "Expected an array")
  }
}

///|
fn scalar_value(value : Json, path : String) -> Value raise ConfigError {
  match value {
    Null => Null
    True => Boolean(true)
    False => Boolean(false)
    String(text) => Text(text)
    Number(_) => Integer(integer_value(value, path))
    _ => raise InvalidConfig(path, "Only scalar values are supported")
  }
}

///|
fn reject_unknown(
  fields : Map[String, Json],
  allowed : Array[String],
  path : String,
) -> Unit raise ConfigError {
  for name, _ in fields {
    if !allowed.contains(name) {
      raise InvalidConfig(path + "." + name, "Unknown property")
    }
  }
}

///|
fn parse_generator(value : Json, path : String) -> Generator raise ConfigError {
  let fields = object_fields(value, path)
  let kind = text_value(required(fields, "kind", path), path + ".kind")
  fn text(name : String) -> String raise ConfigError {
    text_value(required(fields, name, path), path + "." + name)
  }
  fn int(name : String) -> Int raise ConfigError {
    integer_value(required(fields, name, path), path + "." + name)
  }
  let allowed = match kind {
    "constant" => ["kind", "value"]
    "sequence" => ["kind", "start", "step"]
    "integer" => ["kind", "min", "max"]
    "date_offset" => ["kind", "min", "max"]
    "boolean" => ["kind", "chance"]
    "choice" => ["kind", "values"]
    "weighted_choice" => ["kind", "values"]
    "pattern" => ["kind", "alphabet", "length"]
    "reference" => ["kind", "entity", "key"]
    "copy" => ["kind", "field"]
    "add" | "multiply" => ["kind", "left", "right"]
    "concat" => ["kind", "fields", "separator"]
    "lookup" => ["kind", "field", "entity", "key", "value"]
    _ => raise InvalidConfig(path + ".kind", "Unknown generator: " + kind)
  }
  reject_unknown(fields, allowed, path)
  match kind {
    "constant" =>
      Constant(scalar_value(required(fields, "value", path), path + ".value"))
    "sequence" => Sequence(int("start"), int("step"))
    "integer" => IntegerRange(int("min"), int("max"))
    "date_offset" => DateOffset(int("min"), int("max"))
    "boolean" => BooleanChance(int("chance"))
    "choice" => {
      let items = array_value(
        required(fields, "values", path),
        path + ".values",
      )
      Choice(items.map(v => scalar_value(v, path + ".values")))
    }
    "weighted_choice" => {
      let items = array_value(
        required(fields, "values", path),
        path + ".values",
      )
      let values : Array[(Value, Int)] = []
      for index in 0.. Pattern(text("alphabet"), int("length"))
    "reference" => Reference(text("entity"), text("key"))
    "copy" => Copy(text("field"))
    "add" => Add(text("left"), text("right"))
    "multiply" => Multiply(text("left"), text("right"))
    "concat" => {
      let names = array_value(
        required(fields, "fields", path),
        path + ".fields",
      )
      Concat(names.map(v => text_value(v, path + ".fields")), text("separator"))
    }
    _ => Lookup(text("field"), text("entity"), text("key"), text("value"))
  }
}

///|
fn parse_field(value : Json, path : String) -> Field raise ConfigError {
  let fields = object_fields(value, path)
  reject_unknown(
    fields,
    ["name", "generator", "unique", "primary", "null_per_mille"],
    path,
  )
  let name = text_value(required(fields, "name", path), path + ".name")
  let generator = parse_generator(
    required(fields, "generator", path),
    path + ".generator",
  )
  let unique = match fields.get("unique") {
    Some(v) => bool_value(v, path + ".unique")
    None => false
  }
  let primary = match fields.get("primary") {
    Some(v) => bool_value(v, path + ".primary")
    None => false
  }
  let null_per_mille = match fields.get("null_per_mille") {
    Some(v) => integer_value(v, path + ".null_per_mille")
    None => 0
  }
  Field::new(name, generator, unique~, primary~, null_per_mille~)
}

///|
fn decode_model(value : Json) -> Model raise ConfigError {
  let fields = object_fields(value, "model")
  reject_unknown(fields, ["name", "entities"], "model")
  let name = text_value(required(fields, "name", "model"), "model.name")
  let items = array_value(
    required(fields, "entities", "model"),
    "model.entities",
  )
  let entities : Array[Entity] = []
  for i in 0.. Result[Model, Issue] {
  if guard_json(text, max_units, "model") is Err(error) {
    return Err(error)
  }
  if max_units <= 0 || text.length() > max_units {
    return Err(
      issue("config_limit", "model", "Model text exceeds configured size"),
    )
  }
  try {
    let value = @json.parse(text)
    Ok(decode_model(value))
  } catch {
    InvalidConfig(path, message) => Err(issue("invalid_config", path, message))
    _ => Err(issue("invalid_json", "model", "Invalid JSON syntax"))
  }
}