///|
/// Async cache storage used by the serving layer. Implemented by
/// `@cache.RedisClient`; the HTTP layer only knows this interface.
pub(open) trait CacheStore {
  async fn cache_get(Self, String) -> String?
  async fn cache_set_ex(Self, String, String, Int32) -> Bool
}

///|
/// Response-cache policy for GET requests, bound to a concrete store.
pub(all) struct CachePolicy[S] {
  store : S
  prefix : String
  ttl : Int32
}

///|
pub fn[S] CachePolicy::key(self : CachePolicy[S], path : String) -> String {
  self.prefix + ":" + path
}

///|
/// A cached response: the Content-Type and the body.
pub(all) struct CachedResponse {
  content_type : String
  body : String
}

///|
fn encode_payload(cached : CachedResponse) -> String {
  cached.content_type + "\n" + cached.body
}

///|
fn decode_payload(payload : String) -> CachedResponse? {
  let split = payload.split("\n").next()
  match split {
    Some(first_line) => {
      let content_type = first_line.to_owned()
      let header_len = content_type.length() + 1
      if payload.length() < header_len {
        return None
      }
      Some({ content_type, body: payload[header_len:].to_owned(), })
    }
    None => None
  }
}

///|
/// Look up a cached response; transport failures surface as raises so the
/// serving layer can degrade gracefully.
pub async fn[S : CacheStore] CachePolicy::lookup(
  self : CachePolicy[S],
  path : String,
) -> CachedResponse? {
  match self.store.cache_get(self.key(path)) {
    Some(payload) => decode_payload(payload)
    None => None
  }
}

///|
/// Store a response. Transport failures raise; the serving layer ignores
/// them (a cache write failure must not fail the request).
pub async fn[S : CacheStore] CachePolicy::store(
  self : CachePolicy[S],
  path : String,
  cached : CachedResponse,
) -> Unit {
  let _ = self.store.cache_set_ex(
    self.key(path),
    encode_payload(cached),
    self.ttl,
  )
}