/** Strict runtime validation for untrusted realtime protocol payloads. */

import type {
  ClientRealtimeMessage,
  EntityPoseSnapshot,
  InterestCell,
  MotionLimits,
  MotionValidationResult,
  RealtimeSequence,
  RealtimeValidationResult,
  ServerRealtimeMessage,
  SpatialPose,
} from "./types.ts";

const UINT32_MAX = 4_294_967_295;
const MAX_TIMESTAMP_MS = 8_640_000_000_000_000;
const MAX_INTERESTS = 128;
const MAX_ENTITIES_PER_MESSAGE = 2_048;
const MAX_STRING = 256;
const EARTH_RADIUS_M = 6_371_000;

function record(value: unknown): value is Record<string, unknown> {
  return typeof value === "object" && value !== null && !Array.isArray(value);
}

function string(value: unknown, maximum = MAX_STRING): value is string {
  return typeof value === "string" && value.length > 0 && value.length <= maximum;
}

function finite(value: unknown): value is number {
  return typeof value === "number" && Number.isFinite(value);
}

export function isRealtimeSequence(value: unknown): value is RealtimeSequence {
  return Number.isSafeInteger(value) && Number(value) >= 0 && Number(value) <= UINT32_MAX;
}

export function isRealtimeTimestamp(value: unknown): value is number {
  return Number.isSafeInteger(value) && Number(value) >= 0 && Number(value) <= MAX_TIMESTAMP_MS;
}

export function isInterestCell(value: unknown): value is InterestCell {
  if (!record(value)) return false;
  switch (value.kind) {
    case "california-tile":
      return (
        Number.isInteger(value.x) &&
        Number.isInteger(value.y) &&
        Number.isInteger(value.level) &&
        Number(value.x) >= -1_000_000 &&
        Number(value.x) <= 1_000_000 &&
        Number(value.y) >= -1_000_000 &&
        Number(value.y) <= 1_000_000 &&
        Number(value.level) >= 0 &&
        Number(value.level) <= 24
      );
    case "city":
      return value.cityId === "bay-area" || value.cityId === "socal";
    case "office":
      return string(value.officeId);
    case "floor":
      return string(value.officeId) && string(value.floorId);
    case "room":
      return string(value.officeId) && string(value.floorId) && string(value.roomId);
    default:
      return false;
  }
}

function isSpatialPose(value: unknown): value is SpatialPose {
  if (!record(value)) return false;
  if (value.space === "geographic") {
    return (
      finite(value.lat) && value.lat >= -90 && value.lat <= 90 &&
      finite(value.lng) && value.lng >= -180 && value.lng <= 180 &&
      finite(value.altitudeM) && value.altitudeM >= -500 && value.altitudeM <= 100_000 &&
      finite(value.headingDeg) && value.headingDeg >= -360 && value.headingDeg <= 360 &&
      finite(value.pitchDeg) && value.pitchDeg >= -90 && value.pitchDeg <= 90
    );
  }
  return (
    value.space === "local" &&
    isInterestCell(value.cell) &&
    value.cell.kind !== "california-tile" &&
    finite(value.xM) && Math.abs(value.xM) <= 1_000_000 &&
    finite(value.yM) && Math.abs(value.yM) <= 100_000 &&
    finite(value.zM) && Math.abs(value.zM) <= 1_000_000 &&
    finite(value.headingDeg) && value.headingDeg >= -360 && value.headingDeg <= 360 &&
    finite(value.pitchDeg) && value.pitchDeg >= -90 && value.pitchDeg <= 90
  );
}

function isVelocity(value: unknown): boolean {
  return (
    record(value) &&
    finite(value.xMps) && Math.abs(value.xMps) <= 1_000 &&
    finite(value.yMps) && Math.abs(value.yMps) <= 1_000 &&
    finite(value.zMps) && Math.abs(value.zMps) <= 1_000 &&
    finite(value.yawDegPerSec) && Math.abs(value.yawDegPerSec) <= 10_000
  );
}

