/**
 * PluginEventBus — typed in-process event bus for the Paperclip plugin system.
 *
 * Responsibilities:
 * - Deliver core domain events to subscribing plugin workers (server-side).
 * - Apply `EventFilter` server-side so filtered-out events never reach the handler.
 * - Namespace plugin-emitted events as `plugin.<pluginId>.<eventName>`.
 * - Guard the core namespace: plugins may not emit events with the `plugin.` prefix.
 * - Isolate subscriptions per plugin — a plugin cannot enumerate or interfere with
 *   another plugin's subscriptions.
 * - Support wildcard subscriptions via prefix matching (e.g. `plugin.acme.linear.*`).
 *
 * The bus operates in-process. In the full out-of-process architecture the host
 * calls `bus.emit()` after receiving events from the DB/queue layer, and the bus
 * forwards to handlers that proxy the call to the relevant worker process via IPC.
 * That IPC layer is separate; this module only handles routing and filtering.
 *
 * @see PLUGIN_SPEC.md §16 — Event System
 * @see PLUGIN_SPEC.md §16.1 — Event Filtering
 * @see PLUGIN_SPEC.md §16.2 — Plugin-to-Plugin Events
 */

import type { PluginEventType } from "@paperclipai/shared";
import type { PluginEvent, EventFilter } from "@paperclipai/plugin-sdk";

// ---------------------------------------------------------------------------
// Internal types
// ---------------------------------------------------------------------------

/**
 * A registered subscription record stored per plugin.
 */
interface Subscription {
  /** The event name or prefix pattern this subscription matches. */
  eventPattern: string;
  /** Optional server-side filter applied before delivery. */
  filter: EventFilter | null;
  /** Async handler to invoke when a matching event passes the filter. */
  handler: (event: PluginEvent) => Promise<void>;
}

// ---------------------------------------------------------------------------
// Pattern matching helpers
// ---------------------------------------------------------------------------

/**
 * Returns true if the event type matches the subscription pattern.
 *
 * Matching rules:
 * - Exact match: `"issue.created"` matches `"issue.created"`.
 * - Wildcard suffix: `"plugin.acme.*"` matches any event type that starts with
 *   `"plugin.acme."`. The wildcard `*` is only supported as a trailing token.
 *
 * No full glob syntax is supported — only trailing `*` after a `.` separator.
 */
function matchesPattern(eventType: string, pattern: string): boolean {
  if (pattern === eventType) return true;

  // Trailing wildcard: "plugin.foo.*" → prefix is "plugin.foo."
  if (pattern.endsWith(".*")) {
    const prefix = pattern.slice(0, -1); // remove the trailing "*", keep the "."
    return eventType.startsWith(prefix);
  }

  return false;
}

/**
 * Returns true if the event passes all fields of the filter.
 * A `null` or empty filter object passes all events.
 *
 * **Resolution strategy per field:**
 *
 * - `projectId` — checked against `event.entityId` when `entityType === "project"`,
 *   otherwise against `payload.projectId`. This covers both direct project events
 *   (e.g. `project.created`) and secondary events that embed a project reference in
 *   their payload (e.g. `issue.created` with `payload.projectId`).
 *
 * - `companyId` — always resolved from `payload.companyId`. Core domain events that
 *   belong to a company embed the company ID in their payload.
 *
 * - `agentId` — checked against `event.entityId` when `entityType === "agent"`,
 *   otherwise against `payload.agentId`. Covers both direct agent lifecycle events
 *   (e.g. `agent.created`) and run-level events with `payload.agentId` (e.g.
 *   `agent.run.started`).
 *
 * Multiple filter fields are ANDed — all specified fields must match.
 */
function passesFilter(event: PluginEvent, filter: EventFilter | null): boolean {
  if (!filter) return true;

  const payload = event.payload as Record<string, unknown> | null;

  if (filter.projectId !== undefined) {
    const projectId = event.entityType === "project"
      ? event.entityId
      : (typeof payload?.projectId === "string" ? payload.projectId : undefined);
    if (projectId !== filter.projectId) return false;
  }

  if (filter.companyId !== undefined) {
    if (event.companyId !== filter.companyId) return false;
  }

  if (filter.agentId !== undefined) {
    const agentId = event.entityType === "agent"
      ? event.entityId
      : (typeof payload?.agentId === "string" ? payload.agentId : undefined);
    if (agentId !== filter.agentId) return false;
  }

  return true;
}

// ---------------------------------------------------------------------------
// Event bus factory
// ---------------------------------------------------------------------------

