import * as THREE from "three";
import {
  DISPLAY_UNITS_PER_KILOMETER,
  DISPLAY_UNITS_PER_LIGHT_YEAR,
  KILOMETERS_PER_AU,
  computeMoonLocalPosition,
  computePlanetLocalPosition,
  currentWorldTimeSeconds,
  resolveOrbitalAnchorPosition,
  toThreeVector,
} from "./viewerMath";
import { ACTIVE_SYSTEM_DETAIL_SCALE } from "./viewerConstants";
import { describeActiveSpace, resolveFocusedCelestialId } from "./viewerSelection";
import {
  resolveShipHeading,
  updateSystemStarPresentation,
  getAnimatedShipLocalPosition,
  iconWorldScale,
} from "./viewerPresentation";
import { rawObject } from "./viewerScenePrimitives";
import type {
  ResourceNodeDelta,
  ResourceNodeSnapshot,
  ShipSnapshot,
} from "./contracts";
import type {
  CelestialVisual,
  ClaimVisual,
  Selectable,
  ConstructionSiteVisual,
  NodeVisual,
  OrbitalAnchor,
  ShipVisual,
  StructureVisual,
  SystemVisual,
  WorldState,
  PovLevel,
  CameraMode,
} from "./viewerTypes";

type SummaryIconKind = "ship" | "station" | "structure";

const SHIP_BILLBOARD_HIDE_DISTANCE = 0.003;
const SHIP_BILLBOARD_FULL_DISTANCE = 0.018;
const SHIP_BILLBOARD_MIN_PIXELS = 34;
const SHIP_BILLBOARD_MAX_PIXELS = 82;
const STATION_ICON_MIN_PIXELS = 28;
const STATION_ICON_MAX_PIXELS = 72;

export interface WorldOrbitalContext {
  world?: WorldState;
  worldTimeSyncMs: number;
  worldSeed: number;
  nodeVisuals: Map<string, NodeVisual>;
  celestialVisuals: Map<string, CelestialVisual>;
  stationVisuals: Map<string, StructureVisual>;
}

export interface WorldPresentationContext extends WorldOrbitalContext {
  activeSystemId?: string;
  cameraMode: CameraMode;
  povLevel: PovLevel;
  orbitYaw: number;
  camera: THREE.PerspectiveCamera;
  systemAnchor: THREE.Vector3;
  shipVisuals: Map<string, ShipVisual>;
  claimVisuals: Map<string, ClaimVisual>;
  constructionSiteVisuals: Map<string, ConstructionSiteVisual>;
  systemVisuals: Map<string, SystemVisual>;
  systemSummaryVisuals: Map<string, any>;
  toDisplayLocalPosition: (localPosition: THREE.Vector3) => THREE.Vector3;
  updateSystemDetailVisibility: () => void;
  setShellReticleOpacity: (sprite: SystemVisual["shellReticle"], opacity: number) => void;
}

export interface GameStatusParams {
  world?: WorldState;
  activeSystemId?: string;
  cameraMode: CameraMode;
  povLevel: PovLevel;
  selectedItems: Selectable[];
  mode: string;
  galaxyAnchor?: THREE.Vector3;
  systemAnchor?: THREE.Vector3;
}

