///|
fn audit_new() -> HeaderAudit {
  { ok: true, warnings: [], errors: [] }
}

///|
fn audit_warn(audit : HeaderAudit, message : String) -> HeaderAudit {
  let warnings : Array[String] = []
  for item in audit.warnings {
    warnings.push(item)
  }
  warnings.push(message)
  { ok: audit.ok, warnings, errors: audit.errors }
}

///|
fn audit_error(audit : HeaderAudit, message : String) -> HeaderAudit {
  let errors : Array[String] = []
  for item in audit.errors {
    errors.push(item)
  }
  errors.push(message)
  { ok: false, warnings: audit.warnings, errors }
}

///|
fn append_audit(left : HeaderAudit, right : HeaderAudit) -> HeaderAudit {
  let warnings : Array[String] = []
  let errors : Array[String] = []
  for item in left.warnings {
    warnings.push(item)
  }
  for item in right.warnings {
    warnings.push(item)
  }
  for item in left.errors {
    errors.push(item)
  }
  for item in right.errors {
    errors.push(item)
  }
  { ok: left.ok && right.ok, warnings, errors }
}

///|
fn parse_error_label(err : ParseError) -> String {
  match err {
    EmptyItem(index) => "empty item at " + index.to_string()
    MissingSlash(index, value) =>
      "missing slash at " + index.to_string() + ": " + value
    InvalidToken(index, kind, value) =>
      "invalid " + kind + " at " + index.to_string() + ": " + value
    InvalidParameter(index, value) =>
      "invalid parameter at " + index.to_string() + ": " + value
    InvalidQ(index, value) => "invalid q at " + index.to_string() + ": " + value
    InvalidWildcard(index, value) =>
      "invalid wildcard at " + index.to_string() + ": " + value
    InvalidOffer(index, value) =>
      "invalid offer at " + index.to_string() + ": " + value
    InvalidLanguageTag(index, value) =>
      "invalid language tag at " + index.to_string() + ": " + value
  }
}

///|
pub fn audit_accept(header : StringView) -> HeaderAudit {
  match parse_accept(header) {
    Ok(ranges) => {
      let mut audit = audit_new()
      if ranges.length() == 0 {
        audit = audit_warn(
          audit, "Accept is empty; server order will decide media",
        )
      }
      for range in ranges {
        if range.q == 0 {
          audit = audit_warn(
            audit,
            "media range " + range.raw + " is explicitly refused",
          )
        }
        if range.top == "*" {
          audit = audit_warn(
            audit, "media wildcard */* may hide client preference",
          )
        } else if range.sub == "*" {
          audit = audit_warn(
            audit,
            "subtype wildcard " + range.raw + " is broad",
          )
        }
        if range.params.length() > 0 && range.top == "*" {
          audit = audit_warn(
            audit, "parameters on wildcard media range are unlikely to match",
          )
        }
      }
      audit
    }
    Err(err) =>
      audit_error(audit_new(), "Accept parse error: " + parse_error_label(err))
  }
}

///|
pub fn audit_accept_language(header : StringView) -> HeaderAudit {
  match parse_accept_language(header) {
    Ok(ranges) => {
      let mut audit = audit_new()
      if ranges.length() == 0 {
        audit = audit_warn(
          audit, "Accept-Language is empty; server order will decide language",
        )
      }
      for range in ranges {
        if range.q == 0 {
          audit = audit_warn(
            audit,
            "language range " + range.raw + " is explicitly refused",
          )
        }
        if range.primary == "*" {
          audit = audit_warn(audit, "language wildcard allows every locale")
        }
        if range.subtags.length() > 3 {
          audit = audit_warn(
            audit,
            "language range " + range.raw + " is very specific",
          )
        }
      }
      audit
    }
    Err(err) =>
      audit_error(
        audit_new(),
        "Accept-Language parse error: " + parse_error_label(err),
      )
  }
}

