///|
/// Helpers shared by every backend (FSEvents / inotify / RDCW / Node fs.watch).
/// Each backend's drain function pulls raw `(path, flag_bits_unused)` pairs
/// from its source and feeds them through these helpers to produce coalesced
/// Created / Modified / Removed events whose paths match the user-provided
/// roots.

///|
priv struct NormalizeState {
  exclude : (String) -> Bool
  // (canonical_prefix, original_root): backends emit OS-canonical paths
  // (e.g. macOS resolves /tmp → /private/tmp); we rewrite them back so
  // callers see the same prefixes they passed in.
  prefix_map : Array[(String, String)]
  // Paths known to be regular files at start (or seen in prior events).
  // Used to disambiguate "new file" from "existing file modified" — most
  // OS-level event flags conflate the two.
  known : Map[String, Bool]
}

///|
async fn build_normalize_state(
  roots : Array[String],
  exclude : (String) -> Bool,
) -> NormalizeState {
  let prefix_map : Array[(String, String)] = []
  for root in roots {
    let real = try @xfs.realpath(root) catch {
      err => Err(err)
    } noraise {
      value => Ok(value)
    }
    match real {
      Ok(r) => if r != root { prefix_map.push((r, root)) }
      Err(_) => ()
    }
  }
  let known : Map[String, Bool] = Map([])
  for root in roots {
    if !@xfs.exists(root) {
      continue
    }
    @xfs.walk(root, max_concurrency=4, exclude~, async fn(dir, names) {
      for name in names {
        let full = dir + "/" + name
        if exclude(full) {
          continue
        }
        let k = try @xfs.kind(full) catch {
          err => Err(err)
        } noraise {
          value => Ok(value)
        }
        if k is Ok(Regular) {
          known[full] = true
        }
      }
    })
  }
  NormalizeState::{ exclude, prefix_map, known }
}

///|
fn rewrite_path(path : String, prefix_map : Array[(String, String)]) -> String {
  for entry in prefix_map {
    let canonical = entry.0
    let original = entry.1
    if path == canonical {
      return original
    }
    if path.length() > canonical.length() &&
      path[:canonical.length()].to_owned() == canonical &&
      path[canonical.length()] == '/' {
      return original + path[canonical.length():].to_owned()
    }
  }
  path
}

///|
/// Classify a single raw event from a native backend. The caller passes the
/// raw (canonical) path; we rewrite it for the user-facing path. Returns
/// `None` to drop the event (excluded, or a phantom Remove for a path we
/// never tracked, or a non-regular-file Create).
async fn classify_event(state : NormalizeState, raw_path : String) -> Event? {
  let path = rewrite_path(raw_path, state.prefix_map)
  if (state.exclude)(path) {
    return None
  }
  let exists_now = @xfs.exists(raw_path)
  if !exists_now {
    if state.known.contains(path) {
      state.known.remove(path)
      return Some(Event::{ path, kind: Removed })
    }
    return None
  }
  let kind_ok = try @xfs.kind(raw_path) catch {
    _ => false
  } noraise {
    kind => kind is Regular
  }
  if !kind_ok {
    return None
  }
  let was_known = state.known.contains(path)
  state.known[path] = true
  let kind : EventKind = if was_known { Modified } else { Created }
  Some(Event::{ path, kind })
}