// In-memory pub/sub for cross-component live activity.
// Used so the dashboard can see what background agents are doing in real time —
// e.g. when the Main Agent spawns a sub-agent and it starts running its own tools.
//
// Event shape (loose by design):
//   { type: 'agent_run_start' | 'agent_run_complete' | 'agent_tool_start' | 'agent_tool_result' | 'log',
//     agent_id?: string, agent_name?: string, tool?: string, input?: any,
//     status?: string, isError?: boolean, message?: string, ts: number, id: string }

import { persistLiveEvent } from './delegation.js';

// Bounded LRU of event IDs published by THIS process. The cross-process bridge
// (lib/live-events-bridge.js) uses it to skip rows in activity_log that came
// from us, preventing double-delivery to local subscribers.
const PUBLISHED_LRU_SIZE = 1000;

function getBus() {
  if (!globalThis.__madarxLiveEventBus) {
    globalThis.__madarxLiveEventBus = {
      subscribers: new Set(),
      nextId: 1,
      publishedIds: new Set(),
    };
  }
  // Backfill publishedIds if the bus was created by an older instance of this
  // module that didn't initialize it.
  if (!globalThis.__madarxLiveEventBus.publishedIds) {
    globalThis.__madarxLiveEventBus.publishedIds = new Set();
  }
  return globalThis.__madarxLiveEventBus;
}

function rememberPublishedId(bus, id) {
  if (!id) return;
  bus.publishedIds.add(id);
  if (bus.publishedIds.size > PUBLISHED_LRU_SIZE) {
    // Set iterates in insertion order — drop the oldest.
    const oldest = bus.publishedIds.values().next().value;
    bus.publishedIds.delete(oldest);
  }
}

export function subscribeLiveEvents(handler) {
  const bus = getBus();
  bus.subscribers.add(handler);
  return () => {
    bus.subscribers.delete(handler);
  };
}

export function publishLiveEvent(event) {
  const bus = getBus();
  const enriched = {
    ts: event?.ts || Date.now(),
    id: event?.id || `evt-${bus.nextId++}-${Math.random().toString(36).slice(2, 8)}`,
    ...event,
  };
  persistLiveEvent(enriched);
  rememberPublishedId(bus, enriched.id);
  for (const handler of bus.subscribers) {
    try {
      handler(enriched);
    } catch {
      // Subscriber threw — drop it silently so one bad consumer can't kill the bus.
    }
  }
  return enriched;
}

export function subscriberCount() {
  return getBus().subscribers.size;
}

export function wasLocallyPublished(id) {
  if (!id) return false;
  return getBus().publishedIds.has(id);
}

// State-change signal for the dashboard. Replaces 5s /api/state polling: emit
// `state.<slice>.changed` whenever a slice mutates and the dashboard refetches.
// Slices match the keys returned by `getState()` that the dashboard cares about:
// agents, runs, artifacts, heartbeat, approvals, connectors, schedules, sessions.
const STATE_SLICES = new Set([
  'agents',
  'runs',
  'artifacts',
  'heartbeat',
  'approvals',
  'connectors',
  'schedules',
  'sessions',
  'local_actions',
  'local_processes',
  'hermes_jobs',
  'projects',
  'disk',
  'acl',
  'share_links',
  'processes',
]);

export function publishStateChanged(slice, meta) {
  if (!slice || !STATE_SLICES.has(slice)) return;
  // Boot-time seeds call appendAudit, which fans out here. Those rows mutate
  // state but the dashboard reads the post-seed snapshot from /api/state on
  // mount, so the SSE events would only add noise (and would break tests that
  // expect zero events on a quiet bus). `seeds.js` toggles this flag while it
  // runs.
  if (globalThis.__madarxSuppressStateEmits) return;
  publishLiveEvent({
    type: `state.${slice}.changed`,
    slice,
    ...(meta && typeof meta === 'object' ? { meta } : {}),
  });
}
