///|
pub enum Target {
  Memory
  User
}

///|
pub enum AddResult {
  AddOk(Int, Int) // (used_chars, max_chars)
  AddErr(String, String) // (error_message, error_code)
}

///|
pub enum OpResult {
  OpOk(Int, Int) // (used_chars, max_chars)
  OpErr(String, String) // (error_message, error_code)
}

// --- Filesystem ops ---
//
// Core reads / writes / exists / mkdir -p are backed by the cross-target
// moonbitlang/x/fs. The atomic-write path still reaches into node:fs
// because we need fsync + rename guarantees that x/fs doesn't surface; a
// future native stub can fill that in. For now atomic write is
// target-gated to js (see moon.pkg) and native users get best-effort
// write_string_to_file instead.

///| Exists check, cross-target.
fn ffi_exists(path : String) -> Bool {
  @fs.path_exists(path)
}

///| Read as UTF-8 string, returning "" on any error (consistent with the
///| previous JS semantics — missing files are treated as empty).
fn ffi_read_file(path : String) -> String {
  (try? @fs.read_file_to_string(path)).unwrap_or("")
}

///| `mkdir -p`: walk the path and create intermediate dirs. Built on
///| moonbitlang/x/fs.create_dir which is not recursive.
fn ffi_mkdirp(path : String) -> Unit {
  if @fs.path_exists(path) {
    return
  }
  // Recurse on the parent first; ignore errors (already-exists etc).
  let mut last = -1
  let p = path.view()
  let mut i = 0
  while i < p.length() {
    if p.get_char(i) is Some('/') {
      last = i
    }
    i = i + 1
  }
  if last > 0 {
    let parent = p[0:last].to_string()
    ffi_mkdirp(parent)
  }
  let _ = try? @fs.create_dir(path)

}

///| Join two path segments with "/". Not full POSIX-correct (no
///| normalization), but sufficient for the in-repo paths we build.
fn ffi_path_join(a : String, b : String) -> String {
  if a.is_empty() {
    b
  } else if a.has_suffix("/") {
    a + b
  } else {
    a + "/" + b
  }
}

///| Atomic write primitive: tmp + write + fsync + rename, with tmp
///| cleanup on any failure. Delegates to a per-target implementation
///| in `memory_store_js.mbt` (Node fs.openSync + fsyncSync + renameSync)
///| or `memory_store_native.mbt` (C stub in native_stub.c). Callers
///| here stay target-agnostic.
fn ffi_write_file(path : String, content : String) -> Unit {
  ffi_atomic_write_impl(path, content)
}

// --- Internal helpers ---

///|
fn target_filename(target : Target) -> String {
  match target {
    Memory => "MEMORY.md"
    User => "USER.md"
  }
}

///|
fn read_entries(path : String) -> Array[String] {
  if !ffi_exists(path) {
    return []
  }
  let raw = ffi_read_file(path)
  if raw.is_empty() {
    return []
  }
  raw
  .split(delim)
  .filter(fn(s) { !s.is_empty() })
  .map(fn(sv) { sv.to_string() })
  .to_array()
}

// --- MemoryStore struct ---

///|
pub struct MemoryStore {
  root_dir : String
  mut entries_memory : Array[String]
  mut entries_user : Array[String]
  snapshot_memory : String
  snapshot_user : String
  max_memory : Int
  max_user : Int
}

///| Load (or create) a MemoryStore from the given root directory.
pub fn MemoryStore::load(
  root_dir : String,
  max_memory~ : Int = 2200,
  max_user~ : Int = 1375
) -> MemoryStore {
  let mem_dir = ffi_path_join(root_dir, "memories")
  ffi_mkdirp(mem_dir)
  // No pre-creation of .lock: O_EXCL strategy requires the file to be absent
  // at acquire time. Pre-creating it would prevent with_lock from ever acquiring.
  let entries_memory = read_entries(
    ffi_path_join(mem_dir, target_filename(Memory)),
  )
  let entries_user = read_entries(
    ffi_path_join(mem_dir, target_filename(User)),
  )
  let snapshot_memory = render_block("memory", entries_memory, max_memory)
  let snapshot_user = render_block("user", entries_user, max_user)
  {
    root_dir,
    entries_memory,
    entries_user,
    snapshot_memory,
    snapshot_user,
    max_memory,
    max_user,
  }
}

