/**
 * Shade-control-plane signaling for WebRTC.
 *
 * SDP offer/answer + trickle-ICE candidates ride as JSON plaintext over
 * `Shade.send` / `Shade.onMessage` — the same Double Ratchet that
 * everything else does. The signaling layer never sees ciphertext or
 * crypto material directly; it just dispatches typed messages.
 *
 * The interface is host-agnostic on purpose: tests inject a memory pair
 * (`MemoryShadeBridge.linked()`), the SDK injects a thin adapter over
 * `Shade.send` / `Shade.onMessage` (`createShadeBridgeFromShade()`).
 */

import { WebRtcSignalingError } from './errors.js';
import {
  encodeWebRtcSignaling,
  parseWebRtcSignaling,
  type WebRtcAnswerMessage,
  type WebRtcByeMessage,
  type WebRtcIceMessage,
  type WebRtcOfferMessage,
  type WebRtcSignalingMessage,
} from './types.js';

/**
 * Minimal bridge into the host messaging layer (typically `Shade.send` +
 * `Shade.onMessage`). The bridge transports plaintext strings end-to-end
 * encrypted by the underlying ratchet.
 *
 * `send` MUST resolve only after the receiver's `onMessage` handler has
 * fully processed the plaintext, so the signaling layer can rely on
 * causal ordering between offer/answer and trickled candidates.
 */
export interface ShadeBridge {
  send(peerAddress: string, plaintext: string): Promise<void>;
  onMessage(handler: (from: string, plaintext: string) => void | Promise<void>): () => void;
  /** This endpoint's own address — used by the manager when generating sessionIds. */
  readonly myAddress: string;
}

export type SignalingHandler = (
  from: string,
  message: WebRtcSignalingMessage,
) => void | Promise<void>;

export class WebRtcSignalingChannel {
  private readonly handlers = new Set<SignalingHandler>();
  private readonly unsubscribeBridge: () => void;
  private readonly passthrough: ((from: string, plaintext: string) => void) | undefined;

  constructor(
    private readonly bridge: ShadeBridge,
    options?: { passthrough?: (from: string, plaintext: string) => void },
  ) {
    this.passthrough = options?.passthrough;
    this.unsubscribeBridge = bridge.onMessage(async (from, plaintext) => {
      if (!plaintext.includes('shade.webrtc-')) {
        this.passthrough?.(from, plaintext);
        return;
      }
      const msg = parseWebRtcSignaling(plaintext);
      if (msg === null) {
        this.passthrough?.(from, plaintext);
        return;
      }
      // Awaiting handlers preserves causal order — the manager relies on
      // setRemoteDescription completing before trickled candidates arrive.
      for (const handler of [...this.handlers]) {
        try {
          await handler(from, msg);
        } catch (err) {
          console.error('[WebRtcSignalingChannel] handler error:', err);
        }
      }
    });
  }

  get myAddress(): string {
    return this.bridge.myAddress;
  }

  async sendOffer(peerAddress: string, sessionId: string, sdp: string): Promise<void> {
    const msg: WebRtcOfferMessage = {
      kind: 'shade.webrtc-offer/v1',
      sessionId,
      sdp,
    };
    return this.transmit(peerAddress, msg);
  }

  async sendAnswer(peerAddress: string, sessionId: string, sdp: string): Promise<void> {
    const msg: WebRtcAnswerMessage = {
      kind: 'shade.webrtc-answer/v1',
      sessionId,
      sdp,
    };
    return this.transmit(peerAddress, msg);
  }

  async sendIce(
    peerAddress: string,
    sessionId: string,
    candidate: WebRtcIceMessage['candidate'],
  ): Promise<void> {
    const msg: WebRtcIceMessage = {
      kind: 'shade.webrtc-ice/v1',
      sessionId,
      candidate,
    };
    return this.transmit(peerAddress, msg);
  }

  async sendBye(peerAddress: string, sessionId: string, reason?: string): Promise<void> {
    const msg: WebRtcByeMessage = {
      kind: 'shade.webrtc-bye/v1',
      sessionId,
      ...(reason !== undefined ? { reason } : {}),
    };
    return this.transmit(peerAddress, msg);
  }

  onSignal(handler: SignalingHandler): () => void {
    this.handlers.add(handler);
    return () => this.handlers.delete(handler);
  }

  destroy(): void {
    this.unsubscribeBridge();
    this.handlers.clear();
  }

  private async transmit(peer: string, msg: WebRtcSignalingMessage): Promise<void> {
    try {
      await this.bridge.send(peer, encodeWebRtcSignaling(msg));
    } catch (err) {
      throw new WebRtcSignalingError(
        `failed to send ${msg.kind} to ${peer}: ${(err as Error).message}`,
      );
    }
  }
}

// ─── Memory bridge (tests) ─────────────────────────────────

export class MemoryShadeBridge implements ShadeBridge {
  private peer: MemoryShadeBridge | null = null;
  private handlers = new Set<(from: string, plaintext: string) => void | Promise<void>>();

  private constructor(public readonly myAddress: string) {}

  static linked(addressA: string, addressB: string): {
    a: MemoryShadeBridge;
    b: MemoryShadeBridge;
  } {
    const a = new MemoryShadeBridge(addressA);
    const b = new MemoryShadeBridge(addressB);
    a.peer = b;
    b.peer = a;
    return { a, b };
  }

  async send(peerAddress: string, plaintext: string): Promise<void> {
    if (this.peer === null) {
      throw new WebRtcSignalingError('MemoryShadeBridge: not linked');
    }
    if (peerAddress !== this.peer.myAddress) {
      throw new WebRtcSignalingError(
        `MemoryShadeBridge: peer mismatch (expected ${this.peer.myAddress}, got ${peerAddress})`,
      );
    }
    const target = this.peer;
    const from = this.myAddress;
    for (const handler of [...target.handlers]) {
      await handler(from, plaintext);
    }
  }

  onMessage(handler: (from: string, plaintext: string) => void | Promise<void>): () => void {
    this.handlers.add(handler);
    return () => this.handlers.delete(handler);
  }
}