export function isEntityPoseSnapshot(value: unknown): value is EntityPoseSnapshot {
  if (
    !record(value) ||
    !isRealtimeSequence(value.sequence) ||
    !isRealtimeTimestamp(value.timestampMs) ||
    !isSpatialPose(value.pose) ||
    !isVelocity(value.velocity)
  ) return false;
  if (value.entity === "actor") {
    return (
      string(value.actorId) &&
      (value.kind === "humanoid" || value.kind === "dog" || value.kind === "crow")
    );
  }
  if (value.entity === "vehicle") {
    return (
      string(value.vehicleId) &&
      value.kind === "model-x" &&
      (value.driverActorId === null || string(value.driverActorId)) &&
      finite(value.steering) && value.steering >= -1 && value.steering <= 1 &&
      finite(value.wheelRadians) && Math.abs(value.wheelRadians) <= 1_000_000
    );
  }
  return (
    value.entity === "aircraft" &&
    string(value.aircraftId) &&
    value.kind === "electric-vtail" &&
    string(value.pilotActorId) &&
    finite(value.rollDeg) && value.rollDeg >= -90 && value.rollDeg <= 90 &&
    finite(value.throttle) && value.throttle >= 0 && value.throttle <= 1 &&
    finite(value.rollInput) && value.rollInput >= -1 && value.rollInput <= 1 &&
    finite(value.pitchInput) && value.pitchInput >= -1 && value.pitchInput <= 1 &&
    finite(value.yawInput) && value.yawInput >= -1 && value.yawInput <= 1 &&
    finite(value.fanRadians) && Math.abs(value.fanRadians) <= 1_000_000
  );
}

function interests(value: unknown): value is readonly InterestCell[] {
  return Array.isArray(value) && value.length <= MAX_INTERESTS && value.every(isInterestCell);
}

function entities(value: unknown): value is readonly EntityPoseSnapshot[] {
  return Array.isArray(value) && value.length <= MAX_ENTITIES_PER_MESSAGE && value.every(isEntityPoseSnapshot);
}

function common(value: Record<string, unknown>): boolean {
  return value.protocolVersion === 1 && string(value.requestId);
}

export function validateClientRealtimeMessage(
  value: unknown,
): RealtimeValidationResult<ClientRealtimeMessage> {
  if (!record(value) || !common(value)) return { ok: false, error: "realtime: invalid envelope" };
  if (value.type === "join-request") {
    if (
      string(value.actorId) &&
      (value.resumeToken === null || string(value.resumeToken, 512)) &&
      (value.lastReceivedSequence === null || isRealtimeSequence(value.lastReceivedSequence)) &&
      interests(value.interests)
    ) return { ok: true, value: value as unknown as ClientRealtimeMessage };
    return { ok: false, error: "realtime: invalid join request" };
  }
  if (value.type === "resume-request") {
    if (
      string(value.sessionId) && string(value.resumeToken, 512) && string(value.serverEpoch) &&
      isRealtimeSequence(value.lastReceivedSequence) && interests(value.interests)
    ) return { ok: true, value: value as unknown as ClientRealtimeMessage };
    return { ok: false, error: "realtime: invalid resume request" };
  }
  return { ok: false, error: "realtime: unknown client message" };
}

export function validateServerRealtimeMessage(
  value: unknown,
): RealtimeValidationResult<ServerRealtimeMessage> {
  if (!record(value) || value.protocolVersion !== 1) {
    return { ok: false, error: "realtime: invalid envelope" };
  }
  if (value.type === "join-grant") {
    if (
      string(value.requestId) && string(value.sessionId) && string(value.actorId) &&
      (value.role === "visitor" || value.role === "member" || value.role === "admin") &&
      string(value.serverEpoch) && isRealtimeTimestamp(value.serverTimeMs) &&
      isRealtimeSequence(value.nextSequence) && string(value.resumeToken, 512) &&
      interests(value.interests) && entities(value.initial)
    ) return { ok: true, value: value as unknown as ServerRealtimeMessage };
    return { ok: false, error: "realtime: invalid join grant" };
  }
  if (value.type === "resume-grant") {
    if (
      string(value.requestId) && string(value.sessionId) && string(value.serverEpoch) &&
      isRealtimeTimestamp(value.serverTimeMs) && isRealtimeSequence(value.nextSequence) &&
      string(value.resumeToken, 512) && typeof value.continuous === "boolean" && entities(value.snapshot)
    ) return { ok: true, value: value as unknown as ServerRealtimeMessage };
    return { ok: false, error: "realtime: invalid resume grant" };
  }
  if (value.type === "pose-delta") {
    if (
      string(value.serverEpoch) && isRealtimeSequence(value.sequence) &&
      isRealtimeTimestamp(value.timestampMs) && entities(value.updates) &&
      Array.isArray(value.removedEntityIds) &&
      value.removedEntityIds.length <= MAX_ENTITIES_PER_MESSAGE &&
      value.removedEntityIds.every((id) => string(id))
    ) return { ok: true, value: value as unknown as ServerRealtimeMessage };
    return { ok: false, error: "realtime: invalid pose delta" };
  }
  if (value.type === "membership-revoked") {
    const reasons = new Set([
      "signed-out", "membership-revoked", "session-replaced", "protocol-violation", "server-shutdown",
    ]);
    if (
      string(value.serverEpoch) && isRealtimeSequence(value.sequence) &&
      isRealtimeTimestamp(value.timestampMs) && string(value.sessionId) && string(value.actorId) &&
      reasons.has(String(value.reason)) && typeof value.reconnectAllowed === "boolean"
    ) return { ok: true, value: value as unknown as ServerRealtimeMessage };
    return { ok: false, error: "realtime: invalid membership revocation" };
  }
  return { ok: false, error: "realtime: unknown server message" };
}

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