///| Return a copy of entries for the given target.
pub fn MemoryStore::list(self : MemoryStore, target : Target) -> Array[String] {
  match target {
    Memory => self.entries_memory.copy()
    User => self.entries_user.copy()
  }
}

///| Return the frozen snapshot for the given target (taken at load time).
pub fn MemoryStore::snapshot(self : MemoryStore, target : Target) -> String {
  match target {
    Memory => self.snapshot_memory
    User => self.snapshot_user
  }
}

///| Add a new entry. Acquires a per-directory write lock before touching disk.
pub async fn MemoryStore::add(
  self : MemoryStore,
  target : Target,
  content : String
) -> AddResult {
  let trimmed = content.trim().to_string()
  if trimmed.is_empty() {
    return AddErr("empty content", "empty")
  }
  match scan_content(trimmed) {
    ScanBlocked(reason) =>
      return AddErr("blocked: \{reason}", "injection_blocked")
    ScanOk => ()
  }
  let mem_dir = ffi_path_join(self.root_dir, "memories")
  let file_path = ffi_path_join(mem_dir, target_filename(target))
  let lock_path = ffi_path_join(mem_dir, ".writing")
  let max = match target {
    Memory => self.max_memory
    User => self.max_user
  }
  let mut result : AddResult = AddErr("unreachable", "unreachable")
  with_lock(lock_path, fn() {
    let fresh = read_entries(file_path)
    if fresh.contains(trimmed) {
      result = AddErr("duplicate", "duplicate")
      return
    }
    let new_entries = fresh.copy()
    new_entries.push(trimmed)
    let joined = new_entries.join(delim)
    if joined.length() > max {
      result = AddErr("budget exceeded", "budget_exceeded")
      return
    }
    ffi_write_file(file_path, joined)
    match target {
      Memory => self.entries_memory = new_entries
      User => self.entries_user = new_entries
    }
    result = AddOk(joined.length(), max)
  })
  result
}

///| Replace the first entry containing old_substring with new_content.
pub async fn MemoryStore::replace(
  self : MemoryStore,
  target : Target,
  old_substring : String,
  new_content : String
) -> OpResult {
  let trimmed = new_content.trim().to_string()
  if trimmed.is_empty() {
    return OpErr("empty", "empty")
  }
  match scan_content(trimmed) {
    ScanBlocked(reason) =>
      return OpErr("blocked: \{reason}", "injection_blocked")
    ScanOk => ()
  }
  let mem_dir = ffi_path_join(self.root_dir, "memories")
  let file_path = ffi_path_join(mem_dir, target_filename(target))
  let lock_path = ffi_path_join(mem_dir, ".writing")
  let max = match target {
    Memory => self.max_memory
    User => self.max_user
  }
  let mut result : OpResult = OpErr("unreachable", "unreachable")
  with_lock(lock_path, fn() {
    let fresh = read_entries(file_path)
    // find index of first entry containing old_substring
    let mut idx = -1
    for i, e in fresh {
      if idx == -1 && e.contains(old_substring) {
        idx = i
      }
    }
    if idx == -1 {
      result = OpErr("substring not found", "substring_not_found")
      return
    }
    let replaced = fresh.copy()
    replaced[idx] = trimmed
    let joined = replaced.join(delim)
    if joined.length() > max {
      result = OpErr("budget exceeded", "budget_exceeded")
      return
    }
    ffi_write_file(file_path, joined)
    match target {
      Memory => self.entries_memory = replaced
      User => self.entries_user = replaced
    }
    result = OpOk(joined.length(), max)
  })
  result
}

