/**
 * Three.js presentation for already-validated remote entity snapshots.
 *
 * Networking, trust, auth, and display names stay outside this module. It keeps
 * only canonical protocol ids in a private Map, buffers authoritative motion,
 * and gives each peer one stable, identity-free scene root. Procedural clones
 * share four prototype resource sets for the lifetime of the adapter.
 */
import * as THREE from "three";
import {
  buildCrow,
  buildDog,
  buildHumanoid,
  cloneCrow,
  cloneDog,
  cloneHumanoid,
  disposeCrow,
  disposeDog,
  disposeHumanoid,
  dogPoseStateForSpeed,
  poseCrowFlight,
  poseDog,
  poseHumanoid,
  type CrowRig,
  type DogRig,
  type HumanoidRig,
} from "../assets/actors/index.ts";
import {
  buildModelX,
  cloneModelX,
  disposeModelX,
  setModelXSteering,
  setModelXWheelRotation,
  type ModelXRig,
} from "../assets/vehicles/index.ts";
import {
  buildElectricAircraft,
  disposeElectricAircraft,
  setAircraftControlSurfaces,
  setAircraftFanRotation,
  type ElectricAircraftRig,
} from "../aircraft/asset.ts";
import { PoseInterpolationBuffer } from "./interpolation.ts";
import { isEntityPoseSnapshot } from "./protocol.ts";
import type {
  BufferedPoseSample,
  EntityPoseSnapshot,
  InterpolationOptions,
  SpatialPose,
} from "./types.ts";

export interface ScenePeersOptions {
  /** Geographic map projection. Returned X/Z are already scene units. */
  project(lat: number, lng: number): readonly [number, number];
  /** Terrain height in scene units at a geographic position. */
  groundAt(lat: number, lng: number): number;
  /** Converts geographic altitude metres and, by default, asset metres to scene units. */
  geographicSceneUnitsPerMetre?: number;
  /** Optional presentation exaggeration independent of geographic motion scale. */
  geographicVisualScale?: number;
  /** Local office/city metre scale. Defaults to direct 1 metre = 1 scene unit. */
  localSceneUnitsPerMetre?: number;
  /** Optional presentation scale independent of local coordinate scale. */
  localVisualScale?: number;
  interpolation?: InterpolationOptions;
  /** Bounded nearby peer count. Defaults to 128, maximum 512. */
  maximumPeers?: number;
}

export interface ScenePeers {
  root: THREE.Group;
  /** Add a new authoritative sample. Invalid/stale/over-capacity input returns false. */
  upsert(snapshot: EntityPoseSnapshot): boolean;
  /** Remove by canonical id from `scenePeerId`/`ids`. */
  remove(id: string): boolean;
  /** Apply interpolated state at authoritative server time; returns roots updated. */
  tick(serverTimeMs: number): number;
  ids(): readonly string[];
  count(): number;
  /** Interest reset: drops peers but retains shared prototype GPU resources. */
  clear(): void;
  dispose(): void;
}

type PeerVisual =
  | { kind: "humanoid"; rig: HumanoidRig }
  | { kind: "dog"; rig: DogRig }
  | { kind: "crow"; rig: CrowRig }
  | { kind: "model-x"; rig: ModelXRig }
  | { kind: "electric-vtail"; rig: ElectricAircraftRig };

interface Prototypes {
  humanoid: HumanoidRig;
  dog: DogRig;
  crow: CrowRig;
  vehicle: ModelXRig;
  aircraft: ElectricAircraftRig;
}

interface Peer {
  readonly id: string;
  readonly root: THREE.Group;
  readonly buffer: PoseInterpolationBuffer;
  visual: PeerVisual;
  kind: EntityPoseSnapshot["kind"];
  spaceKey: string;
  latest: EntityPoseSnapshot;
  lastSequence: number;
  lastTimestampMs: number;
}

