// A cache in front of the calls that are expensive and repeat: the same
// request, asked again inside the time a result stays good, is answered from
// the last one.

///|
/// One stored answer and the moment it was made.
struct Entry {
  value : Json
  started : @bench.Timestamp
}

///|
/// Answers a call from memory when the same call was made recently (← FastMCP's
/// `CachingMiddleware`).
///
/// The key is the operation, the name and the arguments, with object keys
/// sorted at every depth, so a client that sends the same arguments in another
/// order still gets the stored answer. Errors and asks for client input are
/// never stored: repeating them would hide the reason and replay a state the
/// client has already answered.
///
/// `ttl_ms` is how long an answer stays good — five minutes, the default of
/// FastMCP's cache. `capacity` is how many answers are kept before the oldest
/// goes; `shared` says whether one caller's answer may be handed to another,
/// which is only safe when the answer does not depend on who asked.
pub struct Caching {
  priv ttl_ms : Int32
  priv capacity : Int32
  priv operations : Array[Operation]
  priv shared : Bool
  priv entries : Map[String, Entry]
  priv mut order : Array[String]
  priv mut head : Int32
  priv mut hits : Int32
  priv mut misses : Int32
}

///|
/// A cache of every call and every listing, kept for five minutes.
pub fn Caching::new(
  ttl_ms~ : Int32 = 300_000,
  capacity~ : Int32 = 1000,
  operations~ : Array[Operation] = [
    ListTools,
    ListResources,
    ListTemplates,
    ListPrompts,
    CallTool,
    ReadResource,
    GetPrompt,
  ],
  shared~ : Bool = false,
) -> Caching {
  {
    ttl_ms,
    capacity,
    operations,
    shared,
    entries: Map([]),
    order: [],
    head: 0,
    hits: 0,
    misses: 0,
  }
}

///|
/// The layer this cache installs on a server.
pub fn Caching::layer(self : Caching) -> Layer {
  Layer::new(name="cache", run=async fn(
    call,
    next,
  ) -> Json raise @exceptions.FastMCPError {
    guard self.operations.contains(call.operation) else {
      return next.then(call)
    }
    let key = self.key(call)
    match self.take(key) {
      Some(value) => {
        self.hits = self.hits + 1
        return value
      }
      None => {
        self.misses = self.misses + 1
        let value = next.then(call)
        if worth_keeping(value) {
          self.put(key, value)
        }
        return value
      }
    }
  })
}

///|
/// How many calls were answered without asking the server.
pub fn Caching::hits(self : Caching) -> Int32 {
  self.hits
}

///|
/// How many went through.
pub fn Caching::misses(self : Caching) -> Int32 {
  self.misses
}

///|
/// How many answers are held right now.
pub fn Caching::size(self : Caching) -> Int32 {
  self.entries.length()
}

///|
/// Forget everything.
pub fn Caching::clear(self : Caching) -> Unit {
  self.entries.clear()
  self.order.clear()
  self.head = 0
}

///|
/// The key one call is stored under.
fn Caching::key(self : Caching, call : Call) -> String {
  if self.shared {
    call.fingerprint()
  } else {
    Json([Json::string(call.subject()), Json::string(call.fingerprint())]).stringify()
  }
}

///|
/// The stored answer for `key`, if it is still good. A stale one is dropped
/// where it is found.
fn Caching::take(self : Caching, key : String) -> Json? {
  guard self.entries.get(key) is Some(entry) else { return None }
  let age_ms = @bench.monotonic_clock_end(entry.started) / 1000.0
  if age_ms > self.ttl_ms.to_double() {
    self.entries.remove(key)
    return None
  }
  Some(entry.value)
}

///|
/// Store `value` under `key`, dropping the oldest answer when the cache is
/// full.
fn Caching::put(self : Caching, key : String, value : Json) -> Unit {
  if self.entries.contains(key) {
    self.entries[key] = { value, started: @bench.monotonic_clock_start(), }
    return
  }
  while self.entries.length() >= self.capacity &&
        self.head < self.order.length() {
    let oldest = self.order[self.head]
    self.head = self.head + 1
    self.entries.remove(oldest)
  }
  // The queue only grows at the end, so the keys already dropped are cut off
  // once they are more than half of it.
  if self.head >= 32 && self.head * 2 >= self.order.length() {
    self.order = self.order[self.head:].to_owned()
    self.head = 0
  }
  self.entries[key] = { value, started: @bench.monotonic_clock_start(), }
  self.order.push(key)
}

///|
/// Whether an answer may be stored: a refusal is the server's answer to those
/// arguments, which the next call deserves to hear for itself, and an ask for
/// client input carries a state that belongs to one exchange.
fn worth_keeping(value : Json) -> Bool {
  guard value is Object(fields) else { return true }
  if fields.get("isError") == Some(Json::boolean(true)) {
    return false
  }
  if fields.get("resultType") == Some(Json::string("input_required")) {
    return false
  }
  !(fields.contains("inputRequests") || fields.contains("requestState"))
}