// ── GlobWatcher ───────────────────────────────────────────────────────────────

priv struct ListenerEntry {
  event    : @chokidar.EventName
  callback : (@chokidar.Event) -> Unit
}

///|
pub struct GlobWatcher {
  priv chokidar_watcher : @chokidar.Watcher
  priv debouncer        : Debouncer
  priv queue            : CallbackQueue
  priv positives        : Array[GlobPattern]
  priv options          : Options
  priv listeners        : Array[ListenerEntry]
}

///| Create a watcher for the given glob patterns.
/// Raises GlobError if patterns are invalid, SetupError if chokidar fails.
pub fn GlobWatcher::new(
  glob    : @chokidar.WatchPaths,
  options : Options,
) -> GlobWatcher raise GlobWatcherError {
  let raw_patterns = match glob {
    One(p)   => [p]
    Many(ps) => ps.copy()
  }

  let (positives, final_negations) = parse_globs(raw_patterns)

  if positives.is_empty() {
    raise GlobError("no positive glob patterns provided")
  }

  // Build ignored list: user-supplied + negation globs
  let negation_matchers : Array[@chokidar.Matcher] = final_negations.map(fn(g) {
    @chokidar.Matcher::MatchFunction(fn(path, _) { glob_match(g.raw, path) })
  })
  let all_ignored = options.ignored + negation_matchers

  let chokidar_opts = @chokidar.Options::{
    ..@chokidar.Options::default(),
    ignore_initial: options.ignore_initial,
    ignored:        all_ignored,
    cwd:            options.cwd,
    depth:          options.depth,
    persistent:     false,
    use_polling:    options.use_polling,
  }

  // Deduplicate base dirs
  let seen : Array[String] = []
  let base_dirs : Array[String] = []
  for g in positives {
    if !seen.contains(g.base_dir) {
      seen.push(g.base_dir)
      base_dirs.push(g.base_dir)
    }
  }

  // Resolve base_dirs against cwd so the native scanner operates in the right
  // directory (chokidar's cwd option only affects path formatting, not where
  // readdirp actually scans from).
  let resolved_dirs = match options.cwd {
    None => base_dirs
    Some(cwd) =>
      base_dirs.map(fn(d) { if d == "." { cwd } else { cwd + "/" + d } })
  }

  let watch_paths = if resolved_dirs.length() == 1 {
    @chokidar.WatchPaths::One(resolved_dirs[0])
  } else {
    @chokidar.WatchPaths::Many(resolved_dirs)
  }

  let chokidar_watcher = @chokidar.watch(watch_paths, options=chokidar_opts) catch {
    @chokidar.ChokidarError(msg) => raise SetupError(msg)
  }

  {
    chokidar_watcher,
    debouncer: Debouncer::new(options.delay),
    queue:     CallbackQueue::new(options.queue),
    positives,
    options,
    listeners: [],
  }
}

///| Register a listener for a chokidar event (Ready, Error, Raw, etc.).
/// Returns self for chaining.
pub fn GlobWatcher::on(
  self     : GlobWatcher,
  event    : @chokidar.EventName,
  callback : (@chokidar.Event) -> Unit,
) -> GlobWatcher {
  self.listeners.push({ event, callback })
  self
}