///| Remove the single entry containing old_substring.
///| If zero entries match → substring_not_found.
///| If multiple entries match → ambiguous_match (no modification).
pub async fn MemoryStore::remove(
  self : MemoryStore,
  target : Target,
  old_substring : String
) -> OpResult {
  let mem_dir = ffi_path_join(self.root_dir, "memories")
  let file_path = ffi_path_join(mem_dir, target_filename(target))
  let lock_path = ffi_path_join(mem_dir, ".writing")
  let max = match target {
    Memory => self.max_memory
    User => self.max_user
  }
  let mut result : OpResult = OpErr("unreachable", "unreachable")
  with_lock(lock_path, fn() {
    let fresh = read_entries(file_path)
    let matches = fresh.filter(fn(e) { e.contains(old_substring) })
    if matches.length() == 0 {
      result = OpErr("substring not found", "substring_not_found")
      return
    }
    if matches.length() > 1 {
      let n = matches.length()
      result = OpErr(
        "\{n} entries matched the substring; remove is ambiguous",
        "ambiguous_match",
      )
      return
    }
    let filtered = fresh.filter(fn(e) { !e.contains(old_substring) })
    let joined = filtered.join(delim)
    ffi_write_file(file_path, joined)
    match target {
      Memory => self.entries_memory = filtered
      User => self.entries_user = filtered
    }
    result = OpOk(joined.length(), max)
  })
  result
}

// --- atomic write hardening tests ---

///| Sanity: the happy path leaves no .mem_*.tmp droppings.
test "atomic write: no tmp leak on success" {
  let dir = ffi_ms_tmp_dir()
  let path = dir + "/MEMORY.md"
  ffi_write_file(path, "hello")
  let leftovers = ffi_ms_list_tmp_droppings(dir)
  assert_eq(leftovers, 0)
  ffi_ms_rm_dir(dir)
}

///| On rename failure (target is a directory → EISDIR on POSIX), the tmp file
///| must be unlinked instead of accumulating under memories/.
test "atomic write: cleans up tmp when rename fails" {
  let dir = ffi_ms_tmp_dir()
  let collide = dir + "/MEMORY.md"
  ffi_ms_mkdir(collide)
  let ok = ffi_ms_try_write(collide, "hello")
  assert_eq(ok, false)
  let leftovers = ffi_ms_list_tmp_droppings(dir)
  assert_eq(leftovers, 0)
  ffi_ms_rm_dir(dir)
}

///| Monotonic counter for uniqueness across rapid consecutive calls.
let _ms_tmp_counter : Array[Int] = [0]

///| Cross-target scratch dir for inline tests — $TMPDIR (or /tmp) plus
///| a counter + ms suffix so consecutive calls in the same millisecond
///| still produce distinct paths.
fn ffi_ms_tmp_dir() -> String {
  let tmp = (@env.get_env_var("TMPDIR")).unwrap_or("/tmp")
  _ms_tmp_counter[0] = _ms_tmp_counter[0] + 1
  let suffix = @env.now().to_string() + "-" + _ms_tmp_counter[0].to_string()
  let dir = tmp + "/mnemo-ms-" + suffix
  let _ = try? @fs.create_dir(dir)

  dir
}

///| Best-effort recursive rmdir.
fn ffi_ms_rm_dir(path : String) -> Unit {
  let _ = try? @fs.remove_dir(path)

}

///| Cross-target mkdir.
fn ffi_ms_mkdir(path : String) -> Unit {
  let _ = try? @fs.create_dir(path)

}

///| Count files matching `.mem_*.tmp` in a directory.
fn ffi_ms_list_tmp_droppings(dir : String) -> Int {
  let entries = try {
    @fs.read_dir(dir)
  } catch {
    _ => return 0
  }
  let mut count = 0
  for name in entries {
    if name.has_prefix(".mem_") && name.has_suffix(".tmp") {
      count = count + 1
    }
  }
  count
}

// `ffi_ms_try_write` is declared per-target in
// memory_store_js.mbt / memory_store_native.mbt.