import { Vector3 } from "three";
import type { ClientMessage, PeerId, PlayerSnapshot } from "@psx/shared";
import { SNAPSHOT_INTERVAL } from "@psx/shared";
import type { PhysicsWorld } from "../engine/PhysicsWorld.js";
import { CharacterMover, DEFAULT_MOVER_CONFIG } from "../systems/CharacterMover.js";
import { DEFAULT_PLAYER_CONFIG } from "../systems/PlayerController.js";

/**
 * Host-side simulation of every remote player — GDD §8.
 *
 * The host runs a full `CharacterMover` per guest against its own physics
 * world. Guests send *intent* (a movement vector), never position, so a
 * modified client cannot walk through walls or cross the map: the worst it can
 * do is hold a direction the host will collision-resolve like any other.
 *
 * Inputs are consumed at the host's fixed tick rate, not at the rate they
 * arrive. A guest flooding inputs therefore gains no speed advantage — the
 * extra frames simply overwrite the pending intent.
 */

export interface RemotePlayerState {
  peerId: PeerId;
  mover: CharacterMover;
  /** Latest intent, replaced rather than queued. */
  desiredMove: Vector3;
  run: boolean;
  yaw: number;
  lastInputTick: number;
  /** Ticks since we last heard anything; drives idle-out. */
  silentTicks: number;
}

/** ~2 seconds at 60 Hz. A peer this quiet has stalled or dropped. */
const SILENT_TICK_LIMIT = 120;

export class HostAuthority {
  private readonly players = new Map<PeerId, RemotePlayerState>();
  private snapshotTimer = 0;
  private tick = 0;

  constructor(private readonly physics: PhysicsWorld) {}

  /** Spawn a mover for a guest that just connected. */
  addPlayer(peerId: PeerId, spawn: Vector3): RemotePlayerState {
    const existing = this.players.get(peerId);
    if (existing) return existing;

    const bodyId = `player:${peerId}`;
    const totalHeight = DEFAULT_MOVER_CONFIG.cylinderHeight + DEFAULT_MOVER_CONFIG.radius * 2;

    this.physics.createKinematicCapsule({
      id: bodyId,
      position: { x: spawn.x, y: spawn.y + totalHeight / 2, z: spawn.z },
      radius: DEFAULT_MOVER_CONFIG.radius,
      height: DEFAULT_MOVER_CONFIG.cylinderHeight,
    });

    const state: RemotePlayerState = {
      peerId,
      mover: new CharacterMover(this.physics, bodyId, spawn.clone()),
      desiredMove: new Vector3(),
      run: false,
      yaw: 0,
      lastInputTick: 0,
      silentTicks: 0,
    };
    this.players.set(peerId, state);
    return state;
  }

  removePlayer(peerId: PeerId): void {
    if (!this.players.delete(peerId)) return;
    this.physics.remove(`player:${peerId}`);
  }

  /** Record a guest's latest intent. Already validated by `NetSession`. */
  applyInput(peerId: PeerId, message: Extract<ClientMessage, { t: "input" }>): void {
    const player = this.players.get(peerId);
    if (!player) return;

    // Out-of-order arrival on the unreliable channel: keep the newest only.
    if (message.tick < player.lastInputTick) return;

    player.lastInputTick = message.tick;
    player.silentTicks = 0;
    player.run = message.run;
    player.yaw = message.yaw;

    // Re-normalise: the validator clamps each axis, so a diagonal could still
    // arrive at length √2 and out-run an axis-aligned one.
    const move = player.desiredMove.set(message.move.x, 0, message.move.z);
    if (move.lengthSq() > 1) move.normalize();
  }

  /** Advance every remote player one fixed tick. */
  update(dt: number): void {
    this.tick++;

    for (const player of this.players.values()) {
      player.silentTicks++;

      // A silent peer coasts to a stop rather than sliding on forever.
      if (player.silentTicks > SILENT_TICK_LIMIT) {
        player.desiredMove.set(0, 0, 0);
      }

      const speed = player.run ? DEFAULT_PLAYER_CONFIG.runSpeed : DEFAULT_PLAYER_CONFIG.walkSpeed;
      player.mover.update(
        new Vector3(player.desiredMove.x * speed, 0, player.desiredMove.z * speed),
        dt,
      );
    }
  }

  /** True when it is time to emit a snapshot, at `SNAPSHOT_HZ`. */
  shouldSnapshot(dt: number): boolean {
    this.snapshotTimer += dt;
    if (this.snapshotTimer < SNAPSHOT_INTERVAL) return false;
    this.snapshotTimer -= SNAPSHOT_INTERVAL;
    return true;
  }

  /** Build the authoritative snapshot, including the host's own pose. */
  buildSnapshot(hostPose: { position: Vector3; yaw: number; speed: number; grounded: boolean }): {
    tick: number;
    ack: Record<PeerId, number>;
    players: PlayerSnapshot[];
  } {
    const players: PlayerSnapshot[] = [
      {
        peerId: 0,
        p: { x: round(hostPose.position.x), y: round(hostPose.position.y), z: round(hostPose.position.z) },
        yaw: round(hostPose.yaw),
        spd: round(hostPose.speed),
        grounded: hostPose.grounded,
      },
    ];
    const ack: Record<PeerId, number> = {};

    for (const player of this.players.values()) {
      const state = player.mover.state;
      players.push({
        peerId: player.peerId,
        p: { x: round(state.position.x), y: round(state.position.y), z: round(state.position.z) },
        yaw: round(player.yaw),
        spd: round(state.lastMoveDistance * 60),
        grounded: state.grounded,
      });
      ack[player.peerId] = player.lastInputTick;
    }

    return { tick: this.tick, ack, players };
  }

  getPlayer(peerId: PeerId): RemotePlayerState | undefined {
    return this.players.get(peerId);
  }

  get peerIds(): PeerId[] {
    return [...this.players.keys()];
  }

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

/** Three decimals is well under a PSX pixel and roughly halves snapshot size. */
const round = (value: number): number => Math.round(value * 1000) / 1000;
