import type { GameMessage, PeerId, SignalPayload } from "@psx/shared";

/**
 * One WebRTC connection to one peer.
 *
 * GDD §4 and §8 name PeerJS or simple-peer, but both are a poor fit here:
 * simple-peer depends on Node's `buffer` and `readable-stream` (polyfill pain
 * under Vite), and PeerJS is built around its own broker, which would replace
 * the custom WebSocket signalling §4 explicitly calls for. What both libraries
 * would give us is a data channel after an SDP exchange, and that is a thin
 * wrapper over `RTCPeerConnection` — so this is that wrapper, with no
 * dependencies.
 *
 * Two channels, because gameplay traffic has two different delivery needs:
 *
 *   - `state`  unreliable, unordered — position snapshots. A snapshot that
 *              arrives late has already been superseded, so retransmitting it
 *              adds latency and tells us nothing new.
 *   - `event`  reliable, ordered — flags, despawns, inventory, chat. Dropping
 *              "the shutter opened" would desynchronise the world permanently.
 */

export interface PeerLinkOptions {
  peerId: PeerId;
  /** The offerer. In our star topology the host initiates to each guest. */
  isInitiator: boolean;
  sendSignal(payload: SignalPayload): void;
  iceServers?: RTCIceServer[];
}

export const DEFAULT_ICE_SERVERS: RTCIceServer[] = [
  { urls: ["stun:stun.l.google.com:19302", "stun:stun1.l.google.com:19302"] },
];

export type PeerLinkState = "connecting" | "connected" | "closed" | "failed";

export class PeerLink {
  readonly peerId: PeerId;

  private readonly connection: RTCPeerConnection;
  private readonly isInitiator: boolean;
  private readonly sendSignal: (payload: SignalPayload) => void;

  private stateChannel: RTCDataChannel | null = null;
  private eventChannel: RTCDataChannel | null = null;

  /** ICE can arrive before the remote description is set; queue until it is. */
  private pendingCandidates: RTCIceCandidateInit[] = [];
  private remoteDescriptionSet = false;

  state: PeerLinkState = "connecting";

  onMessage: ((message: GameMessage, from: PeerId) => void) | null = null;
  onStateChange: ((state: PeerLinkState, peerId: PeerId) => void) | null = null;

  constructor(options: PeerLinkOptions) {
    this.peerId = options.peerId;
    this.isInitiator = options.isInitiator;
    this.sendSignal = options.sendSignal;

    this.connection = new RTCPeerConnection({
      iceServers: options.iceServers ?? DEFAULT_ICE_SERVERS,
    });

    this.connection.onicecandidate = (event) => {
      if (!event.candidate) return;
      this.sendSignal({
        ice: {
          candidate: event.candidate.candidate,
          sdpMid: event.candidate.sdpMid,
          sdpMLineIndex: event.candidate.sdpMLineIndex,
        },
      });
    };

    this.connection.onconnectionstatechange = () => {
      switch (this.connection.connectionState) {
        case "connected":
          this.setState("connected");
          break;
        case "failed":
          this.setState("failed");
          break;
        case "closed":
        case "disconnected":
          this.setState("closed");
          break;
      }
    };

    if (this.isInitiator) {
      this.stateChannel = this.setupChannel(
        this.connection.createDataChannel("state", {
          ordered: false,
          maxRetransmits: 0,
        }),
      );
      this.eventChannel = this.setupChannel(
        this.connection.createDataChannel("event", { ordered: true }),
      );
      void this.createOffer();
    } else {
      this.connection.ondatachannel = (event) => {
        if (event.channel.label === "state") this.stateChannel = this.setupChannel(event.channel);
        else this.eventChannel = this.setupChannel(event.channel);
      };
    }
  }

  private setState(state: PeerLinkState): void {
    if (this.state === state) return;
    this.state = state;
    this.onStateChange?.(state, this.peerId);
  }

  private setupChannel(channel: RTCDataChannel): RTCDataChannel {
    channel.onmessage = (event) => {
      try {
        this.onMessage?.(JSON.parse(event.data as string) as GameMessage, this.peerId);
      } catch {
        // A peer sending unparseable data is not worth tearing the link down.
      }
    };
    return channel;
  }

  private async createOffer(): Promise<void> {
    const offer = await this.connection.createOffer();
    await this.connection.setLocalDescription(offer);
    this.sendSignal({ sdp: { type: "offer", sdp: offer.sdp ?? "" } });
  }

  /** Feed a relayed signalling payload from this peer. */
  async handleSignal(payload: SignalPayload): Promise<void> {
    if (payload.sdp) {
      await this.connection.setRemoteDescription({
        type: payload.sdp.type,
        sdp: payload.sdp.sdp,
      });
      this.remoteDescriptionSet = true;

      // Candidates that raced ahead of the description can now be applied.
      for (const candidate of this.pendingCandidates) {
        await this.connection.addIceCandidate(candidate).catch(() => undefined);
      }
      this.pendingCandidates = [];

      if (payload.sdp.type === "offer") {
        const answer = await this.connection.createAnswer();
        await this.connection.setLocalDescription(answer);
        this.sendSignal({ sdp: { type: "answer", sdp: answer.sdp ?? "" } });
      }
      return;
    }

    if (payload.ice) {
      const candidate: RTCIceCandidateInit = {
        candidate: payload.ice.candidate,
        sdpMid: payload.ice.sdpMid,
        sdpMLineIndex: payload.ice.sdpMLineIndex,
      };
      if (!this.remoteDescriptionSet) {
        this.pendingCandidates.push(candidate);
        return;
      }
      await this.connection.addIceCandidate(candidate).catch(() => undefined);
    }
  }

  /** Snapshots and other superseded-on-arrival traffic. */
  sendState(message: GameMessage): void {
    if (this.stateChannel?.readyState === "open") {
      this.stateChannel.send(JSON.stringify(message));
    }
  }

  /** World state that must not be dropped. */
  sendEvent(message: GameMessage): void {
    if (this.eventChannel?.readyState === "open") {
      this.eventChannel.send(JSON.stringify(message));
    }
  }

  get isOpen(): boolean {
    return this.eventChannel?.readyState === "open";
  }

  close(): void {
    this.stateChannel?.close();
    this.eventChannel?.close();
    this.connection.close();
    this.setState("closed");
  }
}
