///|
/// Result of comparing the primary cache key and Vary-selected request fields.
pub(all) struct CacheKeyResult {
  matches : Bool
  vary_fields : Array[String]
  trace : Array[TraceStep]
  diagnostics : Array[Diagnostic]
} derive(Debug, Eq)

///|
/// Compare a presented request with the request that selected a stored response.
pub fn evaluate_cache_key(
  stored : StoredResponse,
  presented : RequestMetadata,
) -> CacheKeyResult {
  let trace : Array[TraceStep] = []
  let diagnostics = presented.headers.diagnostics()
  if stored.request.target_uri != presented.target_uri {
    trace.push(
      step(
        "CACHE_KEY_URI_MISMATCH", "presented target URI differs from the stored request URI",
        "RFC 9111 2, 4",
      ),
    )
    return { matches: false, vary_fields: [], trace, diagnostics, }
  }
  if stored.request.http_method != "GET" ||
    (presented.http_method != "GET" && presented.http_method != "HEAD") {
    trace.push(
      step(
        "CACHE_KEY_METHOD_MISMATCH", "stored and presented request methods are not reusable by this engine",
        "RFC 9111 4",
      ),
    )
    return { matches: false, vary_fields: [], trace, diagnostics, }
  }
  let parsed = parse_vary(stored.response.headers)
  append_diagnostics(diagnostics, parsed.1)
  let vary_fields = parsed.0
  if vary_fields.length() == 1 && vary_fields[0] == "*" {
    trace.push(
      step(
        "CACHE_KEY_VARY_STAR", "Vary star never matches without validation", "RFC 9111 4.1",
      ),
    )
    return { matches: false, vary_fields, trace, diagnostics, }
  }
  for field in vary_fields {
    let original = normalize_field_values(stored.request.headers.values(field))
    let current = normalize_field_values(presented.headers.values(field))
    if original != current {
      trace.push(
        step(
          "CACHE_KEY_VARY_MISMATCH",
          "Vary-selected request field differs: " + field,
          "RFC 9111 4.1",
        ),
      )
      return { matches: false, vary_fields, trace, diagnostics, }
    }
  }
  trace.push(
    step(
      "CACHE_KEY_MATCH", "target URI, method, and Vary-selected fields match", "RFC 9111 4.1",
    ),
  )
  { matches: true, vary_fields, trace, diagnostics, }
}

///|
fn parse_vary(headers : Headers) -> (Array[String], Array[Diagnostic]) {
  let fields : Array[String] = []
  let diagnostics : Array[Diagnostic] = []
  for line in headers.values("vary") {
    let split = @lex.split_quoted_list(line)
    if !split.valid {
      diagnostics.push({
        level: Error,
        code: "VARY_SYNTAX_INVALID",
        message: "Vary contains invalid quoted syntax",
        field_name: Some("vary"),
      })
    }
    for item in split.parts {
      let name = @lex.lower(@lex.trim_ows(item))
      if name == "*" {
        return (["*"], diagnostics)
      }
      if !@lex.is_token(name) {
        diagnostics.push({
          level: Error,
          code: "VARY_FIELD_NAME_INVALID",
          message: "Vary member is not a valid field name",
          field_name: Some("vary"),
        })
      } else if !array_contains(fields, name) {
        fields.push(name)
      }
    }
  }
  (fields, diagnostics)
}

///|
fn normalize_field_values(values : Array[String]) -> String? {
  if values.length() == 0 {
    return None
  }
  let output = StringBuilder()
  for index, value in values {
    if index > 0 {
      output.write_char(',')
    }
    output.write_string(collapse_ows(value))
  }
  Some(output.to_string())
}

///|
fn collapse_ows(value : String) -> String {
  let output = StringBuilder()
  let mut pending_space = false
  let mut wrote = false
  let trimmed = value.trim(chars=" \t")
  for char in trimmed {
    if char == ' ' || char == '\t' {
      pending_space = true
    } else {
      if pending_space && wrote {
        output.write_char(' ')
      }
      output.write_char(char)
      pending_space = false
      wrote = true
    }
  }
  output.to_string()
}

///|
fn array_contains(values : Array[String], candidate : String) -> Bool {
  for value in values {
    if value == candidate {
      return true
    }
  }
  false
}