import type { Vec3 } from "./math.js";

/**
 * Network protocol — GDD §8.
 *
 * Two distinct channels:
 *
 *  - **Signalling** (`SignalMessage`) rides the WebSocket to the Hono server.
 *    It exists only to exchange SDP and ICE candidates and to announce lobby
 *    membership. No gameplay ever crosses it.
 *
 *  - **Game** (`ClientMessage` / `HostMessage`) rides WebRTC data channels
 *    directly between peers. Topology is a star centred on the host, not a
 *    mesh: the host is authoritative, so guest-to-guest links would carry
 *    nothing anyone is allowed to trust.
 *
 * Field names are short because these are serialised on every snapshot at
 * `SNAPSHOT_HZ`, and JSON key overhead dominates a payload this small.
 */

export const PROTOCOL_VERSION = 1;

/** GDD §8: "10–20 Hz (adventure pacing is forgiving)". */
export const SNAPSHOT_HZ = 15;
export const SNAPSHOT_INTERVAL = 1 / SNAPSHOT_HZ;

export const MAX_PLAYERS = 4;

export type PeerId = number;

export interface PeerInfo {
  peerId: PeerId;
  displayName: string;
  isHost: boolean;
  isReady: boolean;
}

// --- signalling -------------------------------------------------------------

/** Opaque WebRTC descriptors. Kept `unknown`-ish so we never re-type the spec. */
export interface SignalPayload {
  sdp?: { type: "offer" | "answer"; sdp: string };
  ice?: { candidate: string; sdpMid: string | null; sdpMLineIndex: number | null };
}

export type SignalMessage =
  /** client → server, first frame after the socket opens */
  | { t: "hello"; token: string; lobbyCode: string; protocol: number }
  /** server → client, accepted */
  | { t: "welcome"; peerId: PeerId; isHost: boolean; peers: PeerInfo[] }
  /** server → all, membership changes */
  | { t: "peer-joined"; peer: PeerInfo }
  | { t: "peer-left"; peerId: PeerId }
  | { t: "peer-updated"; peer: PeerInfo }
  /** client → server → target client, relayed verbatim */
  | { t: "signal"; from: PeerId; to: PeerId; payload: SignalPayload }
  /** client → server */
  | { t: "ready"; isReady: boolean }
  /** server → all, host pressed start */
  | { t: "start"; roomId: string; spawnPointId: string }
  | { t: "error"; message: string };

// --- guest → host -----------------------------------------------------------

/**
 * Guests send intent, never position. The host is free to reject any of it,
 * which is the whole point of host-authoritative play (GDD §8).
 */
export type ClientMessage =
  | {
      t: "input";
      /** Guest's local tick, echoed back in snapshots so it can reconcile. */
      tick: number;
      /** Desired movement on the XZ plane, each component in -1..1. */
      move: { x: number; z: number };
      run: boolean;
      /** Facing the guest believes it has; the host treats it as advisory. */
      yaw: number;
    }
  | { t: "interact"; interactableId: string }
  | { t: "combine"; itemA: string; itemB: string }
  | { t: "chat"; text: string };

// --- host → guests ----------------------------------------------------------

export interface PlayerSnapshot {
  peerId: PeerId;
  p: Vec3;
  yaw: number;
  /** Horizontal speed, so remote avatars animate without a second message. */
  spd: number;
  grounded: boolean;
}

export type HostMessage =
  | {
      t: "snapshot";
      tick: number;
      /** Last input tick the host consumed, per peer, for reconciliation. */
      ack: Record<PeerId, number>;
      players: PlayerSnapshot[];
    }
  /** Authoritative world state. Sent on change, not per tick. */
  | { t: "flags"; set: string[]; unset: string[] }
  | { t: "despawn"; interactableIds: string[] }
  | { t: "inventory"; peerId: PeerId; items: Array<{ itemId: string; quantity: number }> }
  | { t: "notice"; text: string; to?: PeerId }
  | { t: "chat"; from: PeerId; text: string }
  /** Full state for a peer that just joined mid-session. */
  | {
      t: "sync";
      roomId: string;
      flags: string[];
      despawned: string[];
      players: PlayerSnapshot[];
    };

export type GameMessage = ClientMessage | HostMessage;
