// json_utils.mbt — private decode helpers shared by all lody codecs.
//
// Leniency rules (mirroring upstream TypeScript optionality):
// - optional fields: absent key and explicit JSON null both decode to None
// - required fields: absent or null key is a decode error
// - present but mistyped fields always error, naming the contract scope
//   (`lody.`) and the wire field name
// - unknown fields are ignored so newer senders stay decodable
// - numbers must be integral where the contract model is Int/Int64; the
//   check is exact (no silent truncation)

///|
/// Internal decode failure carrying a human-readable message. Public codecs
/// catch this and surface it as `Result`'s `Err` string.
priv suberror DecodeError {
  Msg(String)
}

///|
fn object_fields(
  value : Json,
  ctx : String,
) -> Map[String, Json] raise DecodeError {
  guard value is Json::Object(fields) else {
    raise DecodeError::Msg("\{ctx} must be a JSON object")
  }
  fields
}

///|
fn req_string(
  fields : Map[String, Json],
  ctx : String,
  key : String,
) -> String raise DecodeError {
  match fields.get(key) {
    Some(Json::String(s)) => s
    Some(Json::Null) | None =>
      raise DecodeError::Msg("\{ctx}: missing required field \"\{key}\"")
    Some(_) =>
      raise DecodeError::Msg("\{ctx}: field \"\{key}\" must be a string")
  }
}

///|
fn opt_string(
  fields : Map[String, Json],
  ctx : String,
  key : String,
) -> String? raise DecodeError {
  match fields.get(key) {
    None => None
    Some(Json::Null) => None
    Some(Json::String(s)) => Some(s)
    Some(_) =>
      raise DecodeError::Msg("\{ctx}: field \"\{key}\" must be a string")
  }
}

///|
fn req_bool(
  fields : Map[String, Json],
  ctx : String,
  key : String,
) -> Bool raise DecodeError {
  match fields.get(key) {
    Some(Json::True) => true
    Some(Json::False) => false
    Some(Json::Null) | None =>
      raise DecodeError::Msg("\{ctx}: missing required field \"\{key}\"")
    Some(_) =>
      raise DecodeError::Msg("\{ctx}: field \"\{key}\" must be a boolean")
  }
}

///|
fn opt_bool(
  fields : Map[String, Json],
  ctx : String,
  key : String,
) -> Bool? raise DecodeError {
  match fields.get(key) {
    None => None
    Some(Json::Null) => None
    Some(Json::True) => Some(true)
    Some(Json::False) => Some(false)
    Some(_) =>
      raise DecodeError::Msg("\{ctx}: field \"\{key}\" must be a boolean")
  }
}

///|
fn req_int(
  fields : Map[String, Json],
  ctx : String,
  key : String,
) -> Int raise DecodeError {
  match fields.get(key) {
    Some(Json::Number(d, ..)) =>
      if d != d.trunc() {
        raise DecodeError::Msg("\{ctx}: field \"\{key}\" must be an integer")
      } else {
        d.to_int64().to_int()
      }
    Some(Json::Null) | None =>
      raise DecodeError::Msg("\{ctx}: missing required field \"\{key}\"")
    Some(_) =>
      raise DecodeError::Msg("\{ctx}: field \"\{key}\" must be a number")
  }
}

///|
fn opt_int(
  fields : Map[String, Json],
  ctx : String,
  key : String,
) -> Int? raise DecodeError {
  match fields.get(key) {
    None => None
    Some(Json::Null) => None
    Some(Json::Number(d, ..)) =>
      if d != d.trunc() {
        raise DecodeError::Msg("\{ctx}: field \"\{key}\" must be an integer")
      } else {
        Some(d.to_int64().to_int())
      }
    Some(_) =>
      raise DecodeError::Msg("\{ctx}: field \"\{key}\" must be a number")
  }
}

///|
fn req_int64(
  fields : Map[String, Json],
  ctx : String,
  key : String,
) -> Int64 raise DecodeError {
  match fields.get(key) {
    Some(Json::Number(d, ..)) =>
      if d != d.trunc() {
        raise DecodeError::Msg("\{ctx}: field \"\{key}\" must be an integer")
      } else {
        d.to_int64()
      }
    Some(Json::Null) | None =>
      raise DecodeError::Msg("\{ctx}: missing required field \"\{key}\"")
    Some(_) =>
      raise DecodeError::Msg("\{ctx}: field \"\{key}\" must be a number")
  }
}

///|
fn opt_int64(
  fields : Map[String, Json],
  ctx : String,
  key : String,
) -> Int64? raise DecodeError {
  match fields.get(key) {
    None => None
    Some(Json::Null) => None
    Some(Json::Number(d, ..)) =>
      if d != d.trunc() {
        raise DecodeError::Msg("\{ctx}: field \"\{key}\" must be an integer")
      } else {
        Some(d.to_int64())
      }
    Some(_) =>
      raise DecodeError::Msg("\{ctx}: field \"\{key}\" must be a number")
  }
}

