/**
 * TaskRefreshCoordinator — one place that maps "a task finished" to "what the
 * UI should reload" so it reflects the result without a manual refresh.
 *
 * Why this exists: refresh-on-completion used to be scattered — every page
 * added its own `window.addEventListener('taskCompleted', …)` and hard-coded
 * which `action` strings it cared about. That made it easy to add a task and
 * forget the refresh (stale UI), with no single place to see the wiring.
 *
 * Now components REGISTER a refresh entry here; the coordinator owns the single
 * listener, the dedupe (the SSE bus + synthetic fallbacks can double-fire the
 * same task), and the debounce (bursts of tasks coalesce into one refresh).
 * `window.taskRefresh.table()` is the introspectable "what reloads when" map.
 *
 * An entry is DATA-REFRESH only. Interaction side-effects (button enable/
 * disable, nav locks, welcome modals) stay in their components — they are not
 * "make the data current" concerns.
 *
 *   window.taskRefresh.register({
 *     id: 'backups',                       // unique; re-registering replaces
 *     actions: ['backup', 'restore'],      // match on task type, AND/OR…
 *     commands: /^libreportal\s+backup\b/,  // …match on the command string, OR…
 *     match: (d) => d.status === 'completed',// …a custom predicate
 *     run: async (detail) => { … },        // do the refresh
 *     debounceMs: 300,
 *   });
 *
 * The detail object is the `taskCompleted` event detail:
 *   { taskId, appName, action, status, task, timestamp }
 */
class TaskRefreshCoordinator {
  constructor() {
    this.entries = [];
    this._recent = new Map();    // `${taskId}::${entryId}` -> ts, for dedupe
    this._timers = new Map();    // entryId -> debounce timer
    this._listening = false;
  }

  _ensureListening() {
    if (this._listening) return;
    this._listening = true;
    const onEvent = (e) => this._dispatch(e && e.detail);
    window.addEventListener('taskCompleted', onEvent);
    // Safety net: a terminal status that the bus classified as "updated"
    // (not a clean transition to completed) should still trigger refreshes.
    window.addEventListener('taskUpdated', (e) => {
      const st = e && e.detail && e.detail.task && e.detail.task.status;
      if (st === 'completed' || st === 'failed' || st === 'cancelled') onEvent(e);
    });
  }

  register(spec) {
    if (!spec || !spec.id || typeof spec.run !== 'function') {
      console.warn('taskRefresh.register: ignoring invalid entry', spec);
      return;
    }
    // Idempotent: re-registering the same id (e.g. a page re-mounts) replaces.
    this.entries = this.entries.filter((e) => e.id !== spec.id);
    this.entries.push(spec);
    this._ensureListening();
    return spec;
  }

  unregister(id) {
    this.entries = this.entries.filter((e) => e.id !== id);
  }

  // Introspectable map of what reloads on which tasks.
  table() {
    return this.entries.map((e) => ({
      id: e.id,
      actions: e.actions || null,
      commands: e.commands ? String(e.commands) : null,
      custom: !!e.match,
    }));
  }

  _matches(entry, d) {
    if (entry.match) { try { if (!entry.match(d)) return false; } catch { return false; } }
    // If neither actions nor commands is set, a passing `match` (or no match)
    // means "all tasks".
    if (!entry.actions && !entry.commands) return true;
    if (entry.actions && d.action && entry.actions.includes(d.action)) return true;
    if (entry.commands && entry.commands.test((d.task && d.task.command) || '')) return true;
    return false;
  }

  _dispatch(detail) {
    if (!detail) return;
    const taskId = detail.taskId || detail.id || '?';
    const now = Date.now();
    for (const entry of this.entries) {
      if (!this._matches(entry, detail)) continue;
      // Dedupe the same task+entry firing twice in quick succession.
      const key = `${taskId}::${entry.id}`;
      if (now - (this._recent.get(key) || 0) < 4000) continue;
      this._recent.set(key, now);
      this._runDebounced(entry, detail);
    }
    if (this._recent.size > 256) this._recent.clear();
  }

  _runDebounced(entry, detail) {
    const run = async () => {
      try { await entry.run(detail); }
      catch (err) { console.error(`taskRefresh[${entry.id}] failed:`, err); }
    };
    const ms = Number.isFinite(entry.debounceMs) ? entry.debounceMs : 0;
    // No debounce → run per task. A debounce coalesces a burst into one call,
    // so only use it for detail-agnostic data reloads, never for entries with
    // per-task side-effects (the timer is per-entry and would drop all but the
    // last task's invocation).
    if (ms <= 0) { Promise.resolve().then(run); return; }
    clearTimeout(this._timers.get(entry.id));
    this._timers.set(entry.id, setTimeout(() => { this._timers.delete(entry.id); run(); }, ms));
  }
}

window.taskRefresh = window.taskRefresh || new TaskRefreshCoordinator();
window.TaskRefreshCoordinator = TaskRefreshCoordinator;