/**
 * Creates and returns a new `PluginEventBus` instance.
 *
 * A single bus instance should be shared across the server process. Each
 * plugin interacts with the bus through a scoped handle obtained via
 * {@link PluginEventBus.forPlugin}.
 *
 * @example
 * ```ts
 * const bus = createPluginEventBus();
 *
 * // Give the Linear plugin a scoped handle
 * const linearBus = bus.forPlugin("acme.linear");
 *
 * // Subscribe from the plugin's perspective
 * linearBus.subscribe("issue.created", async (event) => {
 *   // handle event
 * });
 *
 * // Emit a core domain event (called by the host, not the plugin)
 * await bus.emit({
 *   eventId: "evt-1",
 *   eventType: "issue.created",
 *   occurredAt: new Date().toISOString(),
 *   entityId: "iss-1",
 *   entityType: "issue",
 *   payload: { title: "Fix login bug", projectId: "proj-1" },
 * });
 * ```
 */
export function createPluginEventBus(): PluginEventBus {
  // Subscription registry: pluginKey → list of subscriptions
  const registry = new Map<string, Subscription[]>();

  /**
   * Retrieve or create the subscription list for a plugin.
   */
  function subsFor(pluginId: string): Subscription[] {
    let subs = registry.get(pluginId);
    if (!subs) {
      subs = [];
      registry.set(pluginId, subs);
    }
    return subs;
  }

  /**
   * Emit an event envelope to all matching subscribers across all plugins.
   *
   * Subscribers are called concurrently (Promise.all). Each handler's errors
   * are caught individually and collected in the returned `errors` array so a
   * single misbehaving plugin cannot interrupt delivery to other plugins.
   */
  async function emit(event: PluginEvent): Promise<PluginEventBusEmitResult> {
    const errors: Array<{ pluginId: string; error: unknown }> = [];
    const promises: Promise<void>[] = [];

    for (const [pluginId, subs] of registry) {
      for (const sub of subs) {
        if (!matchesPattern(event.eventType, sub.eventPattern)) continue;
        if (!passesFilter(event, sub.filter)) continue;

        // Use Promise.resolve().then() so that synchronous throws from handlers
        // are also caught inside the promise chain. Calling
        // Promise.resolve(syncThrowingFn()) does NOT catch sync throws — the
        // throw escapes before Promise.resolve() can wrap it. Using .then()
        // ensures the call is deferred into the microtask queue where all
        // exceptions become rejections. Each .catch() swallows the rejection
        // and records it — the promise always resolves, so Promise.all never rejects.
        promises.push(
          Promise.resolve().then(() => sub.handler(event)).catch((error: unknown) => {
            errors.push({ pluginId, error });
          }),
        );
      }
    }

    await Promise.all(promises);
    return { errors };
  }

  /**
   * Remove all subscriptions for a plugin (e.g. on worker shutdown or uninstall).
   */
  function clearPlugin(pluginId: string): void {
    registry.delete(pluginId);
  }

  /**
   * Return a scoped handle for a specific plugin. The handle exposes only the
   * plugin's own subscription list and enforces the plugin namespace on `emit`.
   */
  function forPlugin(pluginId: string): ScopedPluginEventBus {
    return {
      /**
       * Subscribe to a core domain event or a plugin-namespaced event.
       *
       * For wildcard subscriptions use a trailing `.*` pattern, e.g.
       * `"plugin.acme.linear.*"`.
       *
       * Requires the `events.subscribe` capability (capability enforcement is
       * done by the host layer before calling this method).
       */
      subscribe(
        eventPattern: PluginEventType | `plugin.${string}`,
        fnOrFilter: EventFilter | ((event: PluginEvent) => Promise<void>),
        maybeFn?: (event: PluginEvent) => Promise<void>,
      ): void {
        let filter: EventFilter | null = null;
        let handler: (event: PluginEvent) => Promise<void>;

        if (typeof fnOrFilter === "function") {
          handler = fnOrFilter;
        } else {
          filter = fnOrFilter;
          if (!maybeFn) throw new Error("Handler function is required when a filter is provided");
          handler = maybeFn;
        }

        subsFor(pluginId).push({ eventPattern, filter, handler });
      },

      /**
       * Emit a plugin-namespaced event. The event type is automatically
       * prefixed with `plugin.<pluginId>.` so:
       * - `emit("sync-done", payload)` becomes `"plugin.acme.linear.sync-done"`.
       *
       * Requires the `events.emit` capability (enforced by the host layer).
       *
       * @throws {Error} if `name` already contains the `plugin.` prefix
       *   (prevents cross-namespace spoofing).
       */
      async emit(name: string, companyId: string, payload: unknown): Promise<PluginEventBusEmitResult> {
        if (!name || name.trim() === "") {
          throw new Error(`Plugin "${pluginId}" must provide a non-empty event name.`);
        }

        if (!companyId || companyId.trim() === "") {
          throw new Error(`Plugin "${pluginId}" must provide a companyId when emitting events.`);
        }

        if (name.startsWith("plugin.")) {
          throw new Error(
            `Plugin "${pluginId}" must not include the "plugin." prefix when emitting events. ` +
            `Emit the bare event name (e.g. "sync-done") and the bus will namespace it automatically.`,
          );
        }

        const eventType = `plugin.${pluginId}.${name}` as const;
        const event: PluginEvent = {
          eventId: crypto.randomUUID(),
          eventType,
          companyId,
          occurredAt: new Date().toISOString(),
          actorType: "plugin",
          actorId: pluginId,
          payload,
        };

        return emit(event);
      },

      /** Remove all subscriptions registered by this plugin. */
      clear(): void {
        clearPlugin(pluginId);
      },
    };
  }

  return {
    emit,
    forPlugin,
    clearPlugin,
    /** Expose subscription count for a plugin (useful for tests and diagnostics). */
    subscriptionCount(pluginId?: string): number {
      if (pluginId !== undefined) {
        return registry.get(pluginId)?.length ?? 0;
      }
      let total = 0;
      for (const subs of registry.values()) total += subs.length;
      return total;
    },
  };
}