export function updateWorldPresentation(context: WorldPresentationContext) {
  const now = performance.now();
  const worldTimeSeconds = currentWorldTimeSeconds(context.world, context.worldTimeSyncMs);
  const renderMode = resolveRenderSpaceMode(context.activeSystemId, context.povLevel);

  for (const [shipId, visual] of context.shipVisuals.entries()) {
    const ship = context.world?.ships.get(shipId);
    if (!ship) {
      continue;
    }

    const worldPosition = getAnimatedShipLocalPosition(visual, now);
    const displayPosition = context.toDisplayLocalPosition(worldPosition);
    visual.mesh.setPosition(displayPosition);
    visual.icon.setPosition(rawObject(visual.mesh).position.clone());
    const shipVisible = isShipVisible(renderMode, context.activeSystemId, ship);
    const distToShip = context.camera.position.distanceTo(displayPosition);
    const billboardOpacity = context.cameraMode === "tactical"
      ? 1
      : THREE.MathUtils.clamp(
        (distToShip - SHIP_BILLBOARD_HIDE_DISTANCE) / (SHIP_BILLBOARD_FULL_DISTANCE - SHIP_BILLBOARD_HIDE_DISTANCE),
        0,
        1,
      );
    const useTacticalIcon = context.cameraMode === "tactical" || billboardOpacity > 0.01;
    const iconScale = THREE.MathUtils.clamp(
      visual.iconBaseScale,
      iconWorldScale(distToShip, context.camera, SHIP_BILLBOARD_MIN_PIXELS),
      iconWorldScale(distToShip, context.camera, SHIP_BILLBOARD_MAX_PIXELS),
    );
    visual.icon.setScaleScalar(iconScale);
    visual.icon.setOpacity(shipVisible ? billboardOpacity : 0);
    visual.mesh.setVisible(shipVisible && context.cameraMode !== "tactical" && billboardOpacity < 0.98);
    visual.icon.setVisible(shipVisible && useTacticalIcon);
    const desiredHeading = resolveShipHeading(visual, worldPosition, context.orbitYaw);
    if (desiredHeading.lengthSq() > 0.01) {
      visual.mesh.lookAt(rawObject(visual.mesh).position.clone().add(desiredHeading));
    }
  }

  for (const visual of context.nodeVisuals.values()) {
    const animatedLocalPosition = computeNodeLocalPosition(context, visual, worldTimeSeconds);
    visual.mesh.setPosition(context.toDisplayLocalPosition(animatedLocalPosition));
    visual.icon.setPosition(rawObject(visual.mesh).position.clone());
    visual.mesh.setVisible(visual.systemId === context.activeSystemId);
  }

  for (const visual of context.celestialVisuals.values()) {
    const animatedLocalPosition = computeCelestialLocalPosition(context, visual, worldTimeSeconds);
    visual.mesh.setPosition(context.toDisplayLocalPosition(animatedLocalPosition));
    visual.icon.setPosition(rawObject(visual.mesh).position.clone());
    visual.mesh.setVisible(visual.systemId === context.activeSystemId);
    const iconWorldPos = visual.icon.getWorldPosition(new THREE.Vector3());
    const distToIcon = context.camera.position.distanceTo(iconWorldPos);
    const isNearPlanetLagrange = /-l[12]$/.test(visual.id);
    const inCluster = !isNearPlanetLagrange || distToIcon < 400;
    visual.icon.setVisible(visual.systemId === context.activeSystemId && inCluster);
    const t = THREE.MathUtils.clamp(distToIcon / 300, 0, 1);
    const rawCelestialScale = visual.iconBaseScale * t * Math.sqrt(t);
    const celestialIconScale = THREE.MathUtils.clamp(rawCelestialScale, iconWorldScale(distToIcon, context.camera, 15), iconWorldScale(distToIcon, context.camera, 100));
    visual.icon.setScaleScalar(celestialIconScale);
  }

  for (const visual of context.stationVisuals.values()) {
    const animatedLocalPosition = resolveStructureAnimatedLocalPosition(context, visual, worldTimeSeconds);
    const displayPosition = context.toDisplayLocalPosition(animatedLocalPosition);
    visual.mesh.setPosition(displayPosition);
    visual.icon.setPosition(rawObject(visual.mesh).position.clone());
    const stationVisible = visual.systemId === context.activeSystemId;
    const distToStation = context.camera.position.distanceTo(displayPosition);
    const stationIconScale = THREE.MathUtils.clamp(
      130,
      iconWorldScale(distToStation, context.camera, STATION_ICON_MIN_PIXELS),
      iconWorldScale(distToStation, context.camera, STATION_ICON_MAX_PIXELS),
    );
    visual.icon.setScaleScalar(stationIconScale);
    visual.icon.setVisible(stationVisible);
    visual.mesh.setVisible(stationVisible && renderMode === "local" && context.cameraMode !== "tactical");
  }

  for (const visual of context.claimVisuals.values()) {
    const animatedLocalPosition = computeCelestialLocalPositionById(context, visual.celestialId, worldTimeSeconds) ?? visual.localPosition.clone();
    visual.mesh.setPosition(context.toDisplayLocalPosition(animatedLocalPosition));
    visual.icon.setPosition(rawObject(visual.mesh).position.clone());
    visual.mesh.setVisible(visual.systemId === context.activeSystemId);
    visual.icon.setVisible(visual.systemId === context.activeSystemId);
  }

  for (const visual of context.constructionSiteVisuals.values()) {
    const animatedLocalPosition = computeCelestialLocalPositionById(context, visual.celestialId, worldTimeSeconds) ?? visual.localPosition.clone();
    visual.mesh.setPosition(context.toDisplayLocalPosition(animatedLocalPosition));
    visual.icon.setPosition(rawObject(visual.mesh).position.clone());
    visual.mesh.setVisible(visual.systemId === context.activeSystemId);
    visual.icon.setVisible(visual.systemId === context.activeSystemId);
  }
}

