///|
pub(all) enum ValidatorKind {
  StrongEtag
  WeakEtag
  LastModified
  StrongEtagAndLastModified
  WeakEtagAndLastModified
  CallerConditions
} derive(Eq, Compare, Debug)

///|
pub fn ValidatorKind::label(self : ValidatorKind) -> String {
  match self {
    StrongEtag => "strong-etag"
    WeakEtag => "weak-etag"
    LastModified => "last-modified"
    StrongEtagAndLastModified => "strong-etag + last-modified"
    WeakEtagAndLastModified => "weak-etag + last-modified"
    CallerConditions => "caller-conditions"
  }
}

///|
fn etag_is_weak(value : String) -> Bool {
  value.has_prefix("W/")
}

///|
pub fn is_valid_etag(value : String) -> Bool {
  let tag = value.trim(chars=" \t").to_owned()
  let tag_text = if tag.has_prefix("W/") {
    tag.sub(start=2).to_owned()
  } else {
    tag
  }
  if tag_text.length() < 2 ||
    !tag_text.has_prefix("\"") ||
    !tag_text.has_suffix("\"") {
    return false
  }
  let inside = tag_text.sub(start=1, end=tag_text.length() - 1).to_owned()
  !inside.contains("\"") && !inside.contains("\r") && !inside.contains("\n")
}

///|
fn valid_last_modified(value : String) -> Bool {
  parse_http_date(value) is Some(_)
}

///|
pub fn response_validator_kind(response : ResponseMeta) -> ValidatorKind? {
  let etag = match response.headers.get_first("etag") {
    Some(value) => if is_valid_etag(value) { Some(value) } else { None }
    None => None
  }
  let last_modified = match response.headers.get_first("last-modified") {
    Some(value) => if valid_last_modified(value) { Some(value) } else { None }
    None => None
  }
  match (etag, last_modified) {
    (Some(tag), Some(_)) =>
      if etag_is_weak(tag) {
        Some(WeakEtagAndLastModified)
      } else {
        Some(StrongEtagAndLastModified)
      }
    (Some(tag), None) =>
      if etag_is_weak(tag) {
        Some(WeakEtag)
      } else {
        Some(StrongEtag)
      }
    (None, Some(_)) => Some(LastModified)
    (None, None) => None
  }
}

///|
pub fn has_usable_validator(response : ResponseMeta) -> Bool {
  response_validator_kind(response) is Some(_)
}

///|
pub(all) struct RevalidationPlan {
  request : RequestMeta
  generated_headers : HeaderMap
  validator_kind : ValidatorKind?
  reasons : Array[CacheReason]
} derive(Eq, Debug)

///|
pub fn RevalidationPlan::can_revalidate(self : RevalidationPlan) -> Bool {
  self.request.headers.contains("if-none-match") ||
  self.request.headers.contains("if-modified-since")
}

///|
/// Generate conditional fields without replacing conditions supplied by the
/// caller. Strong and weak ETags are both valid for If-None-Match.
pub fn create_revalidation_plan(
  request : RequestMeta,
  stored_response : ResponseMeta,
) -> RevalidationPlan {
  let headers = request.headers.copy()
  let generated = HeaderMap::new()
  let reasons : Array[CacheReason] = []
  let caller_has_etag = headers.contains("if-none-match")
  let caller_has_date = headers.contains("if-modified-since")
  if caller_has_etag || caller_has_date {
    reasons.push(
      CacheReason::with_rfc(RevalidateCallerCondition, "RFC9111-4.3.2"),
    )
  }
  let mut added_etag : String? = None
  let mut added_date = false
  if !caller_has_etag {
    match stored_response.headers.get_first("etag") {
      Some(value) =>
        if is_valid_etag(value) {
          ignore(headers.set("if-none-match", value))
          ignore(generated.set("if-none-match", value))
          added_etag = Some(value)
          reasons.push(CacheReason::with_rfc(RevalidateEtag, "RFC9111-4.3.2"))
        }
      None => ()
    }
  }
  if !caller_has_date {
    match stored_response.headers.get_first("last-modified") {
      Some(value) =>
        if valid_last_modified(value) {
          ignore(headers.set("if-modified-since", value))
          ignore(generated.set("if-modified-since", value))
          added_date = true
          reasons.push(
            CacheReason::with_rfc(RevalidateLastModified, "RFC9111-4.3.2"),
          )
        }
      None => ()
    }
  }
  let validator_kind = match (added_etag, added_date) {
    (Some(tag), true) => {
      reasons.push(CacheReason::new(RevalidateBoth))
      if etag_is_weak(tag) {
        Some(WeakEtagAndLastModified)
      } else {
        Some(StrongEtagAndLastModified)
      }
    }
    (Some(tag), false) =>
      if etag_is_weak(tag) {
        Some(WeakEtag)
      } else {
        Some(StrongEtag)
      }
    (None, true) => Some(LastModified)
    (None, false) =>
      if caller_has_etag || caller_has_date {
        Some(CallerConditions)
      } else {
        None
      }
  }
  RevalidationPlan::{
    request: request.with_headers(headers),
    generated_headers: generated,
    validator_kind,
    reasons,
  }
}