export function scenePeerId(snapshot: EntityPoseSnapshot): string {
  if (snapshot.entity === "actor") return `actor:${snapshot.actorId}`;
  if (snapshot.entity === "vehicle") return `vehicle:${snapshot.vehicleId}`;
  return `aircraft:${snapshot.aircraftId}`;
}

function poseSpaceKey(pose: SpatialPose): string {
  return pose.space === "geographic" ? "geographic" : `local:${JSON.stringify(pose.cell)}`;
}

function positive(value: number, name: string): number {
  if (!(value > 0) || !Number.isFinite(value)) throw new RangeError(`${name} must be finite and positive`);
  return value;
}

function resolveMaximumPeers(value: number | undefined): number {
  const resolved = value ?? 128;
  if (!Number.isInteger(resolved) || resolved < 1 || resolved > 512) {
    throw new RangeError("maximumPeers must be an integer from 1 to 512");
  }
  return resolved;
}

function makePrototypes(): Prototypes {
  const vehicle = buildModelX({ detail: "corridor", paint: 0x050607 });
  vehicle.root.name = "generic-black-ev.prototype";
  vehicle.root.userData.vehicleModel = "generic-black-ev";
  return {
    humanoid: buildHumanoid(),
    dog: buildDog(),
    crow: buildCrow(),
    vehicle,
    aircraft: buildElectricAircraft(),
  };
}

function childGroup(root: THREE.Group, name: string): THREE.Group {
  const value = root.getObjectByName(name);
  if (!(value instanceof THREE.Group)) throw new Error(`aircraft prototype is missing ${name}`);
  return value;
}

function cloneAircraft(prototype: ElectricAircraftRig): ElectricAircraftRig {
  const root = prototype.root.clone(true);
  const fans: THREE.Group[] = [];
  root.traverse((value) => {
    if (value instanceof THREE.Group && value.name.startsWith("electric-aircraft.fan-")) {
      fans.push(value);
    }
  });
  return {
    root,
    leftAileron: childGroup(root, "electric-aircraft.aileron-left"),
    rightAileron: childGroup(root, "electric-aircraft.aileron-right"),
    leftVTail: childGroup(root, "electric-aircraft.v-tail-left"),
    rightVTail: childGroup(root, "electric-aircraft.v-tail-right"),
    fans,
    ownsMaterials: false,
  };
}

function cloneVisual(snapshot: EntityPoseSnapshot, prototypes: Prototypes): PeerVisual {
  if (snapshot.entity === "aircraft") {
    return { kind: "electric-vtail", rig: cloneAircraft(prototypes.aircraft) };
  }
  if (snapshot.entity === "vehicle") {
    const rig = cloneModelX(prototypes.vehicle);
    rig.root.name = "generic-black-ev";
    rig.root.userData.vehicleModel = "generic-black-ev";
    return { kind: "model-x", rig };
  }
  if (snapshot.kind === "dog") return { kind: "dog", rig: cloneDog(prototypes.dog) };
  if (snapshot.kind === "crow") return { kind: "crow", rig: cloneCrow(prototypes.crow) };
  return { kind: "humanoid", rig: cloneHumanoid(prototypes.humanoid) };
}

function visualRoot(visual: PeerVisual): THREE.Group {
  return visual.rig.root;
}

function replaceVisual(peer: Peer, snapshot: EntityPoseSnapshot, prototypes: Prototypes): void {
  visualRoot(peer.visual).removeFromParent();
  peer.visual = cloneVisual(snapshot, prototypes);
  peer.root.add(visualRoot(peer.visual));
  peer.kind = snapshot.kind;
}