type RenderSpaceMode = "galaxy" | "system" | "local";

function resolveRenderSpaceMode(activeSystemId: string | undefined, povLevel: PovLevel): RenderSpaceMode {
  if (!activeSystemId || povLevel === "galaxy") {
    return "galaxy";
  }

  return povLevel === "local" ? "local" : "system";
}

function isShipVisible(mode: RenderSpaceMode, activeSystemId: string | undefined, ship: ShipSnapshot) {
  if (ship.spatialState.movementRegime === "ftl-transit") {
    return mode === "galaxy";
  }

  if (!activeSystemId) {
    return false;
  }

  return ship.systemId === activeSystemId;
}

export function resolveShipWorldPosition(
  context: Pick<WorldPresentationContext, "world" | "toDisplayLocalPosition">,
  ship: ShipSnapshot,
  visual: ShipVisual,
  animatedLocalPosition = getAnimatedShipLocalPosition(visual),
) {
  // FTL ships are invisible in system scene; just return their last known local position.
  return context.toDisplayLocalPosition(animatedLocalPosition);
}

export function updateSystemSummaries(world: WorldState | undefined, systemSummaryVisuals: Map<string, any>) {
  if (!world) {
    return;
  }

  const shipCounts = new Map<string, number>();
  const stationCounts = new Map<string, number>();
  const structureCounts = new Map<string, number>();

  for (const ship of world.ships.values()) {
    shipCounts.set(ship.systemId, (shipCounts.get(ship.systemId) ?? 0) + 1);
  }

  for (const station of world.stations.values()) {
    stationCounts.set(station.systemId, (stationCounts.get(station.systemId) ?? 0) + 1);
    structureCounts.set(station.systemId, (structureCounts.get(station.systemId) ?? 0) + 1);
  }

  for (const node of world.nodes.values()) {
    structureCounts.set(node.systemId, (structureCounts.get(node.systemId) ?? 0) + 1);
  }

  for (const [systemId, system] of world.systems.entries()) {
    const visual = systemSummaryVisuals.get(systemId);
    if (!visual) {
      continue;
    }

    const canvas = visual.texture.image as HTMLCanvasElement;
    const context = canvas.getContext("2d");
    if (!context) {
      continue;
    }

    context.clearRect(0, 0, canvas.width, canvas.height);
    context.fillStyle = "#eaf4ff";
    context.font = "600 34px Space Grotesk, sans-serif";
    context.textAlign = "center";
    context.fillText(system.label, canvas.width / 2, 40);

    const ships = shipCounts.get(systemId) ?? 0;
    const stations = stationCounts.get(systemId) ?? 0;
    const structures = structureCounts.get(systemId) ?? 0;
    const gasClouds = [...world.nodes.values()]
      .filter((node) => node.systemId === systemId && node.sourceKind === "gas-cloud")
      .length;
    const total = ships + stations + structures;
    if (total > 0) {
      context.fillStyle = "rgba(3, 8, 18, 0.72)";
      context.fillRect(56, 64, canvas.width - 112, 68);
      context.strokeStyle = "rgba(132, 196, 255, 0.22)";
      context.strokeRect(56, 64, canvas.width - 112, 68);

      drawCountIcon(context, "ship", 126, 98, ships, "#8bc0ff");
      drawCountIcon(context, "station", 256, 98, stations, "#ffbf69");
      drawCountIcon(context, "structure", 386, 98, structures, gasClouds > 0 ? "#7fd6ff" : "#98adc4");
    }

    visual.texture.needsUpdate = true;
  }
}

