'use strict';
/**
 * Dispatch source registry — Item E of multi-agent deep-dive.
 *
 * Borrowed from OpenClaw's `matchedBy` enum on ResolvedAgentRoute
 * (resolve-route.ts:56-67). OpenClaw maps `(channel, peer, guild, role)`
 * → `agentId` and records WHICH binding matched in `matchedBy`. Wall-E's
 * adaptation, scaled to single-tenant single-agent: every dispatch
 * carries a `dispatch_source` enum value so operators can answer "what
 * woke this loop, and why?" without reading source.
 *
 * Source kinds:
 *   'channel'  — inbound message from external chat surface (slack_dm, imessage, telegram)
 *   'internal' — inbound from CTM tab or in-process trigger (chat, scheduled, boot, restart_catchup)
 *   'external' — HTTP-driven (webhook, manual_api)
 *
 * Each source descriptor exposes:
 *   { id, kind, label, default_collapse_policy_hint? }
 *
 * The registry is intentionally simple — just metadata about the
 * source, not a router. Routing remains data-driven:
 *   - Channels emit on event-bus → scheduler.wake('initiative')
 *   - Webhooks dispatch via skill-fallback (Item H of skill-dispatch)
 *   - Scheduled cron runs runDueSkills (Item C of skill-dispatch)
 *
 * The registry's value is OBSERVABILITY: every dispatch site can call
 * `tagDispatch(sourceId, payload)` to attach the source to telemetry +
 * skill_executions.decision_reason. No more guessing why a loop ran.
 */

const _sources = new Map();

const VALID_KINDS = new Set(['channel', 'internal', 'external']);

function registerSource(descriptor) {
  if (!descriptor || typeof descriptor !== 'object') {
    throw new Error('registerSource requires an object');
  }
  if (!descriptor.id || typeof descriptor.id !== 'string') {
    throw new Error('source.id must be a non-empty string');
  }
  if (!VALID_KINDS.has(descriptor.kind)) {
    throw new Error(`source.kind must be one of: ${[...VALID_KINDS].join(', ')}`);
  }
  const previous = _sources.get(descriptor.id);
  _sources.set(descriptor.id, descriptor);
  return () => {
    if (previous) _sources.set(descriptor.id, previous);
    else _sources.delete(descriptor.id);
  };
}

function getSource(id) {
  return _sources.get(id) || null;
}

function listSources(filter = {}) {
  let all = Array.from(_sources.values());
  if (filter.kind) all = all.filter(s => s.kind === filter.kind);
  return all.sort((a, b) => a.id.localeCompare(b.id));
}

function clearSourceRegistry() {
  _sources.clear();
}

/**
 * Best-effort tag a dispatch with its source. Used by callers that
 * already have telemetry/skill_executions wiring.
 *
 * Returns the validated source id (falls back to 'unknown' if not
 * registered, never throws — observability shouldn't break dispatch).
 */
function tagDispatch(sourceId) {
  if (!sourceId) return 'unknown';
  if (_sources.has(sourceId)) return sourceId;
  return 'unknown';
}

// ── Bundled sources ──
// Match the inbound surfaces documented in architecture.md.

registerSource({
  id: 'chat',
  kind: 'internal',
  label: 'CTM Wall-E tab (chat.js)',
  default_collapse_policy_hint: 'main',
});

registerSource({
  id: 'webhook',
  kind: 'external',
  label: 'External webhook (POST /api/wall-e/webhook)',
  default_collapse_policy_hint: 'per-source',
});

registerSource({
  id: 'manual_api',
  kind: 'external',
  label: 'Manual API trigger (POST /api/wall-e/skills/:id/run)',
  default_collapse_policy_hint: 'main',
});

registerSource({
  id: 'scheduled',
  kind: 'internal',
  label: 'Scheduler tick (runDueSkills)',
  default_collapse_policy_hint: 'main',
});

registerSource({
  id: 'boot',
  kind: 'internal',
  label: 'Daemon startup (agent.js main)',
  default_collapse_policy_hint: 'main',
});

registerSource({
  id: 'restart_catchup',
  kind: 'internal',
  label: 'Scheduler missed-job catchup (runMissedJobs)',
  default_collapse_policy_hint: 'main',
});

// Channel sources — match channels/*.js
registerSource({
  id: 'slack_dm',
  kind: 'channel',
  label: 'Slack DM (channels/slack-channel.js)',
  default_collapse_policy_hint: 'per-source',
});

registerSource({
  id: 'imessage',
  kind: 'channel',
  label: 'macOS iMessage (channels/imessage-channel.js)',
  default_collapse_policy_hint: 'per-source',
});

registerSource({
  id: 'telegram',
  kind: 'channel',
  label: 'Telegram bot (channels/telegram-channel.js)',
  default_collapse_policy_hint: 'per-source',
});

module.exports = {
  registerSource,
  getSource,
  listSources,
  clearSourceRegistry,
  tagDispatch,
  VALID_KINDS,
};