///|
fn req_double(
  fields : Map[String, Json],
  ctx : String,
  key : String,
) -> Double raise DecodeError {
  match fields.get(key) {
    Some(Json::Number(d, ..)) => d
    Some(Json::Null) | None =>
      raise DecodeError::Msg("\{ctx}: missing required field \"\{key}\"")
    Some(_) =>
      raise DecodeError::Msg("\{ctx}: field \"\{key}\" must be a number")
  }
}

///|
fn opt_double(
  fields : Map[String, Json],
  ctx : String,
  key : String,
) -> Double? raise DecodeError {
  match fields.get(key) {
    None => None
    Some(Json::Null) => None
    Some(Json::Number(d, ..)) => Some(d)
    Some(_) =>
      raise DecodeError::Msg("\{ctx}: field \"\{key}\" must be a number")
  }
}

///|
/// Required but nullable integer (upstream `number | null` without `?`):
/// the key must be present, but its value may be null.
fn req_int64_nullable(
  fields : Map[String, Json],
  ctx : String,
  key : String,
) -> Int64? raise DecodeError {
  match fields.get(key) {
    Some(Json::Null) => None
    Some(Json::Number(d, ..)) =>
      if d != d.trunc() {
        raise DecodeError::Msg("\{ctx}: field \"\{key}\" must be an integer")
      } else {
        Some(d.to_int64())
      }
    Some(_) =>
      raise DecodeError::Msg(
        "\{ctx}: field \"\{key}\" must be a number or null",
      )
    None => raise DecodeError::Msg("\{ctx}: missing required field \"\{key}\"")
  }
}

///|
/// Required but nullable sub-object (upstream `T | null` without `?`):
/// the key must be present, but its value may be null.
fn[T] req_sub_nullable(
  fields : Map[String, Json],
  ctx : String,
  key : String,
  parse : (Json) -> Result[T, String],
) -> T? raise DecodeError {
  match fields.get(key) {
    Some(Json::Null) => None
    Some(value) =>
      match parse(value) {
        Ok(parsed) => Some(parsed)
        Err(message) =>
          raise DecodeError::Msg("\{ctx}: field \"\{key}\": \{message}")
      }
    None => raise DecodeError::Msg("\{ctx}: missing required field \"\{key}\"")
  }
}

///|
fn req_array(
  fields : Map[String, Json],
  ctx : String,
  key : String,
) -> Array[Json] raise DecodeError {
  match fields.get(key) {
    Some(Json::Array(items)) => items
    Some(Json::Null) | None =>
      raise DecodeError::Msg("\{ctx}: missing required field \"\{key}\"")
    Some(_) =>
      raise DecodeError::Msg("\{ctx}: field \"\{key}\" must be an array")
  }
}

///|
fn opt_object(
  fields : Map[String, Json],
  ctx : String,
  key : String,
) -> Map[String, Json]? raise DecodeError {
  match fields.get(key) {
    None => None
    Some(Json::Null) => None
    Some(Json::Object(inner)) => Some(inner)
    Some(_) =>
      raise DecodeError::Msg("\{ctx}: field \"\{key}\" must be an object")
  }
}

///|
fn[T] opt_sub(
  fields : Map[String, Json],
  ctx : String,
  key : String,
  parse : (Json) -> Result[T, String],
) -> T? raise DecodeError {
  match fields.get(key) {
    None => None
    Some(Json::Null) => None
    Some(value) =>
      match parse(value) {
        Ok(parsed) => Some(parsed)
        Err(message) =>
          raise DecodeError::Msg("\{ctx}: field \"\{key}\": \{message}")
      }
  }
}

///|
/// Decode an optional string-literal-union field already read as `String?`
/// through its enum's `of_wire`.
fn[T] opt_wire(
  ctx : String,
  key : String,
  raw : String?,
  of_wire : (String) -> Result[T, String],
) -> T? raise DecodeError {
  match raw {
    None => None
    Some(s) => Some(wire_of(ctx, key, of_wire(s)))
  }
}

///|
fn[T] req_sub(
  fields : Map[String, Json],
  ctx : String,
  key : String,
  parse : (Json) -> Result[T, String],
) -> T raise DecodeError {
  match fields.get(key) {
    Some(Json::Null) | None =>
      raise DecodeError::Msg("\{ctx}: missing required field \"\{key}\"")
    Some(value) =>
      match parse(value) {
        Ok(parsed) => parsed
        Err(message) =>
          raise DecodeError::Msg("\{ctx}: field \"\{key}\": \{message}")
      }
  }
}

///|
/// Decode a string-literal-union wire value through an enum's `of_wire`,
/// propagating its message as a decode error.
fn[T] wire_of(
  ctx : String,
  key : String,
  parsed : Result[T, String],
) -> T raise DecodeError {
  match parsed {
    Ok(value) => value
    Err(message) =>
      raise DecodeError::Msg("\{ctx}: field \"\{key}\": \{message}")
  }
}