function animateActor(
  visual: Extract<PeerVisual, { kind: "humanoid" | "dog" | "crow" }>,
  sample: BufferedPoseSample,
): void {
  const speed = Math.hypot(sample.velocity.xMps, sample.velocity.zMps);
  const seconds = sample.timestampMs / 1_000;
  if (visual.kind === "humanoid") {
    poseHumanoid(visual.rig, {
      walkPhase: seconds * Math.max(1.2, speed * 5.8),
      stride: THREE.MathUtils.clamp(speed / 3.2, 0, 0.68),
    });
  } else if (visual.kind === "dog") {
    const state = dogPoseStateForSpeed(speed);
    const turn = THREE.MathUtils.clamp(sample.velocity.yawDegPerSec / 160, -1, 1);
    poseDog(visual.rig, {
      state,
      phase: seconds * Math.max(1.4, speed * 7.2),
      amount: state === "idle" ? 1 : THREE.MathUtils.clamp(0.38 + speed / 4.2, 0.38, 1),
      turn,
      attentionYaw: -turn * 0.08,
    });
  } else {
    poseCrowFlight(
      visual.rig,
      seconds * Math.max(3, speed * 1.5),
      THREE.MathUtils.clamp(0.35 + speed / 14, 0.35, 1),
    );
  }
}