export function renderRecentEvents(world: WorldState | undefined, entityKind: string, entityId: string) {
  if (!world) {
    return "";
  }

  return world.recentEvents
    .filter((event) => event.entityKind === entityKind && (!entityId || event.entityId === entityId))
    .slice(0, 8)
    .map((event) => `${new Date(event.occurredAtUtc).toLocaleTimeString()} ${event.message}`)
    .join("<br>");
}

function fmtVec(v: THREE.Vector3, digits: number) {
  return `${v.x.toFixed(digits)} ${v.y.toFixed(digits)} ${v.z.toFixed(digits)}`;
}

export function describeGameStatus(params: GameStatusParams) {
  const { world, activeSystemId, cameraMode, povLevel, selectedItems, mode, galaxyAnchor, systemAnchor } = params;
  const sequence = world?.sequence ?? 0;
  const generatedAt = world?.generatedAtUtc
    ? new Date(world.generatedAtUtc).toLocaleTimeString()
    : "n/a";
  const displayPovLevel = activeSystemId ? povLevel : "galaxy";
  const activeSpace = describeActiveSpace(world, displayPovLevel, activeSystemId, selectedItems);
  const cameraModeLabel = cameraMode === "follow" ? "follow" : "map";

  // Galaxy space: galaxyAnchor in light-years — changes only during galaxy navigation
  const galPos = galaxyAnchor
    ? `gal pos:    ${fmtVec(galaxyAnchor.clone().divideScalar(DISPLAY_UNITS_PER_LIGHT_YEAR), 2)} ly`
    : "";
  // System space: systemAnchor in AU — changes only during system navigation
  const sysPos = systemAnchor
    ? `sys pos:    ${fmtVec(systemAnchor.clone().divideScalar(KILOMETERS_PER_AU), 3)} AU`
    : "";
  // Local space: position relative to the focused celestial's orbital anchor in km
  const focusedCelestialId = resolveFocusedCelestialId(world, selectedItems);
  const celestialAnchor = focusedCelestialId
    ? world?.celestials.get(focusedCelestialId)?.orbitalAnchor
    : undefined;
  const locPos = systemAnchor && celestialAnchor
    ? `loc pos:    ${fmtVec(systemAnchor.clone().sub(toThreeVector(celestialAnchor)), 0)} km`
    : "";

  return {
    bodyText: [
      `mode:       ${mode}`,
      `camera:     ${cameraModeLabel}`,
      `zoom:       ${displayPovLevel}`,
      `space:      ${activeSpace}`,
      galPos,
      sysPos,
      locPos,
      `sequence:   ${sequence}`,
      `snapshot:   ${generatedAt}`,
    ].filter(Boolean).join("\n"),
    summaryText: `${mode} | ${displayPovLevel} | ${activeSpace}`,
  };
}

export function updateGameStatus(params: GameStatusParams & { statusEl: HTMLDivElement; summaryEl?: HTMLSpanElement }) {
  const state = describeGameStatus(params);
  params.statusEl.textContent = state.bodyText;
  if (params.summaryEl) {
    params.summaryEl.textContent = state.summaryText;
  }
}

export function deriveNodeOrbital(
  context: WorldOrbitalContext,
  node: ResourceNodeSnapshot | ResourceNodeDelta,
  anchor: OrbitalAnchor,
) {
  return deriveOrbitalFromLocalPosition(context, toThreeVector(node.localPosition), node.systemId, anchor);
}

