///|
pub(all) struct CacheDecision {
  action : CacheActionKind
  trace : CacheTrace
  storage : StorageDecision
} derive(Eq, Debug)

///|
fn add_request_policy_reasons(
  trace : CacheTrace,
  policy : RequestPolicy,
) -> Unit {
  if policy.bypass {
    trace.add_reason(CacheReason::with_rfc(RequestNoStore, "RFC9111-5.2.1.5"))
  }
  if policy.force_revalidation {
    trace.add_reason(CacheReason::with_rfc(RequestNoCache, "RFC9111-5.2.1.4"))
  }
  if policy.only_if_cached {
    trace.add_reason(
      CacheReason::with_rfc(RequestOnlyIfCached, "RFC9111-5.2.1.7"),
    )
  }
  if policy.max_age is Some(_) {
    trace.add_reason(CacheReason::new(RequestMaxAge))
  }
  if policy.min_fresh.seconds() > 0 {
    trace.add_reason(CacheReason::new(RequestMinFresh))
  }
  if policy.max_stale is Some(_) || policy.max_stale_unbounded {
    trace.add_reason(CacheReason::new(RequestMaxStale))
  }
}

///|
fn response_has_validator(response : ResponseMeta) -> Bool {
  has_usable_validator(response)
}

///|
fn decision_with_action(
  action : CacheActionKind,
  trace : CacheTrace,
  storage : StorageDecision,
  reason : CacheReasonCode,
) -> CacheDecision {
  trace.action = action
  trace.add_reason(CacheReason::new(reason))
  CacheDecision::{ action, trace, storage }
}

///|
/// Evaluate one matching cached response at the injected time. Variant lookup
/// and conditional header generation are layered on this deterministic policy
/// function.
pub fn evaluate_cached_response(
  request : RequestMeta,
  response : ResponseMeta,
  options : CacheOptions,
  now : Timestamp,
) -> CacheDecision {
  let request_policy = evaluate_request_policy(request)
  let storage = evaluate_storage(request, response, options)
  let trace = CacheTrace::new(Fetch)
  add_request_policy_reasons(trace, request_policy)
  if request_policy.bypass {
    return decision_with_action(Bypass, trace, storage, RuntimeBypass)
  }
  if !storage.storable {
    for reason in storage.reasons {
      trace.add_reason(reason)
    }
    if request_policy.only_if_cached {
      return decision_with_action(
        OnlyIfCachedMiss,
        trace,
        storage,
        RuntimeOnlyIfCachedMiss,
      )
    }
    return decision_with_action(Fetch, trace, storage, RuntimeFetch)
  }
  let age = calculate_current_age(response, now)
  let freshness = calculate_freshness_lifetime(response, options)
  let response_directives = cache_control_from_headers(response.headers)
  let effective_lifetime = match request_policy.max_age {
    Some(max_age) => freshness.lifetime.min(max_age)
    None => freshness.lifetime
  }
  let required_age = age.current_age.saturating_add(request_policy.min_fresh)
  let is_fresh = required_age < effective_lifetime
  let force_revalidation = request_policy.force_revalidation ||
    response_directives.no_cache
  let stale_by = age.current_age.saturating_sub(effective_lifetime)
  trace.age = Some(age)
  trace.freshness_lifetime = Some(effective_lifetime)
  trace.add_reason(CacheReason::with_rfc(AgeCorrected, "RFC9111-4.2.3"))
  if age.clock_clamped {
    trace.add_reason(CacheReason::new(AgeClockClamped))
  }
  if age.overflow_clamped {
    trace.add_reason(CacheReason::new(AgeOverflowClamped))
  }
  trace.add_reason(CacheReason::new(freshness.source.reason()))
  if is_fresh && !force_revalidation {
    trace.add_reason(CacheReason::with_rfc(FreshStillFresh, "RFC9111-4.2"))
    return decision_with_action(ServeFresh, trace, storage, RuntimeHit)
  }
  if !is_fresh {
    trace.stale_by = Some(stale_by)
    trace.add_reason(CacheReason::with_rfc(FreshStale, "RFC9111-4.2"))
  }
  let stale_forbidden = response_directives.must_revalidate ||
    (options.mode is Shared && response_directives.proxy_revalidate) ||
    force_revalidation
  if !is_fresh && !stale_forbidden && request_policy.allows_stale(stale_by) {
    return decision_with_action(ServeStale, trace, storage, RuntimeHit)
  }
  if request_policy.only_if_cached {
    return decision_with_action(
      OnlyIfCachedMiss,
      trace,
      storage,
      RuntimeOnlyIfCachedMiss,
    )
  }
  if response_has_validator(response) {
    decision_with_action(Revalidate, trace, storage, RevalidateEtag)
  } else {
    decision_with_action(Fetch, trace, storage, RuntimeFetch)
  }
}