// src/transport/InMemoryTransport.ts

import type { ITransportAdapter, TransportHandler, IWireTopicInfo } from '../engine/remoting/ITransportAdapter';
import type { ITransportMessageMeta } from '../engine/remoting/ITransportMessageMeta';
import { InMemoryBroker } from './InMemoryBroker';

export interface InMemoryTransportOptions {
  queueMessages?: boolean;
  latencyMs?: number;
  name?: string;
  root?: string;
  /** @deprecated Use `root`. */
  realm?: string;
  enableTimingLogs?: boolean;
}

export class InMemoryTransport implements ITransportAdapter {
  private broker: InMemoryBroker;
  private queuedMessages = new Map<string, unknown[]>();
  private subscriptions = new Map<string, Set<TransportHandler>>();
  private brokerUnsubs = new Map<string, () => void>();  // Track broker unsubscribe functions
  private options: InMemoryTransportOptions;

  constructor(broker: InMemoryBroker, options: InMemoryTransportOptions = {}) {
    this.broker = broker;
    this.options = {
      queueMessages: false,
      latencyMs: 0,
      name: 'unnamed',
      ...options
    };
  }

  /**
   * Get the topic prefix (always empty for InMemoryTransport).
   */
  get topicPrefix(): string {
    return '';
  }

  get root(): string {
    return this.options.root ?? this.options.realm ?? '';
  }

  /** @deprecated Use `root`. */
  get realm(): string {
    return this.root;
  }

  getUnderlyingClient(): unknown {
    return this.broker;
  }

  async disconnect(): Promise<void> {
    for (const unsub of this.brokerUnsubs.values()) {
      try {
        unsub();
      } catch {
        // Ignore cleanup errors for in-memory transport teardown.
      }
    }
    this.brokerUnsubs.clear();
    this.subscriptions.clear();
    this.queuedMessages.clear();
  }

  async flush(): Promise<void> {}

  /**
   * Get wire topic info for debugging.
   * InMemoryTransport uses topics as-is (no prefix, no transformation).
   */
  getWireTopicInfo(topic: string): IWireTopicInfo {
    return {
      appTopic: topic,
      wireTopic: topic,  // InMemory: wire === app (no transformation)
      prefix: '',
      root: undefined,
      realm: undefined, // backward compat
      sessionId: undefined,
    };
  }

  subscribe(topic: string, handler: TransportHandler): () => void {
    let handlers = this.subscriptions.get(topic);
    if (!handlers) {
      handlers = new Set();
      this.subscriptions.set(topic, handlers);

      const brokerUnsub = this.broker.subscribe(topic, (payload: unknown) => {
        const delay = this.options.latencyMs ?? 0;

        const deliver = () => {
          const currentHandlers = this.subscriptions.get(topic);
          if (!currentHandlers) return;

          const meta: ITransportMessageMeta = { topic, ts: Date.now() };
          for (const h of currentHandlers) {
            try {
              const result = h(payload, meta);
              // If the handler returns a promise, attach a rejection handler
              // to prevent unhandled promise rejections while still isolating errors
              if (result && typeof result === 'object' && 'catch' in result) {
                (result as Promise<void>).catch((err) => {
                  // eslint-disable-next-line no-console -- kernel in-memory transport has no actor context for isolated async handler failures
                  console.error(`[InMemoryTransport:${this.options.name}] Async handler error for ${topic}:`, err);
                });
              }
            } catch (err) {
              // eslint-disable-next-line no-console -- kernel in-memory transport has no actor context for isolated handler failures
              console.error(`[InMemoryTransport:${this.options.name}] Handler error for ${topic}:`, err);
            }
          }
        };

        if (delay > 0) {
          setTimeout(deliver, delay);
        } else {
          // Synchronous delivery when no latency configured (for testing)
          deliver();
        }
      });

      // Store the broker unsubscribe function so we can clean up properly
      this.brokerUnsubs.set(topic, brokerUnsub);

      const queued = this.queuedMessages.get(topic);
      if (queued) {
        for (const payload of queued) {
          const meta: ITransportMessageMeta = { topic, ts: Date.now() };
          const delay = this.options.latencyMs ?? 0;
          if (delay > 0) {
            setTimeout(() => handler(payload, meta), delay);
          } else {
            // Synchronous delivery when no latency configured
            handler(payload, meta);
          }
        }
        this.queuedMessages.delete(topic);
      }
    }

    handlers.add(handler);

    return () => {
      const set = this.subscriptions.get(topic);
      if (set) {
        set.delete(handler);
        if (set.size === 0) {
          this.subscriptions.delete(topic);
          // Clean up the broker subscription to prevent memory leak
          const brokerUnsub = this.brokerUnsubs.get(topic);
          if (brokerUnsub) {
            brokerUnsub();
            this.brokerUnsubs.delete(topic);
          }
        }
      }
    };
  }

  publish(topic: string, payload?: unknown): void {
    const startTime = this.options.enableTimingLogs ? performance.now() : 0;
    const haveSubscribers = this.subscriptions.has(topic);

    if (this.options.queueMessages && !haveSubscribers) {
      const existing = this.queuedMessages.get(topic);
      if (existing) {
        existing.push(payload);
      } else {
        this.queuedMessages.set(topic, [payload]);
      }
      return;
    }

    const delay = this.options.latencyMs ?? 0;

    if (delay > 0) {
      setTimeout(() => {
        this.broker.publish(topic, payload);
      }, delay);
    } else {
      this.broker.publish(topic, payload);
    }

    if (this.options.enableTimingLogs) {
      const elapsed = performance.now() - startTime;
      // eslint-disable-next-line no-console -- kernel in-memory transport timing diagnostics are opt-in and have no actor context
      console.log(`[TRANSPORT-PUB] ${topic}: ${elapsed.toFixed(3)}ms`);
    }
  }
}