export function deriveOrbitalFromLocalPosition(
  context: WorldOrbitalContext,
  localPosition: THREE.Vector3,
  systemId: string,
  anchor: OrbitalAnchor,
) {
  const anchorPosition = getOrbitalAnchorPosition(context, systemId, anchor, currentWorldTimeSeconds(context.world, context.worldTimeSyncMs));
  const relativePosition = localPosition.clone().sub(anchorPosition);
  const radius = Math.max(Math.sqrt((relativePosition.x * relativePosition.x) + (relativePosition.z * relativePosition.z)), 24);
  const phase = Math.atan2(relativePosition.z, relativePosition.x);
  const inclination = Math.atan2(relativePosition.y, radius);
  return { radius, phase, inclination };
}

export function computeNodeLocalPosition(context: WorldOrbitalContext, node: NodeVisual, timeSeconds: number) {
  const speed = computeNodeOrbitSpeed(node);
  const angle = node.orbitPhase + (timeSeconds * speed);
  const orbit = new THREE.Vector3(
    Math.cos(angle) * node.orbitRadius,
    0,
    Math.sin(angle) * node.orbitRadius,
  );
  orbit.applyAxisAngle(new THREE.Vector3(1, 0, 0), node.orbitInclination);
  return orbit.add(getOrbitalAnchorPosition(context, node.systemId, node.anchor, timeSeconds));
}

export function resolveOrbitalAnchor(context: WorldOrbitalContext, systemId: string, localPosition: THREE.Vector3): OrbitalAnchor {
  if (!context.world) {
    return { kind: "star" };
  }

  const system = context.world.systems.get(systemId);
  if (!system) {
    return { kind: "star" };
  }

  const nowSeconds = currentWorldTimeSeconds(context.world, context.worldTimeSyncMs);
  let bestAnchor: OrbitalAnchor = { kind: "star" };
  let bestDistance = Number.POSITIVE_INFINITY;

  for (const [planetIndex, planet] of system.planets.entries()) {
    const planetPosition = computePlanetLocalPosition(planet, nowSeconds);
    const planetDistance = localPosition.distanceTo(planetPosition);
    const planetThreshold = Math.max(planet.size * 10, 180);
    if (planetDistance < planetThreshold && planetDistance < bestDistance) {
      bestDistance = planetDistance;
      bestAnchor = { kind: "planet", planetIndex };
    }

    for (const [moonIndex, moon] of planet.moons.entries()) {
      const moonPosition = planetPosition
        .clone()
        .add(computeMoonLocalPosition(moon, nowSeconds));
      const moonDistance = localPosition.distanceTo(moonPosition);
      const moonThreshold = Math.max(moon.size * 14, 80);
      if (moonDistance < moonThreshold && moonDistance < bestDistance) {
        bestDistance = moonDistance;
        bestAnchor = { kind: "moon", planetIndex, moonIndex };
      }
    }
  }

  return bestAnchor;
}

export function resolvePointPosition(context: WorldOrbitalContext, _systemId: string, celestialId?: string | null) {
  if (celestialId) {
    const celestial = context.world?.celestials.get(celestialId);
    if (celestial) {
      return toThreeVector(celestial.orbitalAnchor);
    }
  }

  return new THREE.Vector3(0, 0, 0);
}

export function computeCelestialLocalPosition(context: WorldOrbitalContext, visual: CelestialVisual, timeSeconds: number) {
  return computeCelestialLocalPositionById(context, visual.id, timeSeconds) ?? visual.orbitalAnchor.clone();
}

