///|
/// Build a CacheEntry from a URL and @http.HttpResponse.
fn build_cache_entry(url : String, response : @http.HttpResponse) -> CacheEntry {
  let cache_control_header = @http.get_header(response.headers, "cache-control")
  let directives = match cache_control_header {
    Some(h) => parse_cache_control(h)
    None => CacheDirectives::default()
  }
  let etag = @http.get_header(response.headers, "etag")
  let last_modified = @http.get_header(response.headers, "last-modified")
  {
    url,
    status: response.status,
    headers: response.headers,
    body: response.body,
    etag,
    last_modified,
    directives,
    stored_at: now_seconds(),
  }
}

///|
/// Convert a CacheEntry back to an @http.HttpResponse.
fn entry_to_response(entry : CacheEntry) -> @http.HttpResponse {
  { status: entry.status, headers: entry.headers, body: entry.body }
}

///|
/// Fetch with transparent HTTP caching (async version).
/// Wraps an async fetcher (e.g. @http.fetch) with cache lookup/store.
/// Only GET requests are cached.
pub async fn[CacheBackend : HttpCacheBackend] cached_fetch_async(
  url~ : String,
  options~ : @http.FetchOptions,
  cache~ : CacheBackend,
  fetcher~ : async (String, @http.FetchOptions) -> @http.HttpResponse raise @http.HttpError,
) -> @http.HttpResponse raise @http.HttpError {
  if options.http_method != "GET" {
    return fetcher(url, options)
  }
  let now = now_seconds()
  match cache.lookup(url) {
    Some(entry) =>
      if is_fresh(entry, now) {
        println("[cache] HIT (fresh): " + url)
        return entry_to_response(entry)
      } else {
        println("[cache] STALE (revalidating): " + url)
        let modified_headers : Map[String, String] = {}
        for k, v in options.headers {
          modified_headers[k] = v
        }
        match entry.etag {
          Some(etag) => modified_headers["If-None-Match"] = etag
          None => ()
        }
        match entry.last_modified {
          Some(lm) => modified_headers["If-Modified-Since"] = lm
          None => ()
        }
        let modified_options : @http.FetchOptions = {
          ..options,
          headers: modified_headers,
        }
        let response = fetcher(url, modified_options)
        if response.status == 304 {
          println("[cache] 304 Not Modified: " + url)
          let refreshed : CacheEntry = { ..entry, stored_at: now }
          cache.store(refreshed)
          return entry_to_response(entry)
        }
        let new_entry = build_cache_entry(url, response)
        if !new_entry.directives.no_store &&
          is_cacheable_status(response.status) {
          cache.store(new_entry)
        }
        return response
      }
    None => {
      println("[cache] MISS: " + url)
      let response = fetcher(url, options)
      let entry = build_cache_entry(url, response)
      if !entry.directives.no_store && is_cacheable_status(response.status) {
        cache.store(entry)
      }
      return response
    }
  }
}

///|
/// Fetch with transparent HTTP caching (sync version for testing).
/// Only GET requests are cached. The fetcher parameter allows callers to
/// inject any transport function.
pub fn[CacheBackend : HttpCacheBackend] cached_fetch(
  url~ : String,
  options~ : @http.FetchOptions,
  cache~ : CacheBackend,
  fetcher~ : (String, @http.FetchOptions) -> @http.HttpResponse raise @http.HttpError,
) -> @http.HttpResponse raise @http.HttpError {
  // Only cache GET
  if options.http_method != "GET" {
    return fetcher(url, options)
  }
  let now = now_seconds()
  match cache.lookup(url) {
    Some(entry) =>
      if is_fresh(entry, now) {
        return entry_to_response(entry)
      } else {
        // Stale: conditional request
        let modified_headers : Map[String, String] = {}
        for k, v in options.headers {
          modified_headers[k] = v
        }
        match entry.etag {
          Some(etag) => modified_headers["If-None-Match"] = etag
          None => ()
        }
        match entry.last_modified {
          Some(lm) => modified_headers["If-Modified-Since"] = lm
          None => ()
        }
        let modified_options : @http.FetchOptions = {
          ..options,
          headers: modified_headers,
        }
        let response = fetcher(url, modified_options)
        if response.status == 304 {
          let refreshed : CacheEntry = { ..entry, stored_at: now }
          cache.store(refreshed)
          return entry_to_response(entry)
        }
        let new_entry = build_cache_entry(url, response)
        if !new_entry.directives.no_store &&
          is_cacheable_status(response.status) {
          cache.store(new_entry)
        }
        return response
      }
    None => {
      let response = fetcher(url, options)
      let entry = build_cache_entry(url, response)
      if !entry.directives.no_store && is_cacheable_status(response.status) {
        cache.store(entry)
      }
      return response
    }
  }
}