///|
pub struct HarLimits {
  max_input_bytes_value : Int
  max_pages_value : Int
  max_entries_value : Int
  max_headers_per_message_value : Int
  max_cookies_per_message_value : Int
  max_query_parameters_value : Int
} derive(Eq, Debug)

///|
pub fn HarLimits::standard() -> HarLimits {
  {
    max_input_bytes_value: 64 * 1024 * 1024,
    max_pages_value: 10000,
    max_entries_value: 100000,
    max_headers_per_message_value: 10000,
    max_cookies_per_message_value: 10000,
    max_query_parameters_value: 10000,
  }
}

///|
pub fn HarLimits::new(
  max_input_bytes : Int,
  max_pages : Int,
  max_entries : Int,
  max_headers_per_message : Int,
  max_cookies_per_message : Int,
  max_query_parameters : Int,
) -> Result[HarLimits, HarError] {
  for
    item in [
      ("max_input_bytes", max_input_bytes),
      ("max_pages", max_pages),
      ("max_entries", max_entries),
      ("max_headers_per_message", max_headers_per_message),
      ("max_cookies_per_message", max_cookies_per_message),
      ("max_query_parameters", max_query_parameters),
    ] {
    let (name, value) = item
    if value < 0 {
      return Err(
        HarError::new(
          InvalidField,
          "$.limits." + name,
          "resource limits cannot be negative",
        ),
      )
    }
  }
  Ok({
    max_input_bytes_value: max_input_bytes,
    max_pages_value: max_pages,
    max_entries_value: max_entries,
    max_headers_per_message_value: max_headers_per_message,
    max_cookies_per_message_value: max_cookies_per_message,
    max_query_parameters_value: max_query_parameters,
  })
}

///|
pub fn HarLimits::max_input_bytes(self : HarLimits) -> Int {
  self.max_input_bytes_value
}

///|
pub fn HarLimits::max_pages(self : HarLimits) -> Int {
  self.max_pages_value
}

///|
pub fn HarLimits::max_entries(self : HarLimits) -> Int {
  self.max_entries_value
}

///|
pub fn HarLimits::max_headers_per_message(self : HarLimits) -> Int {
  self.max_headers_per_message_value
}

///|
pub fn HarLimits::max_cookies_per_message(self : HarLimits) -> Int {
  self.max_cookies_per_message_value
}

///|
pub fn HarLimits::max_query_parameters(self : HarLimits) -> Int {
  self.max_query_parameters_value
}

///|
fn limit_error(path : String, actual : Int, maximum : Int) -> HarError {
  HarError::new(
    LimitExceeded,
    path,
    "observed " +
    actual.to_string() +
    " items; maximum is " +
    maximum.to_string(),
  )
}

///|
fn enforce_document_limits(
  document : HarDocument,
  limits : HarLimits,
) -> Result[Unit, HarError] {
  if document.page_count() > limits.max_pages_value {
    return Err(
      limit_error("$.log.pages", document.page_count(), limits.max_pages_value),
    )
  }
  if document.entry_count() > limits.max_entries_value {
    return Err(
      limit_error(
        "$.log.entries",
        document.entry_count(),
        limits.max_entries_value,
      ),
    )
  }
  for index, entry in document.entries_value {
    let base = append_json_index("$.log.entries", index)
    let request = entry.request_value
    let response = entry.response_value
    if request.headers_value.length() > limits.max_headers_per_message_value {
      return Err(
        limit_error(
          append_json_field(append_json_field(base, "request"), "headers"),
          request.headers_value.length(),
          limits.max_headers_per_message_value,
        ),
      )
    }
    if response.headers_value.length() > limits.max_headers_per_message_value {
      return Err(
        limit_error(
          append_json_field(append_json_field(base, "response"), "headers"),
          response.headers_value.length(),
          limits.max_headers_per_message_value,
        ),
      )
    }
    if request.cookies_value.length() > limits.max_cookies_per_message_value {
      return Err(
        limit_error(
          append_json_field(append_json_field(base, "request"), "cookies"),
          request.cookies_value.length(),
          limits.max_cookies_per_message_value,
        ),
      )
    }
    if response.cookies_value.length() > limits.max_cookies_per_message_value {
      return Err(
        limit_error(
          append_json_field(append_json_field(base, "response"), "cookies"),
          response.cookies_value.length(),
          limits.max_cookies_per_message_value,
        ),
      )
    }
    if request.query_value.length() > limits.max_query_parameters_value {
      return Err(
        limit_error(
          append_json_field(append_json_field(base, "request"), "queryString"),
          request.query_value.length(),
          limits.max_query_parameters_value,
        ),
      )
    }
  }
  Ok(())
}

