import type { PeerId, PeerInfo, SignalMessage, SignalPayload } from "@psx/shared";
import { PROTOCOL_VERSION } from "@psx/shared";

/**
 * WebSocket link to the signalling server (GDD §8).
 *
 * Carries SDP/ICE and lobby membership only — never gameplay. Once the data
 * channels are open this socket is idle apart from join/leave notifications,
 * which is why losing it mid-session is survivable and not treated as fatal.
 */

export interface SignalingHandlers {
  onWelcome(peerId: PeerId, isHost: boolean, peers: PeerInfo[]): void;
  onPeerJoined(peer: PeerInfo): void;
  onPeerLeft(peerId: PeerId): void;
  onPeerUpdated(peer: PeerInfo): void;
  onSignal(from: PeerId, payload: SignalPayload): void;
  onStart(roomId: string, spawnPointId: string): void;
  onError(message: string): void;
  onClose(): void;
}

export class SignalingClient {
  private socket: WebSocket | null = null;

  constructor(
    private readonly url: string,
    private readonly handlers: SignalingHandlers,
  ) {}

  connect(token: string, lobbyCode: string): Promise<void> {
    return new Promise((resolve, reject) => {
      const socket = new WebSocket(this.url);
      this.socket = socket;

      socket.onopen = () => {
        socket.send(
          JSON.stringify({ t: "hello", token, lobbyCode, protocol: PROTOCOL_VERSION }),
        );
      };

      socket.onmessage = (event) => {
        let message: SignalMessage;
        try {
          message = JSON.parse(event.data as string) as SignalMessage;
        } catch {
          return;
        }

        switch (message.t) {
          case "welcome":
            this.handlers.onWelcome(message.peerId, message.isHost, message.peers);
            resolve();
            break;
          case "peer-joined":
            this.handlers.onPeerJoined(message.peer);
            break;
          case "peer-left":
            this.handlers.onPeerLeft(message.peerId);
            break;
          case "peer-updated":
            this.handlers.onPeerUpdated(message.peer);
            break;
          case "signal":
            this.handlers.onSignal(message.from, message.payload);
            break;
          case "start":
            this.handlers.onStart(message.roomId, message.spawnPointId);
            break;
          case "error":
            this.handlers.onError(message.message);
            // An error before `welcome` means the handshake failed outright.
            reject(new Error(message.message));
            break;
        }
      };

      socket.onerror = () => reject(new Error("signalling connection failed"));
      socket.onclose = () => this.handlers.onClose();
    });
  }

  sendSignal(to: PeerId, payload: SignalPayload): void {
    this.send({ t: "signal", from: -1, to, payload });
  }

  setReady(isReady: boolean): void {
    this.send({ t: "ready", isReady });
  }

  private send(message: SignalMessage): void {
    if (this.socket?.readyState === WebSocket.OPEN) {
      this.socket.send(JSON.stringify(message));
    }
  }

  close(): void {
    this.socket?.close();
    this.socket = null;
  }
}
