///|
/// Cache keys that a caller should invalidate after an unsafe request.
pub(all) struct InvalidationDecision {
  invalidate : Bool
  target_uris : Array[String]
  trace : Array[TraceStep]
  diagnostics : Array[Diagnostic]
} derive(Debug, Eq)

///|
/// Evaluate RFC 9111 section 4.4 invalidation after a state-changing request.
/// The caller remains responsible for locating and deleting stored variants.
pub fn evaluate_invalidation(
  request : RequestMetadata,
  response : ResponseMetadata,
) -> InvalidationDecision {
  let trace : Array[TraceStep] = []
  let diagnostics = request.headers.diagnostics()
  append_diagnostics(diagnostics, response.headers.diagnostics())
  if is_safe_method(request.http_method) {
    trace.push(
      step(
        "INVALIDATION_SAFE_METHOD", "safe request methods do not trigger cache invalidation",
        "RFC 9111 4.4",
      ),
    )
    return { invalidate: false, target_uris: [], trace, diagnostics, }
  }
  if response.status < 200 || response.status >= 400 {
    trace.push(
      step(
        "INVALIDATION_UNSUCCESSFUL_RESPONSE", "non-successful response does not trigger automatic invalidation",
        "RFC 9111 4.4",
      ),
    )
    return { invalidate: false, target_uris: [], trace, diagnostics, }
  }
  let targets : Array[String] = [request.target_uri]
  for field_name in ["location", "content-location"] {
    for value in response.headers.values(field_name) {
      match resolve_same_origin_reference(request.target_uri, value) {
        Some(uri) => if !array_contains(targets, uri) { targets.push(uri) }
        None =>
          diagnostics.push({
            level: Info,
            code: "INVALIDATION_DIFFERENT_ORIGIN_SKIPPED",
            message: field_name +
            " was not invalidated because it is invalid or cross-origin",
            field_name: Some(field_name),
          })
      }
    }
  }
  trace.push(
    step(
      "INVALIDATION_REQUIRED", "unsafe successful request invalidates the effective request URI and same-origin targets",
      "RFC 9111 4.4",
    ),
  )
  { invalidate: true, target_uris: targets, trace, diagnostics, }
}

///|
fn is_safe_method(http_method : String) -> Bool {
  match http_method.to_upper() {
    "GET" | "HEAD" | "OPTIONS" | "TRACE" => true
    _ => false
  }
}

///|
fn resolve_same_origin_reference(base : String, reference : String) -> String? {
  let target = reference.trim().to_owned()
  if target.length() == 0 ||
    target.contains_char('\r') ||
    target.contains_char('\n') ||
    target.contains_char('\u{0000}') {
    return None
  }
  guard split_origin(base) is Some((origin, path_start)) else { return None }
  if target.has_prefix("/") {
    return Some(origin + target)
  }
  if target.has_prefix("http://") || target.has_prefix("https://") {
    guard split_origin(target) is Some((target_origin, _)) else { return None }
    if @lex.equal_ignore_case(origin, target_origin) {
      return Some(target)
    }
    return None
  }
  let base_path = base[path_start:].to_owned()
  let slash = last_slash(base_path)
  let directory = if slash is Some(index) {
    base_path[0:index + 1].to_owned()
  } else {
    "/"
  }
  if target.has_prefix("../") || target.has_prefix("./") {
    return None
  }
  Some(origin + directory + target)
}

///|
fn split_origin(value : String) -> (String, Int)? {
  let start = if value.has_prefix("http://") {
    7
  } else if value.has_prefix("https://") {
    8
  } else {
    return None
  }
  for index = start; index < value.length(); index = index + 1 {
    if value[index] == '/' || value[index] == '?' || value[index] == '#' {
      return Some((value[0:index].to_owned(), index))
    }
  }
  Some((value, value.length()))
}

///|
fn last_slash(value : String) -> Int? {
  let mut found : Int? = None
  for index = 0; index < value.length(); index = index + 1 {
    if value[index] == '/' {
      found = Some(index)
    }
  }
  found
}