///|
pub fn parse_har_with_limits(
  input : String,
  limits : HarLimits,
) -> Result[HarDocument, HarError] {
  if input.length() > limits.max_input_bytes_value {
    return Err(limit_error("$", input.length(), limits.max_input_bytes_value))
  }
  let document = match parse_har(input) {
    Ok(value) => value
    Err(error) => return Err(error)
  }
  match enforce_document_limits(document, limits) {
    Ok(_) => Ok(document)
    Err(error) => Err(error)
  }
}

///|
pub struct HarValidationPolicy {
  timing_tolerance_value : Double
  require_absolute_urls_value : Bool
  require_page_references_value : Bool
} derive(Eq, Debug)

///|
pub fn HarValidationPolicy::standard() -> HarValidationPolicy {
  {
    timing_tolerance_value: 1.0,
    require_absolute_urls_value: true,
    require_page_references_value: true,
  }
}

///|
pub fn HarValidationPolicy::new(
  timing_tolerance : Double,
  require_absolute_urls : Bool,
  require_page_references : Bool,
) -> Result[HarValidationPolicy, HarError] {
  if timing_tolerance < 0.0 {
    return Err(
      HarError::new(
        InvalidField,
        "$.policy.timing_tolerance",
        "timing tolerance cannot be negative",
      ),
    )
  }
  Ok({
    timing_tolerance_value: timing_tolerance,
    require_absolute_urls_value: require_absolute_urls,
    require_page_references_value: require_page_references,
  })
}

///|
pub fn HarValidationPolicy::timing_tolerance(
  self : HarValidationPolicy,
) -> Double {
  self.timing_tolerance_value
}

///|
pub fn HarValidationPolicy::require_absolute_urls(
  self : HarValidationPolicy,
) -> Bool {
  self.require_absolute_urls_value
}

///|
pub fn HarValidationPolicy::require_page_references(
  self : HarValidationPolicy,
) -> Bool {
  self.require_page_references_value
}

///|
fn page_id_occurrences(document : HarDocument, id : String) -> Int {
  let mut count = 0
  for page in document.pages_value {
    if page.id_value == id {
      count += 1
    }
  }
  count
}

///|
fn page_exists(document : HarDocument, id : String) -> Bool {
  for page in document.pages_value {
    if page.id_value == id {
      return true
    }
  }
  false
}

///|
fn absolute_http_url(url : String) -> Bool {
  let lower = url.to_lower()
  lower.has_prefix("http://") || lower.has_prefix("https://")
}

///|
pub fn validate_har(
  document : HarDocument,
  policy? : HarValidationPolicy = HarValidationPolicy::standard(),
) -> ValidationResult {
  let issues : Array[ValidationIssue] = []
  for page_index, page in document.pages_value {
    if page_id_occurrences(document, page.id_value) > 1 {
      issues.push(
        ValidationIssue::error(
          DuplicateIdentifier.code(),
          append_json_field(append_json_index("$.log.pages", page_index), "id"),
          "page id is not unique",
        ),
      )
    }
  }
  for entry_index, entry in document.entries_value {
    let base = append_json_index("$.log.entries", entry_index)
    match entry.page_ref_value {
      Some(page_ref) =>
        if !page_exists(document, page_ref) {
          let issue = if policy.require_page_references_value {
            ValidationIssue::error(
              MissingPageReference.code(),
              append_json_field(base, "pageref"),
              "entry references a page id that does not exist",
            )
          } else {
            ValidationIssue::warning(
              MissingPageReference.code(),
              append_json_field(base, "pageref"),
              "entry references a page id that does not exist",
            )
          }
          issues.push(issue)
        }
      None => ()
    }
    if policy.require_absolute_urls_value &&
      !absolute_http_url(entry.request_value.url_value) {
      issues.push(
        ValidationIssue::error(
          InvalidUrl.code(),
          append_json_field(append_json_field(base, "request"), "url"),
          "request URL must be absolute HTTP or HTTPS",
        ),
      )
    }
    let delta = entry.timing_delta()
    let absolute_delta = if delta < 0.0 { -delta } else { delta }
    if absolute_delta > policy.timing_tolerance_value {
      issues.push(
        ValidationIssue::warning(
          "entry.timing_mismatch",
          append_json_field(base, "time"),
          "entry time differs from the sum of known timing phases",
        ),
      )
    }
    match
      (
        entry.response_value.known_body_size(),
        entry.response_value.content_value.known_size(),
      ) {
      (Some(transferred), Some(decoded)) =>
        if transferred > decoded {
          issues.push(
            ValidationIssue::warning(
              "response.body_larger_than_content",
              append_json_field(append_json_field(base, "response"), "bodySize"),
              "transferred body size is larger than decoded content size",
            ),
          )
        }
      _ => ()
    }
    if entry.response_value.status_value >= 400.0 &&
      entry.response_value.status_text_value.trim().is_empty() {
      issues.push(
        ValidationIssue::warning(
          "response.empty_status_text",
          append_json_field(append_json_field(base, "response"), "statusText"),
          "error response has an empty status text",
        ),
      )
    }
  }
  ValidationResult::new(issues)
}