// storage.mbt — three-tier JSON storage (mirrors Python `storage.py`)
//
// Layout:
//   /meta_index.json          in-memory index snapshot (categories + entries)
//   //.json         one file per entry (optionally encrypted)
//
// The in-memory index is held in a `@ref.Ref` cell so that by-value method
// receivers still share the same underlying map: a `Storage` value (and any
// copy of it) mutates the one shared index, which is what makes
// `sys.remember(...)` / `sys.recall(...)` update state in place.

///|
const SEP : String = "/"

///|
/// Three-tier JSON-backed memory store.
pub struct Storage {
  root : String
  crypto : CryptoProvider
  index_path : String
  entries : @ref.Ref[Map[String, MemoryEntry]]
}

///|
fn join(a : String, b : String) -> String {
  a + SEP + b
}

///|
fn ensure_dir(path : String) -> Unit {
  if !@fs.path_exists(path) {
    @fs.create_dir(path) catch {
      _ => ()
    }
  }
}

///|
fn Storage::ensure_dirs(self : Storage) -> Unit {
  ensure_dir(self.root)
  for t in all_types() {
    ensure_dir(join(self.root, t.value()))
  }
}

///|
fn Storage::entry_path(self : Storage, entry : MemoryEntry) -> String {
  join(join(self.root, entry.mtype), entry.id + ".json")
}

///|
fn Storage::load_index(self : Storage) -> Unit {
  if !@fs.path_exists(self.index_path) {
    return
  }
  try {
    let raw = @fs.read_file_to_string(self.index_path)
    let dec = self.crypto.decrypt(raw)
    let j = @json.parse(dec)
    match j {
      Object(m) =>
        match m.get("entries") {
          Some(Object(ents)) =>
            for v in ents.values() {
              match MemoryEntry::from_json(v) {
                Some(e) => self.entries.val.set(e.id, e)
                None => ()
              }
            }
          _ => ()
        }
      _ => ()
    }
  } catch {
    _ => ()
  }
}

///|
fn Storage::save_index(self : Storage) -> Unit raise @fs.IOError {
  let cats : Array[Json] = []
  for t in all_types() {
    let tv = t.value()
    let of_type = self.entries.val
      .values()
      .filter(fn(e) { e.mtype == tv })
      .to_array()
    let count = of_type.length()
    let updated = if count > 0 {
      let mut mx = of_type[0].timestamp
      for e in of_type {
        if e.timestamp > mx {
          mx = e.timestamp
        }
      }
      mx.to_double()
    } else {
      0.0
    }
    let cat : Map[String, Json] = Map([], capacity=0)
    cat.set("type", Json::string(tv))
    cat.set("path", Json::string(join(self.root, tv)))
    cat.set("entry_count", Json::number(count.to_double()))
    cat.set("updated_at", Json::number(updated))
    cats.push(Json::object(cat))
  }
  let ents : Map[String, Json] = Map([], capacity=0)
  for e in self.entries.val.values() {
    ents.set(e.id, e.to_json())
  }
  let root_map : Map[String, Json] = Map([], capacity=0)
  root_map.set("categories", Json::array(cats))
  root_map.set("entries", Json::object(ents))
  let s = Json::object(root_map).stringify(indent=2)
  @fs.write_string_to_file(self.index_path, self.crypto.encrypt(s))
}

///|
/// Open (or create) a store rooted at `root`.
pub fn Storage::new(
  root : String,
  crypto? : CryptoProvider = CryptoProvider::disabled(),
) -> Storage {
  let s = {
    root,
    crypto,
    index_path: join(root, "meta_index.json"),
    entries: @ref.Ref(Map([], capacity=0)),
  }
  s.ensure_dirs()
  s.load_index()
  // Best-effort initial index write; persistence is retried on first `put`.
  s.save_index() catch {
    _ => ()
  }
  s
}

///|
/// Persist an entry (file + index).
pub fn Storage::put(
  self : Storage,
  entry : MemoryEntry,
) -> Unit raise @fs.IOError {
  let path = self.entry_path(entry)
  let raw = entry.to_json().stringify(indent=2)
  @fs.write_string_to_file(path, self.crypto.encrypt(raw))
  self.entries.val.set(entry.id, entry)
  self.save_index()
}

///|
/// Fetch an entry by id (from the in-memory index).
pub fn Storage::get(self : Storage, entry_id : String) -> MemoryEntry? {
  self.entries.val.get(entry_id)
}

///|
/// Delete an entry by id. Returns true if something was removed.
pub fn Storage::delete(
  self : Storage,
  entry_id : String,
) -> Bool raise @fs.IOError {
  match self.entries.val.get(entry_id) {
    None => false
    Some(e) => {
      let path = self.entry_path(e)
      if @fs.path_exists(path) {
        @fs.remove_file(path) catch {
          _ => ()
        }
      }
      self.entries.val.remove(entry_id)
      self.save_index()
      true
    }
  }
}

///|
/// All entries.
pub fn Storage::all_entries(self : Storage) -> Array[MemoryEntry] {
  self.entries.val.values().to_array()
}

///|
/// Entries of a given type.
pub fn Storage::query_by_type(
  self : Storage,
  mtype : MemoryType,
) -> Array[MemoryEntry] {
  self.entries.val
  .values()
  .filter(fn(e) { e.mtype == mtype.value() })
  .to_array()
}

///|
/// Aggregate statistics (mirrors Python `Storage.stats`).
pub fn Storage::stats(self : Storage) -> Map[String, Json] {
  let entries = self.entries.val.values().to_array()
  let by_type : Map[String, Int] = Map([], capacity=0)
  let mut active = 0
  let mut deprecated = 0
  for e in entries {
    by_type.set(e.mtype, by_type.get_or_default(e.mtype, 0) + 1)
    if e.status == MemoryStatus::Active.value() {
      active = active + 1
    } else if e.status == MemoryStatus::Deprecated.value() {
      deprecated = deprecated + 1
    }
  }
  let by_type_json : Map[String, Json] = Map([], capacity=0)
  for k, v in by_type {
    by_type_json.set(k, Json::number(v.to_double()))
  }
  let res : Map[String, Json] = Map([], capacity=0)
  res.set("total", Json::number(entries.length().to_double()))
  res.set("by_type", Json::object(by_type_json))
  res.set("active", Json::number(active.to_double()))
  res.set("deprecated", Json::number(deprecated.to_double()))
  res
}