import { Color, Scene } from "three";
import type { PeerId, PlayerSnapshot } from "@psx/shared";
import { PlayerAvatar } from "../world/PlayerAvatar.js";
import { SnapshotBuffer } from "./SnapshotBuffer.js";
import type { HostAuthority } from "./HostAuthority.js";

/**
 * Avatars for everyone who is not the local player.
 *
 * Host and guest reach the same visual result by different routes:
 *
 *  - The **host** already simulates every guest at 60 Hz, so it reads poses
 *    straight out of `HostAuthority`. Routing them through the interpolation
 *    buffer would add ~133ms of lag to information it already has exactly.
 *  - A **guest** only receives 15 Hz snapshots, so it interpolates.
 */

/** Distinct enough to read at 240p without a palette that fights the fog. */
const PEER_COLORS = [0x9aa3b2, 0xb2907a, 0x7fa382, 0xa88bb0];

interface RemotePlayer {
  peerId: PeerId;
  avatar: PlayerAvatar;
  buffer: SnapshotBuffer;
  lastSpeed: number;
}

export class RemotePlayers {
  private readonly players = new Map<PeerId, RemotePlayer>();

  constructor(
    private readonly scene: Scene,
    private readonly avatarHeight: number,
  ) {}

  private ensure(peerId: PeerId): RemotePlayer {
    const existing = this.players.get(peerId);
    if (existing) return existing;

    const avatar = new PlayerAvatar({
      totalHeight: this.avatarHeight,
      color: PEER_COLORS[peerId % PEER_COLORS.length]!,
    });
    this.scene.add(avatar.group);

    const player: RemotePlayer = { peerId, avatar, buffer: new SnapshotBuffer(), lastSpeed: 0 };
    this.players.set(peerId, player);
    return player;
  }

  /** Guest path: buffer an incoming snapshot for later interpolation. */
  ingestSnapshot(snapshots: PlayerSnapshot[], localPeerId: PeerId, now: number): void {
    const seen = new Set<PeerId>();

    for (const snapshot of snapshots) {
      if (snapshot.peerId === localPeerId) continue;
      seen.add(snapshot.peerId);
      this.ensure(snapshot.peerId).buffer.push(snapshot, now);
    }

    // A peer absent from the authoritative snapshot has left the session.
    for (const peerId of [...this.players.keys()]) {
      if (!seen.has(peerId)) this.remove(peerId);
    }
  }

  /** Host path: read exact poses out of its own simulation. */
  syncFromAuthority(authority: HostAuthority, dt: number): void {
    const seen = new Set<PeerId>();

    for (const peerId of authority.peerIds) {
      const remote = authority.getPlayer(peerId);
      if (!remote) continue;
      seen.add(peerId);

      const player = this.ensure(peerId);
      const state = remote.mover.state;
      player.avatar.setTransform(state.position.x, state.position.y, state.position.z, remote.yaw);
      player.avatar.update(state.lastMoveDistance, dt);
    }

    for (const peerId of [...this.players.keys()]) {
      if (!seen.has(peerId)) this.remove(peerId);
    }
  }

  /** Guest path: sample the buffers and drive the avatars. */
  update(now: number, dt: number): void {
    for (const player of this.players.values()) {
      const pose = player.buffer.sample(now);
      if (!pose) continue;

      player.avatar.setTransform(pose.position.x, pose.position.y, pose.position.z, pose.yaw);
      // Speed is metres/second; the walk cycle wants distance for this frame.
      player.avatar.update(pose.fresh ? pose.speed * dt : 0, dt);
      player.lastSpeed = pose.speed;
    }
  }

  remove(peerId: PeerId): void {
    const player = this.players.get(peerId);
    if (!player) return;
    this.scene.remove(player.avatar.group);
    player.avatar.dispose();
    this.players.delete(peerId);
  }

  clear(): void {
    for (const peerId of [...this.players.keys()]) this.remove(peerId);
  }

  get count(): number {
    return this.players.size;
  }

  /** Peer colour, so the lobby list and the HUD agree with the world. */
  static colorFor(peerId: PeerId): number {
    return PEER_COLORS[peerId % PEER_COLORS.length]!;
  }

  static cssColorFor(peerId: PeerId): string {
    return `#${new Color(RemotePlayers.colorFor(peerId)).getHexString()}`;
  }
}