export function computeCelestialLocalPositionById(
  context: WorldOrbitalContext,
  celestialId: string,
  timeSeconds: number,
  visiting = new Set<string>(),
): THREE.Vector3 | undefined {
  if (!context.world || visiting.has(celestialId)) {
    return undefined;
  }

  const celestial = context.world.celestials.get(celestialId);
  if (!celestial) {
    return undefined;
  }

  const basePosition = toThreeVector(celestial.orbitalAnchor);
  if (!celestial.parentNodeId) {
    return basePosition;
  }

  const parentCelestial = context.world.celestials.get(celestial.parentNodeId);
  if (!parentCelestial) {
    return basePosition;
  }

  visiting.add(celestialId);
  const parentCurrentPosition = computeCelestialLocalPositionById(context, celestial.parentNodeId, timeSeconds, visiting);
  visiting.delete(celestialId);
  if (!parentCurrentPosition) {
    return basePosition;
  }

  const parentInitialPosition = toThreeVector(parentCelestial.orbitalAnchor);
  const relativeOffset = basePosition.clone().sub(parentInitialPosition);
  const initialDir = parentInitialPosition.clone().normalize();
  const currentDir = parentCurrentPosition.clone().normalize();
  let rotatedOffset: THREE.Vector3;
  if (initialDir.lengthSq() > 0.0001 && currentDir.lengthSq() > 0.0001) {
    const quaternion = new THREE.Quaternion().setFromUnitVectors(initialDir, currentDir);
    rotatedOffset = relativeOffset.clone().applyQuaternion(quaternion);
  } else {
    rotatedOffset = relativeOffset.clone();
  }
  return parentCurrentPosition.clone().add(rotatedOffset);
}

function drawCountIcon(
  context: CanvasRenderingContext2D,
  kind: SummaryIconKind,
  x: number,
  y: number,
  value: number,
  color: string,
) {
  context.save();
  context.strokeStyle = color;
  context.fillStyle = color;
  context.lineWidth = 3;

  if (kind === "ship") {
    context.beginPath();
    context.moveTo(x - 14, y + 10);
    context.lineTo(x, y - 14);
    context.lineTo(x + 14, y + 10);
    context.closePath();
    context.stroke();
  } else if (kind === "station") {
    context.strokeRect(x - 14, y - 14, 28, 28);
  } else {
    context.beginPath();
    context.arc(x, y, 14, 0, Math.PI * 2);
    context.stroke();
    context.beginPath();
    context.moveTo(x - 8, y);
    context.lineTo(x + 8, y);
    context.moveTo(x, y - 8);
    context.lineTo(x, y + 8);
    context.stroke();
  }

  context.fillStyle = "#eaf4ff";
  context.font = "600 26px IBM Plex Mono, monospace";
  context.textAlign = "left";
  context.fillText(String(value), x + 24, y + 9);
  context.restore();
}

function computeNodeOrbitSpeed(node: NodeVisual) {
  const base = node.sourceKind === "gas-cloud" ? 0.16 : 0.24;
  return base / Math.sqrt(Math.max(node.orbitRadius / 140, 0.4));
}

function computeStructureLocalPosition(
  context: WorldOrbitalContext,
  structure: StructureVisual,
  timeSeconds: number,
  baseSpeed: number,
) {
  const angle = structure.orbitPhase + (timeSeconds * (baseSpeed / Math.sqrt(Math.max(structure.orbitRadius / 180, 0.45))));
  const orbit = new THREE.Vector3(
    Math.cos(angle) * structure.orbitRadius,
    0,
    Math.sin(angle) * structure.orbitRadius,
  );
  orbit.applyAxisAngle(new THREE.Vector3(1, 0, 0), structure.orbitInclination);
  return orbit.add(getOrbitalAnchorPosition(context, structure.systemId, structure.anchor, timeSeconds));
}

function getOrbitalAnchorPosition(context: WorldOrbitalContext, systemId: string, anchor: OrbitalAnchor, timeSeconds: number) {
  return resolveOrbitalAnchorPosition(context.world, systemId, anchor, timeSeconds);
}

function resolveStructureAnimatedLocalPosition(context: WorldOrbitalContext, visual: StructureVisual, timeSeconds: number) {
  if (!context.world) {
    return visual.localPosition.clone();
  }

  const station = context.world.stations.get(visual.id);
  if (!station?.celestialId) {
    return computeStructureLocalPosition(context, visual, timeSeconds, 0.14);
  }

  return computeCelestialLocalPositionById(context, station.celestialId, timeSeconds) ?? visual.localPosition.clone();
}
