/**
 * V4.7 — bridge-connection presence tracking.
 *
 * The bridge handlers (`/v1/bridge/stream` and `/v1/bridge/ws`) call
 * `markConnected` on open and `markDisconnected` on close. The tracker
 * keeps a per-address set of connection ids; the `inbox.peer_connected`
 * / `inbox.peer_disconnected` events fire only on the 0↔1 boundary so
 * that two simultaneous bridges (e.g. SSE + WS during a transport-
 * fallback handover) collapse into a single connected/disconnected
 * pair from the consumer's point of view.
 *
 * Long-poll (`/v1/bridge/poll`) is intentionally NOT tracked — see the
 * note on `InboxServerEventMap` in `events.ts`.
 */

import type { InboxServerEvents } from './events.js';

export type TrackedBridgeKind = 'ws' | 'sse';

export class PresenceTracker {
  private readonly connections = new Map<string, Set<string>>();
  private nextConnId = 1;

  constructor(private readonly events: InboxServerEvents | null) {}

  /** Allocate a fresh connection id for `markConnected` / `markDisconnected`. */
  newConnectionId(): string {
    return `c${this.nextConnId++}`;
  }

  /**
   * Snapshot: is `address` currently connected over any tracked transport?
   * Used by `/v1/bridge/presence` to push the initial state to a new
   * subscriber.
   */
  isOnline(address: string): boolean {
    const set = this.connections.get(address);
    return set !== undefined && set.size > 0;
  }

  markConnected(address: string, bridgeKind: TrackedBridgeKind, connectionId: string): void {
    let set = this.connections.get(address);
    const wasOnline = set !== undefined && set.size > 0;
    if (!set) {
      set = new Set();
      this.connections.set(address, set);
    }
    set.add(connectionId);
    if (!wasOnline) {
      this.events?.emit('inbox.peer_connected', { address, bridgeKind });
    }
  }

  markDisconnected(
    address: string,
    bridgeKind: TrackedBridgeKind,
    connectionId: string,
    reason: 'closed' | 'error',
  ): void {
    const set = this.connections.get(address);
    if (!set) return;
    if (!set.delete(connectionId)) return;
    if (set.size === 0) {
      this.connections.delete(address);
      this.events?.emit('inbox.peer_disconnected', { address, bridgeKind, reason });
    }
  }

  /** Inspect the underlying map. Test/observability use only. */
  snapshot(): Map<string, ReadonlySet<string>> {
    return new Map(
      Array.from(this.connections.entries(), ([k, v]) => [k, v as ReadonlySet<string>]),
    );
  }
}