///|
pub suberror ValidationError {
  ExpectedNotModified(Int)
} derive(Eq, Debug)

///|
pub fn ValidationError::message(self : ValidationError) -> String {
  match self {
    ExpectedNotModified(status) => "expected status 304, received \{status}"
  }
}

///|
fn connection_named_fields(headers : HeaderMap) -> Array[String] {
  let result : Array[String] = []
  match headers.get("connection") {
    Some(value) =>
      for part in value.split(",") {
        let name = normalize_header_name(part.to_owned())
        if name != "" && !result.contains(name) {
          result.push(name)
        }
      }
    None => ()
  }
  result
}

///|
fn forbidden_304_merge_field(
  name : String,
  connection_fields : Array[String],
) -> Bool {
  name == "connection" ||
  name == "keep-alive" ||
  name == "proxy-authenticate" ||
  name == "proxy-authorization" ||
  name == "te" ||
  name == "trailer" ||
  name == "transfer-encoding" ||
  name == "upgrade" ||
  name == "content-length" ||
  connection_fields.contains(name)
}

///|
fn replace_header_values(
  target : HeaderMap,
  name : String,
  values : Array[String],
) -> Unit {
  if values.length() == 0 {
    return
  }
  ignore(target.set(name, values[0]))
  for index = 1; index < values.length(); index = index + 1 {
    ignore(target.append(name, values[index]))
  }
}

///|
fn merged_304_headers(
  cached : HeaderMap,
  not_modified : HeaderMap,
) -> HeaderMap {
  let result = cached.copy()
  ignore(result.remove("warning"))
  let connection_fields = connection_named_fields(not_modified)
  for name in not_modified.names() {
    if !forbidden_304_merge_field(name, connection_fields) {
      replace_header_values(result, name, not_modified.get_all(name))
    }
  }
  result
}

///|
pub(all) struct NotModifiedResult {
  entry : StoredEntry
  storage : StorageDecision
  reasons : Array[CacheReason]
} derive(Eq, Debug)

///|
/// Merge a 304 response into a stored entry while retaining the cached status
/// and body, then recompute cacheability, freshness, and variant identity.
pub fn merge_not_modified(
  entry : StoredEntry,
  not_modified : ResponseMeta,
  options : CacheOptions,
  now : Timestamp,
) -> NotModifiedResult raise ValidationError {
  if not_modified.status != 304 {
    raise ExpectedNotModified(not_modified.status)
  }
  let headers = merged_304_headers(entry.response.headers, not_modified.headers)
  let response = ResponseMeta::new(
    entry.response.status,
    headers,
    not_modified.request_time,
    not_modified.response_time,
    entry.response.body_complete,
  )
  let storage = evaluate_storage(entry.request, response, options)
  let variant = build_variant_key(entry.request.headers, parse_vary(headers)).unwrap_or(
    entry.variant_key,
  )
  let merged = StoredEntry::{
    primary_key: entry.primary_key,
    variant_key: variant,
    request: entry.request,
    response,
    body: entry.body,
    policy: storage.policy,
    stored_at: now,
    last_accessed_at: now,
  }
  let reasons : Array[CacheReason] = [
    CacheReason::with_rfc(RevalidateNotModified, "RFC9111-4.3.4"),
  ]
  reasons.push_iter(storage.reasons.iter())
  NotModifiedResult::{ entry: merged, storage, reasons }
}