function localCellKey(pose: SpatialPose): string | null {
  if (pose.space !== "local") return null;
  return JSON.stringify(pose.cell);
}

function angleDeltaDegrees(from: number, to: number): number {
  return ((to - from + 540) % 360) - 180;
}

function displacementMetres(from: SpatialPose, to: SpatialPose): { horizontal: number; vertical: number } | null {
  if (from.space !== to.space) return null;
  if (from.space === "local" && to.space === "local") {
    if (localCellKey(from) !== localCellKey(to)) return null;
    return { horizontal: Math.hypot(to.xM - from.xM, to.zM - from.zM), vertical: to.yM - from.yM };
  }
  if (from.space === "geographic" && to.space === "geographic") {
    const meanLat = ((from.lat + to.lat) / 2) * Math.PI / 180;
    const north = (to.lat - from.lat) * Math.PI / 180 * EARTH_RADIUS_M;
    const east = (to.lng - from.lng) * Math.PI / 180 * Math.cos(meanLat) * EARTH_RADIUS_M;
    return { horizontal: Math.hypot(north, east), vertical: to.altitudeM - from.altitudeM };
  }
  return null;
}

/** Server-side helper for rejecting impossible client-proposed movement. */
export function validateMotion(
  previous: EntityPoseSnapshot,
  proposed: EntityPoseSnapshot,
  limits: MotionLimits,
): MotionValidationResult {
  if (entityId(previous) !== entityId(proposed)) return { ok: false, reason: "identity-mismatch" };
  const displacement = displacementMetres(previous.pose, proposed.pose);
  if (!displacement) return { ok: false, reason: "space-mismatch" };
  const dt = (proposed.timestampMs - previous.timestampMs) / 1_000;
  const minimum = Math.max(0.001, limits.minimumDeltaSeconds ?? 1 / 240);
  const maximum = Math.max(minimum, limits.maximumDeltaSeconds ?? 2);
  if (!Number.isFinite(dt) || dt < minimum || dt > maximum || proposed.sequence <= previous.sequence) {
    return { ok: false, reason: "time-invalid" };
  }
  const horizontalSpeedMps = Math.max(0, displacement.horizontal - Math.max(0, limits.positionSlackM)) / dt;
  const verticalSpeedMps = Math.abs(displacement.vertical) / dt;
  const turnRateDegPerSec = Math.abs(angleDeltaDegrees(previous.pose.headingDeg, proposed.pose.headingDeg)) / dt;
  if (
    !finite(limits.maximumHorizontalSpeedMps) || horizontalSpeedMps > limits.maximumHorizontalSpeedMps ||
    !finite(limits.maximumVerticalSpeedMps) || verticalSpeedMps > limits.maximumVerticalSpeedMps ||
    !finite(limits.maximumTurnRateDegPerSec) || turnRateDegPerSec > limits.maximumTurnRateDegPerSec
  ) return { ok: false, reason: "speed-exceeded" };
  return { ok: true, horizontalSpeedMps, verticalSpeedMps, turnRateDegPerSec };
}