export function createScenePeers(options: ScenePeersOptions): ScenePeers {
  if (typeof options.project !== "function" || typeof options.groundAt !== "function") {
    throw new RangeError("scene peers require project and groundAt functions");
  }
  const geographicScale = positive(
    options.geographicSceneUnitsPerMetre ?? 1,
    "geographicSceneUnitsPerMetre",
  );
  const geographicVisualScale = positive(
    options.geographicVisualScale ?? geographicScale,
    "geographicVisualScale",
  );
  const localScale = positive(options.localSceneUnitsPerMetre ?? 1, "localSceneUnitsPerMetre");
  const localVisualScale = positive(options.localVisualScale ?? localScale, "localVisualScale");
  const limit = resolveMaximumPeers(options.maximumPeers);
  const interpolation = { ...(options.interpolation ?? {}) };
  const prototypes = makePrototypes();
  const peers = new Map<string, Peer>();
  const root = new THREE.Group();
  root.name = "realtime-scene-peers";
  root.userData.kind = "remote-presence";
  let disposed = false;

  function add(snapshot: EntityPoseSnapshot): Peer {
    const visual = cloneVisual(snapshot, prototypes);
    const peerRoot = new THREE.Group();
    // IDs deliberately stay in the private Map, not names/userData inspected by render/debug tooling.
    peerRoot.name = "remote-peer";
    peerRoot.userData.entity = snapshot.entity;
    peerRoot.userData.kind = snapshot.kind;
    peerRoot.userData.forwardAxis = "-Z";
    peerRoot.add(visualRoot(visual));
    root.add(peerRoot);
    const peer: Peer = {
      id: scenePeerId(snapshot),
      root: peerRoot,
      buffer: new PoseInterpolationBuffer(interpolation),
      visual,
      kind: snapshot.kind,
      spaceKey: poseSpaceKey(snapshot.pose),
      latest: snapshot,
      lastSequence: -1,
      lastTimestampMs: -1,
    };
    peers.set(peer.id, peer);
    return peer;
  }

  function apply(peer: Peer, sample: BufferedPoseSample): boolean {
    const pose = sample.pose;
    let visualScale: number;
    if (pose.space === "geographic") {
      const projected = options.project(pose.lat, pose.lng);
      const ground = options.groundAt(pose.lat, pose.lng);
      if (
        projected.length < 2 ||
        !Number.isFinite(projected[0]) ||
        !Number.isFinite(projected[1]) ||
        !Number.isFinite(ground)
      ) {
        peer.root.visible = false;
        return false;
      }
      peer.root.position.set(
        projected[0],
        ground + pose.altitudeM * geographicScale,
        projected[1],
      );
      visualScale = geographicVisualScale;
    } else {
      peer.root.position.set(pose.xM * localScale, pose.yM * localScale, pose.zM * localScale);
      visualScale = localVisualScale;
    }
    peer.root.visible = true;
    peer.root.scale.setScalar(visualScale);
    peer.root.rotation.order = "YXZ";
    peer.root.rotation.set(
      THREE.MathUtils.degToRad(pose.pitchDeg),
      -THREE.MathUtils.degToRad(pose.headingDeg),
      peer.latest.entity === "aircraft"
        ? -THREE.MathUtils.degToRad(peer.latest.rollDeg)
        : 0,
    );
    if (peer.visual.kind === "model-x") {
      const latest = peer.latest.entity === "vehicle" ? peer.latest : null;
      if (latest) {
        setModelXSteering(peer.visual.rig, latest.steering * 0.62);
        // Simulation publishes positive travelled radians; the -Z-forward
        // wheel geometry rolls forward with negative local-X rotation.
        setModelXWheelRotation(peer.visual.rig, -latest.wheelRadians);
      }
    } else if (peer.visual.kind === "electric-vtail") {
      const latest = peer.latest.entity === "aircraft" ? peer.latest : null;
      if (latest) {
        setAircraftControlSurfaces(peer.visual.rig, {
          roll: latest.rollInput,
          pitch: latest.pitchInput,
          yaw: latest.yawInput,
        });
        setAircraftFanRotation(peer.visual.rig, latest.fanRadians);
      }
    } else {
      animateActor(peer.visual, sample);
    }
    return true;
  }

  return {
    root,
    upsert(snapshot) {
      if (disposed || !isEntityPoseSnapshot(snapshot)) return false;
      const id = scenePeerId(snapshot);
      let peer = peers.get(id);
      if (!peer) {
        if (peers.size >= limit) return false;
        peer = add(snapshot);
      } else if (
        snapshot.sequence <= peer.lastSequence ||
        snapshot.timestampMs <= peer.lastTimestampMs
      ) {
        return false;
      }

      if (peer.kind !== snapshot.kind) replaceVisual(peer, snapshot, prototypes);
      const nextSpaceKey = poseSpaceKey(snapshot.pose);
      if (nextSpaceKey !== peer.spaceKey) {
        // Interest boundaries are not continuous coordinate frames. Resetting
        // prevents a floor/city switch from interpolating through empty space.
        peer.buffer.clear();
        peer.spaceKey = nextSpaceKey;
      }
      if (!peer.buffer.push(snapshot)) {
        // A failed first push must not consume bounded capacity.
        if (peer.lastSequence < 0) {
          peer.root.removeFromParent();
          peers.delete(id);
        }
        return false;
      }
      peer.latest = snapshot;
      peer.lastSequence = snapshot.sequence;
      peer.lastTimestampMs = snapshot.timestampMs;
      peer.root.userData.entity = snapshot.entity;
      peer.root.userData.kind = snapshot.kind;
      return true;
    },
    remove(id) {
      if (disposed) return false;
      const peer = peers.get(id);
      if (!peer) return false;
      peer.root.removeFromParent();
      peer.buffer.clear();
      peers.delete(id);
      return true;
    },
    tick(serverTimeMs) {
      if (disposed || !Number.isFinite(serverTimeMs)) return 0;
      let updated = 0;
      for (const peer of peers.values()) {
        const sample = peer.buffer.sample(serverTimeMs);
        if (sample && apply(peer, sample)) updated += 1;
      }
      return updated;
    },
    ids: () => [...peers.keys()],
    count: () => peers.size,
    clear() {
      for (const peer of peers.values()) {
        peer.root.removeFromParent();
        peer.buffer.clear();
      }
      peers.clear();
    },
    dispose() {
      if (disposed) return;
      for (const peer of peers.values()) peer.root.removeFromParent();
      peers.clear();
      root.removeFromParent();
      disposeHumanoid(prototypes.humanoid);
      disposeDog(prototypes.dog);
      disposeCrow(prototypes.crow);
      disposeModelX(prototypes.vehicle);
      disposeElectricAircraft(prototypes.aircraft);
      disposed = true;
    },
  };
}
