///|
/// JSON encoding for the WAL image.
///
/// The image is small and is read by a human when something has gone wrong in
/// a bucket, so it is stored as JSON rather than a packed binary format.
/// Payloads — the things that are actually large — never pass through here;
/// entries carry only their keys.

///|
fn json_string(
  obj : Map[String, Json],
  key : String,
  fallback : String,
) -> String {
  match obj.get(key) {
    Some(Json::String(value)) => value
    _ => fallback
  }
}

///|
fn json_int64(obj : Map[String, Json], key : String, fallback : Int64) -> Int64 {
  match obj.get(key) {
    Some(Json::Number(value, ..)) => value.to_int64()
    _ => fallback
  }
}

///|
fn json_int(obj : Map[String, Json], key : String, fallback : Int) -> Int {
  match obj.get(key) {
    Some(Json::Number(value, ..)) => value.to_int()
    _ => fallback
  }
}

///|
fn json_string_array(obj : Map[String, Json], key : String) -> Array[String] {
  let out : Array[String] = []
  match obj.get(key) {
    Some(Json::Array(items)) =>
      for item in items {
        match item {
          Json::String(value) => out.push(value)
          _ => ()
        }
      }
    _ => ()
  }
  out
}

///|
fn string_array_json(values : Array[String]) -> Json {
  let items : Array[Json] = []
  for value in values {
    items.push(Json::string(value))
  }
  Json::array(items)
}

///|
fn entry_json(entry : WalEntry) -> Json {
  let obj : Map[String, Json] = Map([])
  obj["seq"] = Json::number(entry.seq.to_double())
  obj["kind"] = Json::string(entry.kind)
  obj["payload_keys"] = string_array_json(entry.payload_keys)
  obj["data"] = Json::string(entry.data)
  obj["idempotency_key"] = Json::string(entry.idempotency_key)
  Json::object(obj)
}

///|
fn entry_of_json(value : Json) -> WalEntry? {
  guard value is Json::Object(obj) else { return None }
  Some({
    seq: json_int64(obj, "seq", 0L),
    kind: json_string(obj, "kind", ""),
    payload_keys: json_string_array(obj, "payload_keys"),
    data: json_string(obj, "data", ""),
    idempotency_key: json_string(obj, "idempotency_key", ""),
  })
}

///|
pub fn encode_image(image : WalImage) -> Bytes {
  let entries : Array[Json] = []
  for entry in image.entries {
    entries.push(entry_json(entry))
  }
  let obj : Map[String, Json] = Map([])
  obj["version"] = Json::number(image.version.to_double())
  obj["snapshot"] = Json::string(image.snapshot)
  obj["snapshot_seq"] = Json::number(image.snapshot_seq.to_double())
  obj["snapshot_payloads"] = string_array_json(image.snapshot_payloads)
  obj["entries"] = Json::array(entries)
  obj["settings"] = Json::string(image.settings)
  @utf8.encode(Json::object(obj).stringify())
}

///|
pub fn decode_image(body : Bytes) -> WalImage raise @bit.GitError {
  let text = @utf8.decode_lossy(body[:])
  let parsed = @json.parse(text) catch {
    e => raise @bit.GitError::IoError("WAL image is not valid JSON: \{e}")
  }
  guard parsed is Json::Object(obj) else {
    raise @bit.GitError::IoError("WAL image is not a JSON object")
  }
  let version = json_int(obj, "version", 0)
  // A newer writer may have added fields this reader would drop on its next
  // rewrite. Refuse rather than silently truncate someone else's state.
  if version > WAL_FORMAT_VERSION {
    raise @bit.GitError::IoError(
      "WAL image format version \{version} is newer than supported " +
      "(\{WAL_FORMAT_VERSION}); upgrade before writing to this log",
    )
  }
  let entries : Array[WalEntry] = []
  match obj.get("entries") {
    Some(Json::Array(items)) =>
      for item in items {
        match entry_of_json(item) {
          Some(entry) => entries.push(entry)
          None =>
            raise @bit.GitError::IoError("WAL image has a malformed entry")
        }
      }
    _ => ()
  }
  {
    version,
    snapshot: json_string(obj, "snapshot", ""),
    snapshot_seq: json_int64(obj, "snapshot_seq", 0L),
    snapshot_payloads: json_string_array(obj, "snapshot_payloads"),
    entries,
    settings: json_string(obj, "settings", ""),
  }
}