///|
pub fn audit_accept_encoding(header : StringView) -> HeaderAudit {
  match parse_accept_encoding(header) {
    Ok(ranges) => {
      let mut audit = audit_new()
      if ranges.length() == 0 {
        audit = audit_warn(
          audit, "Accept-Encoding is empty; any coding is acceptable",
        )
      }
      let mut identity_refused = false
      for range in ranges {
        if range.coding == "identity" && range.q == 0 {
          identity_refused = true
        }
        if range.coding == "*" {
          audit = audit_warn(
            audit, "encoding wildcard may select an unexpected compression",
          )
        }
      }
      if identity_refused {
        audit = audit_warn(
          audit, "identity coding is refused; uncompressed fallback is disabled",
        )
      }
      audit
    }
    Err(err) =>
      audit_error(
        audit_new(),
        "Accept-Encoding parse error: " + parse_error_label(err),
      )
  }
}

///|
pub fn audit_accept_charset(header : StringView) -> HeaderAudit {
  match parse_accept_charset(header) {
    Ok(ranges) => {
      let mut audit = audit_new()
      if ranges.length() == 0 {
        audit = audit_warn(
          audit, "Accept-Charset is empty; any charset is acceptable",
        )
      }
      for range in ranges {
        if range.charset == "*" {
          audit = audit_warn(audit, "charset wildcard allows every charset")
        }
        if range.q == 0 {
          audit = audit_warn(
            audit,
            "charset " + range.charset + " is explicitly refused",
          )
        }
      }
      audit
    }
    Err(err) =>
      audit_error(
        audit_new(),
        "Accept-Charset parse error: " + parse_error_label(err),
      )
  }
}

///|
pub fn audit_vary_header(header : StringView) -> HeaderAudit {
  match parse_vary(header) {
    Ok(fields) => {
      let mut audit = audit_new()
      if fields.length() == 0 {
        audit = audit_warn(
          audit, "Vary is empty; cache key will not include request headers",
        )
      }
      if fields.length() == 1 && fields[0] == "*" {
        audit = audit_warn(audit, "Vary: * disables shared cache reuse")
      }
      for field in fields {
        if field == "User-Agent" {
          audit = audit_warn(
            audit, "Vary: User-Agent can create very large cache keys",
          )
        }
        if field == "Cookie" {
          audit = audit_warn(
            audit, "Vary: Cookie often prevents effective shared caching",
          )
        }
      }
      audit
    }
    Err(err) =>
      audit_error(audit_new(), "Vary parse error: " + parse_error_label(err))
  }
}

///|
pub fn audit_cache_control_header(header : StringView) -> HeaderAudit {
  match parse_cache_control(header) {
    Ok(directives) => {
      let summary = match summarize_cache_control(directives) {
        Ok(summary) => summary
        Err(err) =>
          return audit_error(
            audit_new(),
            "Cache-Control summary error: " + parse_error_label(err),
          )
      }
      let mut audit = audit_new()
      if summary.no_store && summary.max_age is Some(_) {
        audit = audit_warn(audit, "no-store with max-age is contradictory")
      }
      if summary.is_public && summary.is_private {
        audit = audit_warn(audit, "public and private are both present")
      }
      if summary.no_cache && summary.immutable {
        audit = audit_warn(audit, "no-cache and immutable are both present")
      }
      if summary.unknown.length() > 0 {
        audit = audit_warn(
          audit,
          "unknown cache directives: " + summary.unknown.length().to_string(),
        )
      }
      audit
    }
    Err(err) =>
      audit_error(
        audit_new(),
        "Cache-Control parse error: " + parse_error_label(err),
      )
  }
}

///|
pub fn audit_preferences(preferences : RequestPreferences) -> HeaderAudit {
  append_audit(
    append_audit(
      audit_accept(preferences.accept[:]),
      audit_accept_language(preferences.accept_language[:]),
    ),
    append_audit(
      audit_accept_encoding(preferences.accept_encoding[:]),
      audit_accept_charset(preferences.accept_charset[:]),
    ),
  )
}

///|
pub fn format_audit(audit : HeaderAudit) -> String {
  let buf = StringBuilder()
  buf.write_string("ok=")
  buf.write_string(audit.ok.to_string())
  buf.write_string(", warnings=")
  buf.write_string(audit.warnings.length().to_string())
  buf.write_string(", errors=")
  buf.write_string(audit.errors.length().to_string())
  if audit.warnings.length() > 0 {
    buf.write_string(", first-warning=")
    buf.write_string(audit.warnings[0])
  }
  if audit.errors.length() > 0 {
    buf.write_string(", first-error=")
    buf.write_string(audit.errors[0])
  }
  buf.to_string()
}