// ---------------------------------------------------------------------------
// Public types
// ---------------------------------------------------------------------------

/**
 * Result returned from `emit()`. Handler errors are collected and returned
 * rather than thrown so a single misbehaving plugin cannot block delivery to
 * other plugins.
 */
export interface PluginEventBusEmitResult {
  /** Errors thrown by individual handlers, keyed by the plugin that failed. */
  errors: Array<{ pluginId: string; error: unknown }>;
}

/**
 * The full event bus — held by the host process.
 *
 * Call `forPlugin(id)` to obtain a `ScopedPluginEventBus` for each plugin worker.
 */
export interface PluginEventBus {
  /**
   * Emit a typed domain event to all matching subscribers.
   *
   * Called by the host when a domain event occurs (e.g. from the DB layer or
   * message queue). All registered subscriptions across all plugins are checked.
   */
  emit(event: PluginEvent): Promise<PluginEventBusEmitResult>;

  /**
   * Get a scoped handle for a specific plugin worker.
   *
   * The scoped handle isolates the plugin's subscriptions and enforces the
   * plugin namespace on outbound events.
   */
  forPlugin(pluginId: string): ScopedPluginEventBus;

  /**
   * Remove all subscriptions for a plugin (called on worker shutdown/uninstall).
   */
  clearPlugin(pluginId: string): void;

  /**
   * Return the total number of active subscriptions, or the count for a
   * specific plugin if `pluginId` is provided.
   */
  subscriptionCount(pluginId?: string): number;
}

/**
 * A plugin-scoped view of the event bus. Handed to the plugin worker (or its
 * host-side proxy) during initialisation.
 *
 * Plugins use this to:
 * 1. Subscribe to domain events (with optional server-side filter).
 * 2. Emit plugin-namespaced events for other plugins to consume.
 *
 * Note: `subscribe` overloads mirror the `PluginEventsClient.on()` interface
 * from the SDK. `emit` intentionally returns `PluginEventBusEmitResult` rather
 * than `void` so the host layer can inspect handler errors; the SDK-facing
 * `PluginEventsClient.emit()` wraps this and returns `void`.
 */
export interface ScopedPluginEventBus {
  /**
   * Subscribe to a core domain event or a plugin-namespaced event.
   *
   * **Pattern syntax:**
   * - Exact match: `"issue.created"` — receives only that event type.
   * - Wildcard suffix: `"plugin.acme.linear.*"` — receives all events emitted by
   *   the `acme.linear` plugin. The `*` is supported only as a trailing token after
   *   a `.` separator; no other glob syntax is supported.
   * - Top-level plugin wildcard: `"plugin.*"` — receives all plugin-emitted events
   *   regardless of which plugin emitted them.
   *
   * Wildcards apply only to the `plugin.*` namespace. Core domain events must be
   * subscribed to by exact name (e.g. `"issue.created"`, not `"issue.*"`).
   *
   * An optional `EventFilter` can be passed as the second argument to perform
   * server-side pre-filtering; filtered-out events are never delivered to the handler.
   */
  subscribe(
    eventPattern: PluginEventType | `plugin.${string}`,
    fn: (event: PluginEvent) => Promise<void>,
  ): void;
  subscribe(
    eventPattern: PluginEventType | `plugin.${string}`,
    filter: EventFilter,
    fn: (event: PluginEvent) => Promise<void>,
  ): void;

  /**
   * Emit a plugin-namespaced event. The bus automatically prepends
   * `plugin.<pluginId>.` to the `name`, so passing `"sync-done"` from plugin
   * `"acme.linear"` produces the event type `"plugin.acme.linear.sync-done"`.
   *
   * @param name  Bare event name (e.g. `"sync-done"`). Must be non-empty and
   *   must not include the `plugin.` prefix — the bus adds that automatically.
   * @param companyId  UUID of the company this event belongs to.
   * @param payload  Arbitrary JSON-serializable data to attach to the event.
   *
   * @throws {Error} if `name` is empty or whitespace-only.
   * @throws {Error} if `name` starts with `"plugin."` (namespace spoofing guard).
   */
  emit(name: string, companyId: string, payload: unknown): Promise<PluginEventBusEmitResult>;

  /**
   * Remove all subscriptions registered by this plugin.
   */
  clear(): void;
}
