// `mctl model cache` (← goctl's `model … --cache`): the cache-aside layer in front
// of a table's CRUD.
//
// A cache is not a second copy of the truth: it holds what a read already produced,
// and the write path deletes rather than updates, because a deleted key cannot be
// stale. That is the whole of what is generated here — read-through on `find`,
// invalidate on `write` and on `evict` — and it is generated for one table at a time
// so a service can cache the two tables it actually reads hot.
//
// The port is generic (`Cache[T]`) rather than JSON-shaped: a cache holds the record
// the table produced, so nothing is encoded on the way in or out, and an
// in-memory implementation is a `Map` — which is what the generated `memory_cache`
// is, and what a test drives the layer with.
//
// A broken cache never fails a request: a miss falls back to the table, and a failed
// `set` or `del` is swallowed. A cache that can take a service down is worse than no
// cache at all.

///|
/// The prelude every cached layer shares: the failure a cache may report, the
/// three-call port, and an in-memory implementation to run or test against.
fn cache_prelude() -> String {
  "// Code generated by moonctl. DO NOT EDIT.\n" +
  "//\n" +
  "// `mctl model cache`: cache-aside CRUD for the tables of a spec, over the\n" +
  "// `Cache[T]` port below. The record types are the ones `mctl model` generates for\n" +
  "// the same spec, so this file goes in the same package as `_model.mbt`.\n" +
  "//\n" +
  "// Nothing here needs an import: the port is generic, so a cache holds records\n" +
  "// rather than the text they serialise to.\n" +
  "\n" +
  "///|\n/// The one failure a cache reports. A cache failure is not a service failure: the\n/// generated layer falls back to the table when a read misses, and swallows a failed\n/// write, so this surfaces only where a caller wants to see it.\npub(all) suberror CacheError {\n  CacheFailed(why~ : String)\n}\n" +
  "\n" +
  "///|\n/// A cache of `T` by key: three calls, which every store — Redis, Memcached, a\n/// `Map` — reduces to. `get` answers `None` for a key that is not there, and a\n/// `raise` only when the cache itself is broken.\npub(all) struct Cache[T] {\n  get : (String) -> T? raise CacheError\n  set : (String, T) -> Unit raise CacheError\n  del : (String) -> Unit raise CacheError\n}\n" +
  "\n" +
  "///|\n/// An in-memory cache: what a test drives the layer with, and what a single-process\n/// service may use as it is.\npub fn[T] memory_cache() -> Cache[T] {\n  let store : Map[String, T] = Map([])\n  {\n    get: key => store.get(key),\n    set: (key, value) => {\n      store[key] = value\n    },\n    del: key => {\n      ignore(store.remove(key))\n    },\n  }\n}\n\n"
}

///|
/// The key type of a table: its key field's type, without the `?` an optional field
/// carries — a key a cache is looked up by is never absent.
fn cache_key_type(f : @api.Field) -> String {
  let t = @text.trim(f.type_)
  if @text.ends_with(t, "?") && t.length() > 1 {
    t[0:t.length() - 1].to_owned()
  } else {
    t
  }
}

///|
/// One table's cache-aside layer: the key function, the read-through `find`, the
/// invalidating `write`, and `evict`.
fn emit_cached(
  name : String,
  key : @api.Field,
  table : String,
  vp : String,
) -> String {
  let kv = key.mbt_name()
  let kt = cache_key_type(key)
  "///|\n/// The key a `" +
  name +
  "` record is cached under: the table and its `" +
  kv +
  "`.\npub fn " +
  vp +
  "_cache_key(" +
  kv +
  " : " +
  kt +
  ") -> String {\n  " +
  @text.quote(table + ":") +
  " + " +
  kv +
  ".to_string()\n}\n\n" +
  "///|\n/// `" +
  name +
  "` by `" +
  kv +
  "`, through the cache: a hit answers without touching `load`, and a\n/// miss calls it and caches what came back. A cache that fails on the read is\n/// treated as a miss, so a broken cache costs latency rather than availability.\npub fn " +
  vp +
  "_find_cached(\n  cache : Cache[" +
  name +
  "],\n  " +
  kv +
  " : " +
  kt +
  ",\n  load : (" +
  kt +
  ") -> " +
  name +
  "? raise CacheError,\n) -> " +
  name +
  "? raise CacheError {\n  let key = " +
  vp +
  "_cache_key(" +
  kv +
  ")\n  let hit : " +
  name +
  "? = (cache.get)(key) catch { _ => None }\n  match hit {\n    Some(record) => Some(record)\n    None =>\n      match load(" +
  kv +
  ") {\n        Some(record) => {\n          (cache.set)(key, record) catch { _ => () }\n          Some(record)\n        }\n        None => None\n      }\n  }\n}\n\n" +
  "///|\n/// Write a `" +
  name +
  "` through to `save`, then drop the cached copy: a deleted key cannot be\n/// stale, whereas an updated one can.\npub fn " +
  vp +
  "_write_cached(\n  cache : Cache[" +
  name +
  "],\n  record : " +
  name +
  ",\n  save : (" +
  name +
  ") -> Unit raise CacheError,\n) -> Unit raise CacheError {\n  save(record)\n  (cache.del)(" +
  vp +
  "_cache_key(record." +
  kv +
  ")) catch { _ => () }\n}\n\n" +
  "///|\n/// Delete a `" +
  name +
  "` through to `remove`, then drop the cached copy.\npub fn " +
  vp +
  "_evict_cached(\n  cache : Cache[" +
  name +
  "],\n  " +
  kv +
  " : " +
  kt +
  ",\n  remove : (" +
  kt +
  ") -> Unit raise CacheError,\n) -> Unit raise CacheError {\n  remove(" +
  kv +
  ")\n  (cache.del)(" +
  vp +
  "_cache_key(" +
  kv +
  ")) catch { _ => () }\n}\n\n"
}

///|
/// Generate the cached CRUD layer for every table of `spec`: for a `type` block with
/// a field to look records up by, the key function plus read-through `find`,
/// invalidating `write`, and `evict`. A block with no single-valued field has no key
/// to cache under, so it gets no layer and the file says why.
///
/// The output needs no import and no driver: the record types it names are the ones
/// `mctl model` generates for the same spec.
pub fn generate_cached(spec : @api.Spec) -> String {
  let mut out = cache_prelude()
  let mut wrote = false
  for t in spec.types {
    let fields = @api.flat_fields(t, spec.types)
    if fields.length() == 0 {
      continue
    }
    match key_field(fields) {
      None => ()
      Some(key) => {
        out = out +
          emit_cached(t.name, key, table_name(t.name), @text.to_snake(t.name))
        wrote = true
      }
    }
  }
  if wrote == false {
    out = out +
      "// No `type` block of this spec has a field a record could be looked up by, so\n// there is nothing to cache.\n"
  }
  out
}

///|
/// The file a cached layer lands in: `greet` → `greet_cache.mbt`, beside the
/// `_model.mbt` whose records it caches.
pub fn cache_file_name(service : String) -> String {
  (if service == "" { "model" } else { @text.to_snake(service) }) + "_cache.mbt"
}