/**
 * Channel registry — manages adapter lifecycle and message routing.
 *
 * The ChannelRegistry holds all registered channel adapters and routes
 * inbound messages through a single MessageHandler. Each adapter's
 * onMessage callback is wired at registration time so that messages
 * flow through handleInbound → messageHandler → reply.
 */

import type {
  ChannelAdapter,
  InboundMessage,
  MessageHandler,
  OutboundMessage,
} from './types.js';

type PresenceStatus = 'online' | 'offline';

interface PresenceRecord {
  channel: string;
  senderId: string;
  senderName?: string;
  firstSeenAt: number;
  lastSeenAt: number;
  messageCount: number;
}

export interface PresenceEntry {
  channel: string;
  senderId: string;
  senderName?: string;
  firstSeenAt: number;
  lastSeenAt: number;
  messageCount: number;
  status: PresenceStatus;
}

interface PresenceQuery {
  channel?: string;
  status?: PresenceStatus;
  limit?: number;
}

interface ChannelRegistryOptions {
  offlineAfterMs?: number;
  maxPresenceEntries?: number;
}

export class ChannelRegistry {
  private adapters: Map<string, ChannelAdapter> = new Map();
  private messageHandler?: MessageHandler;
  private presence: Map<string, PresenceRecord> = new Map();
  private readonly offlineAfterMs: number;
  private readonly maxPresenceEntries: number;

  constructor(options?: ChannelRegistryOptions) {
    this.offlineAfterMs = options?.offlineAfterMs ?? 5 * 60 * 1000;
    this.maxPresenceEntries = options?.maxPresenceEntries ?? 10_000;
  }

  /** Register an adapter. Throws if name already registered. */
  register(adapter: ChannelAdapter): void {
    if (this.adapters.has(adapter.name)) {
      throw new Error(`Channel adapter '${adapter.name}' is already registered`);
    }

    // Wire the adapter's onMessage to route through our messageHandler
    adapter.onMessage((msg) => this.handleInbound(msg));
    this.adapters.set(adapter.name, adapter);
  }

  /** Unregister an adapter by name. Calls disconnect() if connected. */
  async unregister(name: string): Promise<void> {
    const adapter = this.adapters.get(name);
    if (!adapter) {return;}

    if (adapter.status === 'connected' || adapter.status === 'connecting') {
      await adapter.disconnect();
    }

    this.adapters.delete(name);
  }

  /** Get an adapter by name. */
  get(name: string): ChannelAdapter | undefined {
    return this.adapters.get(name);
  }

  /** List all registered adapters. */
  list(): ChannelAdapter[] {
    return Array.from(this.adapters.values());
  }

  /** Set the message handler that all adapters route inbound messages to. */
  setMessageHandler(handler: MessageHandler): void {
    this.messageHandler = handler;
  }

  /**
   * Return observed sender presence entries sorted by most recent activity.
   * Status is inferred from lastSeenAt with an inactivity threshold.
   */
  getPresence(query?: PresenceQuery): PresenceEntry[] {
    const now = Date.now();
    const limit = Math.max(1, query?.limit ?? 100);

    const entries = Array.from(this.presence.values())
      .filter((record) => !query?.channel || record.channel === query.channel)
      .map((record): PresenceEntry => {
        const status: PresenceStatus = (now - record.lastSeenAt) <= this.offlineAfterMs ? 'online' : 'offline';
        return { ...record, status };
      })
      .filter((entry) => !query?.status || entry.status === query.status)
      .sort((a, b) => b.lastSeenAt - a.lastSeenAt);

    return entries.slice(0, limit);
  }

  /** Aggregate quick presence stats for dashboards. */
  getPresenceSummary(): { total: number; online: number; offline: number } {
    const entries = this.getPresence({ limit: this.maxPresenceEntries });
    const online = entries.filter((entry) => entry.status === 'online').length;
    return {
      total: entries.length,
      online,
      offline: entries.length - online,
    };
  }

  /** Start all registered adapters. Logs errors per adapter, doesn't throw. */
  async startAll(): Promise<void> {
    const adapters = Array.from(this.adapters.values());
    const results = await Promise.allSettled(
      adapters.map((a) => a.connect()),
    );

    for (const [i, result] of results.entries()) {
      if (result.status === 'rejected') {
        console.error(
          `Failed to start channel '${adapters[i].name}':`,
          result.reason,
        );
      }
    }
  }

  /** Stop all registered adapters. */
  async stopAll(): Promise<void> {
    const adapters = Array.from(this.adapters.values());
    const results = await Promise.allSettled(
      adapters.map((a) => a.disconnect()),
    );

    for (const [i, result] of results.entries()) {
      if (result.status === 'rejected') {
        console.error(
          `Failed to stop channel '${adapters[i].name}':`,
          result.reason,
        );
      }
    }
  }

  /** Internal: route an inbound message to the message handler. */
  private handleInbound(msg: InboundMessage): void {
    this.recordPresence(msg);

    if (!this.messageHandler) {
      console.warn(`No message handler set, dropping message from '${msg.channel}'`);
      return;
    }

    const adapter = this.adapters.get(msg.channel);
    if (!adapter) {
      console.warn(`Unknown channel '${msg.channel}' in inbound message`);
      return;
    }

    // Create a reply function bound to this message's channel and sender
    const reply = async (response: OutboundMessage): Promise<void> => {
      const metadata = msg.metadata as Record<string, unknown> | undefined;
      const replyPeerId = typeof metadata?.replyPeerId === 'string' && metadata.replyPeerId.length > 0
        ? metadata.replyPeerId
        : msg.senderId;
      await adapter.send(replyPeerId, response);
    };

    // Fire and forget — errors are logged, not propagated
    this.messageHandler(msg, reply).catch((err: unknown) => {
      console.error(`Error handling message from '${msg.channel}':`, err);
    });
  }

  private recordPresence(msg: InboundMessage): void {
    const key = `${msg.channel}:${msg.senderId}`;
    const now = Date.now();
    const existing = this.presence.get(key);
    if (existing) {
      existing.lastSeenAt = now;
      existing.messageCount += 1;
      if (msg.senderName) {
        existing.senderName = msg.senderName;
      }
      return;
    }

    this.presence.set(key, {
      channel: msg.channel,
      senderId: msg.senderId,
      senderName: msg.senderName,
      firstSeenAt: now,
      lastSeenAt: now,
      messageCount: 1,
    });

    if (this.presence.size > this.maxPresenceEntries) {
      // Keep bounded memory by evicting least recently seen entry.
      let oldestKey: string | undefined;
      let oldestTs = Number.POSITIVE_INFINITY;
      for (const [candidateKey, record] of this.presence.entries()) {
        if (record.lastSeenAt < oldestTs) {
          oldestTs = record.lastSeenAt;
          oldestKey = candidateKey;
        }
      }
      if (oldestKey) {
        this.presence.delete(oldestKey);
      }
    }
  }
}
