///|
fn set_optional_string(
  object : Map[String, Json],
  name : String,
  value : String?,
) -> Unit {
  match value {
    Some(text) => object.set(name, Json::string(text))
    None => ()
  }
}

///|
fn set_optional_number(
  object : Map[String, Json],
  name : String,
  value : Double?,
) -> Unit {
  match value {
    Some(number) => object.set(name, Json::number(number))
    None => ()
  }
}

///|
fn set_optional_bool(
  object : Map[String, Json],
  name : String,
  value : Bool?,
) -> Unit {
  match value {
    Some(flag) => object.set(name, Json::boolean(flag))
    None => ()
  }
}

///|
fn name_value_json(value : HarNameValue) -> Json {
  let object : Map[String, Json] = {
    "name": Json::string(value.name_value),
    "value": Json::string(value.value_value),
  }
  set_optional_string(object, "comment", value.comment_value)
  Json::object(object)
}

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

///|
fn cookie_json(cookie : HarCookie) -> Json {
  let object : Map[String, Json] = {
    "name": Json::string(cookie.name_value),
    "value": Json::string(cookie.value_value),
  }
  set_optional_string(object, "path", cookie.path_value)
  set_optional_string(object, "domain", cookie.domain_value)
  set_optional_string(object, "expires", cookie.expires_value)
  set_optional_bool(object, "httpOnly", cookie.http_only_value)
  set_optional_bool(object, "secure", cookie.secure_value)
  set_optional_string(object, "comment", cookie.comment_value)
  Json::object(object)
}

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

///|
fn post_param_json(parameter : HarPostParam) -> Json {
  let object : Map[String, Json] = {
    "name": Json::string(parameter.name_value),
  }
  set_optional_string(object, "value", parameter.value_value)
  set_optional_string(object, "fileName", parameter.file_name_value)
  set_optional_string(object, "contentType", parameter.content_type_value)
  set_optional_string(object, "comment", parameter.comment_value)
  Json::object(object)
}

///|
fn post_data_json(post_data : HarPostData) -> Json {
  let parameters : Array[Json] = []
  for parameter in post_data.params_value {
    parameters.push(post_param_json(parameter))
  }
  let object : Map[String, Json] = {
    "mimeType": Json::string(post_data.mime_type_value),
  }
  if !parameters.is_empty() {
    object.set("params", Json::array(parameters))
  }
  set_optional_string(object, "text", post_data.text_value)
  set_optional_string(object, "comment", post_data.comment_value)
  Json::object(object)
}

///|
fn content_json(content : HarContent) -> Json {
  let object : Map[String, Json] = {
    "size": Json::number(content.size_value),
    "mimeType": Json::string(content.mime_type_value),
  }
  set_optional_number(object, "compression", content.compression_value)
  set_optional_string(object, "text", content.text_value)
  set_optional_string(object, "encoding", content.encoding_value)
  set_optional_string(object, "comment", content.comment_value)
  Json::object(object)
}

///|
fn cache_entry_json(entry : HarCacheEntry) -> Json {
  let object : Map[String, Json] = {
    "lastAccess": Json::string(entry.last_access_value),
    "eTag": Json::string(entry.etag_value),
    "hitCount": Json::number(entry.hit_count_value),
  }
  set_optional_string(object, "expires", entry.expires_value)
  set_optional_string(object, "comment", entry.comment_value)
  Json::object(object)
}

///|
fn cache_json(cache : HarCache) -> Json {
  let object : Map[String, Json] = Map([])
  match cache.before_request_value {
    Some(value) => object.set("beforeRequest", cache_entry_json(value))
    None => ()
  }
  match cache.after_request_value {
    Some(value) => object.set("afterRequest", cache_entry_json(value))
    None => ()
  }
  set_optional_string(object, "comment", cache.comment_value)
  Json::object(object)
}

///|
fn timings_json(timings : HarTimings) -> Json {
  let object : Map[String, Json] = {
    "send": Json::number(timings.send_value),
    "wait": Json::number(timings.wait_value),
    "receive": Json::number(timings.receive_value),
  }
  set_optional_number(object, "blocked", timings.blocked_value)
  set_optional_number(object, "dns", timings.dns_value)
  set_optional_number(object, "connect", timings.connect_value)
  set_optional_number(object, "ssl", timings.ssl_value)
  set_optional_string(object, "comment", timings.comment_value)
  Json::object(object)
}