///| Drive one process + debounce + queue cycle (non-blocking).
/// Used by tests and custom loop implementations.
pub fn GlobWatcher::tick(
  self : GlobWatcher,
  cb?  : (@chokidar.Event) -> Unit raise Error,
) -> Unit raise GlobWatcherError {
  let file_events   : Array[@chokidar.Event] = []
  let direct_events : Array[@chokidar.Event] = []

  // Collect events from this process() call via a temporary listener.
  let handler : (@chokidar.Event) -> Unit = fn(event) {
    match event.name {
      @chokidar.EventName::Ready
      | @chokidar.EventName::Error
      | @chokidar.EventName::Raw => direct_events.push(event)
      _ =>
        if self.options.events.contains(event.name) &&
           self.positives.any(fn(p) { glob_match(p.raw, event.path) }) {
          file_events.push(event)
        }
    }
  }

  // Register for All (file events) AND explicitly for Ready/Error/Raw since
  // the chokidar emitter does not forward those to All listeners.
  self.chokidar_watcher.on(@chokidar.EventName::All, handler) |> ignore
  self.chokidar_watcher.on(@chokidar.EventName::Ready, handler) |> ignore
  self.chokidar_watcher.on(@chokidar.EventName::Error, handler) |> ignore
  self.chokidar_watcher.on(@chokidar.EventName::Raw, handler) |> ignore

  let process_err = try {
    self.chokidar_watcher.process()
    None
  } catch {
    @chokidar.ChokidarError(msg) => Some(msg)
  }

  self.chokidar_watcher.off(@chokidar.EventName::All, handler) |> ignore
  self.chokidar_watcher.off(@chokidar.EventName::Ready, handler) |> ignore
  self.chokidar_watcher.off(@chokidar.EventName::Error, handler) |> ignore
  self.chokidar_watcher.off(@chokidar.EventName::Raw, handler) |> ignore

  // Forward Ready / Error / Raw to registered listeners.
  for event in direct_events {
    emit_to_listeners(self.listeners, event)
  }

  // Surface process errors as Error events.
  match process_err {
    Some(msg) =>
      emit_to_listeners(
        self.listeners,
        {
          name:  @chokidar.EventName::Error,
          path:  "",
          stats: None,
          raw:   None,
          error: Some(@chokidar.ChokidarError(msg)),
        },
      )
    None => ()
  }

  // Feed file events into the debouncer.
  let now = @env.now()
  for event in file_events {
    self.debouncer.feed(event, now)
  }

  // Flush and run callback when debounce window has elapsed.
  let now2 = @env.now()
  match self.debouncer.flush(now2) {
    Some(event) =>
      match cb {
        Some(callback) =>
          self.queue.try_run(event, callback, fn(msg) {
            emit_to_listeners(
              self.listeners,
              {
                name:  @chokidar.EventName::Error,
                path:  "",
                stats: None,
                raw:   None,
                error: Some(@chokidar.ChokidarError(msg)),
              },
            )
          })
        None => ()
      }
    None => ()
  }
}

///| Block in a polling loop until SIGINT/SIGTERM terminates the process.
pub fn GlobWatcher::run(
  self : GlobWatcher,
  cb?  : (@chokidar.Event) -> Unit raise Error,
) -> Unit raise GlobWatcherError {
  while true {
    self.tick(cb?)
    gw_sleep_ms(self.options.loop_interval)
  }
}

///| Stop watching and release resources.
pub fn GlobWatcher::close(self : GlobWatcher) -> Unit {
  self.chokidar_watcher.close()
  self.listeners.clear()
}

///| Return a snapshot of the currently watched paths.
pub fn GlobWatcher::get_watched(
  self : GlobWatcher,
) -> Map[String, Array[String]] {
  self.chokidar_watcher.get_watched()
}

// ── High-level entry point ────────────────────────────────────────────────────

///| Watch glob patterns and invoke `cb` on each debounced change.
/// Blocks until SIGINT/SIGTERM.
pub fn watch(
  glob    : @chokidar.WatchPaths,
  options : Options,
  cb      : (@chokidar.Event) -> Unit raise Error,
) -> Unit raise GlobWatcherError {
  let w = GlobWatcher::new(glob, options)
  w.run(cb=cb)
}

// ── Helpers ───────────────────────────────────────────────────────────────────

fn emit_to_listeners(
  listeners : Array[ListenerEntry],
  event     : @chokidar.Event,
) -> Unit {
  for entry in listeners {
    if entry.event == event.name || entry.event == @chokidar.EventName::All {
      (entry.callback)(event)
    }
  }
}

extern "C" fn gw_sleep_ms(ms : Int) -> Unit = "globwatcher_sleep_ms"