///|
/// Identity of a file at the moment it was read.
///
/// The read/write/edit tools record a stamp at read time and compare it
/// against a fresh stat of the same path before modifying it, so a write
/// never silently clobbers content that changed after the model last saw it.
/// `mtime_s`/`mtime_ns` are the (seconds, nanoseconds) pair surfaced by
/// `@fs.mtime` on native and by `statSync` on js; `size` is the file length
/// in bytes.
pub(all) struct FileStamp {
  mtime_s : Int64
  mtime_ns : Int
  size : Int64
} derive(Eq, Debug)

///|
pub extend FileStamp with Eq::{not_equal, equal}

///|
pub extend FileStamp with @moonbitlang/core/debug.Debug::{to_repr}

///|
/// Outcome of checking a modification target against a `FreshnessGuard`.
pub(all) enum FreshnessVerdict {
  Fresh
  NeverRead
  Modified
} derive(Eq, Debug)

///|
pub extend FreshnessVerdict with Eq::{not_equal, equal}

///|
pub extend FreshnessVerdict with @moonbitlang/core/debug.Debug::{to_repr}

///|
/// Read-before-modify ledger shared by the read/write/edit tool extensions.
///
/// One guard instance per composed Agent: the host creates it and injects it
/// into all three tools, so a `read` in one tool is visible to `write`/`edit`
/// in the others. The guard itself touches no IO — callers stat files and
/// pass stamps in. Its granularity is the Agent lifetime, matching a
/// permission gate's session cache.
pub struct FreshnessGuard {
  records : Map[String, FileStamp]
}

///|
pub fn FreshnessGuard::FreshnessGuard() -> FreshnessGuard {
  { records: {}, }
}

///|
/// Record that `path` was successfully read with identity `stamp`.
pub fn FreshnessGuard::note_read(
  self : FreshnessGuard,
  path : String,
  stamp : FileStamp,
) -> Unit {
  self.records[path] = stamp
}

///|
/// Drop any recorded read for `path` (e.g. after the file was rewritten).
pub fn FreshnessGuard::forget(self : FreshnessGuard, path : String) -> Unit {
  ignore(self.records.remove(path))
}

///|
/// Classify a modification target against the guard.
///
/// `current` is a stat of the file as it exists now. `None` means the path
/// does not exist, in which case creating it is always allowed — a new file
/// cannot have been clobbered.
pub fn FreshnessGuard::check(
  self : FreshnessGuard,
  path : String,
  current : FileStamp?,
) -> FreshnessVerdict {
  match current {
    None => Fresh
    Some(now) =>
      if self.records.contains(path) {
        if self.records[path] == now {
          Fresh
        } else {
          Modified
        }
      } else {
        NeverRead
      }
  }
}

///|
/// Plain-language hint for a non-fresh verdict, for tool error messages.
#as_free_fn(freshness_hint, visibility="pub", deprecated="use `FreshnessVerdict::hint` instead")
pub fn FreshnessVerdict::hint(self : FreshnessVerdict) -> String {
  match self {
    Fresh => "file is current"
    NeverRead =>
      "file exists but has not been read in this session; read it first"
    Modified => "file changed since it was last read; read it again"
  }
}