///|
fn request_json(request : HarRequest) -> Json {
  let object : Map[String, Json] = {
    "method": Json::string(request.method_value),
    "url": Json::string(request.url_value),
    "httpVersion": Json::string(request.http_version_value),
    "cookies": cookie_array_json(request.cookies_value),
    "headers": name_value_array_json(request.headers_value),
    "queryString": name_value_array_json(request.query_value),
    "headersSize": Json::number(request.headers_size_value),
    "bodySize": Json::number(request.body_size_value),
  }
  match request.post_data_value {
    Some(value) => object.set("postData", post_data_json(value))
    None => ()
  }
  set_optional_string(object, "comment", request.comment_value)
  Json::object(object)
}

///|
fn response_json(response : HarResponse) -> Json {
  let object : Map[String, Json] = {
    "status": Json::number(response.status_value),
    "statusText": Json::string(response.status_text_value),
    "httpVersion": Json::string(response.http_version_value),
    "cookies": cookie_array_json(response.cookies_value),
    "headers": name_value_array_json(response.headers_value),
    "content": content_json(response.content_value),
    "redirectURL": Json::string(response.redirect_url_value),
    "headersSize": Json::number(response.headers_size_value),
    "bodySize": Json::number(response.body_size_value),
  }
  set_optional_string(object, "comment", response.comment_value)
  Json::object(object)
}

///|
fn software_json(software : HarSoftware) -> Json {
  let object : Map[String, Json] = {
    "name": Json::string(software.name_value),
    "version": Json::string(software.version_value),
  }
  set_optional_string(object, "comment", software.comment_value)
  Json::object(object)
}

///|
fn page_timings_json(timings : HarPageTimings) -> Json {
  let object : Map[String, Json] = Map([])
  set_optional_number(object, "onContentLoad", timings.on_content_load_value)
  set_optional_number(object, "onLoad", timings.on_load_value)
  set_optional_string(object, "comment", timings.comment_value)
  Json::object(object)
}

///|
fn page_json(page : HarPage) -> Json {
  let object : Map[String, Json] = {
    "startedDateTime": Json::string(page.started_datetime_value),
    "id": Json::string(page.id_value),
    "title": Json::string(page.title_value),
    "pageTimings": page_timings_json(page.page_timings_value),
  }
  set_optional_string(object, "comment", page.comment_value)
  Json::object(object)
}

///|
fn entry_json(entry : HarEntry) -> Json {
  let object : Map[String, Json] = {
    "startedDateTime": Json::string(entry.started_datetime_value),
    "time": Json::number(entry.time_value),
    "request": request_json(entry.request_value),
    "response": response_json(entry.response_value),
    "cache": cache_json(entry.cache_value),
    "timings": timings_json(entry.timings_value),
  }
  set_optional_string(object, "pageref", entry.page_ref_value)
  set_optional_string(object, "serverIPAddress", entry.server_ip_value)
  set_optional_string(object, "connection", entry.connection_value)
  set_optional_string(object, "comment", entry.comment_value)
  Json::object(object)
}

///|
fn document_json(document : HarDocument) -> Json {
  let pages : Array[Json] = []
  for page in document.pages_value {
    pages.push(page_json(page))
  }
  let entries : Array[Json] = []
  for entry in document.entries_value {
    entries.push(entry_json(entry))
  }
  let log : Map[String, Json] = {
    "version": Json::string(document.version_value),
    "creator": software_json(document.creator_value),
    "pages": Json::array(pages),
    "entries": Json::array(entries),
  }
  match document.browser_value {
    Some(value) => log.set("browser", software_json(value))
    None => ()
  }
  set_optional_string(log, "comment", document.comment_value)
  Json::object({ "log": Json::object(log) })
}

///|
pub fn serialize_har(
  document : HarDocument,
  indent? : Int = 0,
) -> Result[String, HarError] {
  if indent < 0 || indent > 16 {
    return Err(
      HarError::new(
        InvalidField,
        "$.serialization.indent",
        "indent must be between 0 and 16",
      ),
    )
  }
  Ok(document_json(document).stringify(indent~))
}