import spaceJson from "../data/space-logistics.json" with { type: "json" };

import {
  calculateBeaconDesign,
  calculateRocketSiloBeaconDesign,
} from "./beacons.ts";
import { beaconEffects, effectsForModules } from "./calculator.ts";
import {
  CATALOG_META,
  MACHINE_BY_ID,
  QUALITY_BY_ID,
  RECIPE_BY_ID,
  catalogPlacement,
  displayName,
  iconFor,
} from "./factorio-data.ts";
import { inserterPowerKw, itemThroughput } from "./logistics.ts";
import type { RocketFreightInput, RocketFreightResult } from "./types.ts";

type SpaceData = {
  rocketLiftWeight: number;
  rocketInventorySlots: number;
  rocketPartsRequired: number;
  rocketPartSeconds: number;
  launchToPlatformSeconds: number;
  cargoPodLandingSeconds: number;
  cargoLandingPadHatches: number;
  spacePlatformHubSlots: number;
  cargoBaySlots: number;
  connections: Array<{
    id: string;
    from: string;
    to: string;
    lengthKm: number;
  }>;
  thruster: {
    minFluid: number;
    maxFluid: number;
    minUsagePerSecond: number;
    maxUsagePerSecond: number;
    minThrustMn: number;
    maxThrustMn: number;
  };
};
const data = spaceJson as SpaceData;
const silo = MACHINE_BY_ID.get("rocket-silo")!;
const rocketPart = RECIPE_BY_ID.get("rocket-part")!;
const QUALITY_CYCLE_SECONDS = {
  normal: 1614 / 60,
  uncommon: 1423 / 60,
  rare: 1301 / 60,
  epic: 1218 / 60,
  legendary: 1097 / 60,
} as const;
const LOGISTIC_ROBOT_BASE_SPEED = 3;
const ROBOPORT_TO_CHEST_DISTANCE = 4.5;
const ROBOPORT_DRAIN_KW = 50;

function workerRobotSpeedBonus(level: number): number {
  const researchLevel = Math.max(0, Math.floor(level));
  const fixed = [0, 0.35, 0.75, 1.2, 1.75, 2.4];
  if (researchLevel < fixed.length) return fixed[researchLevel];
  return fixed[5] + (researchLevel - 5) * 0.65;
}

export const ROCKET_LOCATIONS = [
  ...new Set(
    data.connections.flatMap((connection) => [connection.from, connection.to]),
  ),
];
export const ROCKET_PRODUCTS = Object.entries(CATALOG_META.items)
  .map(([id, item]) => {
    const placement = catalogPlacement(id, "item");
    return {
      id,
      name: displayName(id),
      icon: iconFor(id),
      stackSize: item.stackSize,
      weight: item.weight,
      group: placement.group,
      subgroup: placement.subgroup,
      order: placement.order,
      capacity: Math.max(
        0,
        Math.min(
          Math.floor(data.rocketLiftWeight / Math.max(item.weight, 1)),
          item.stackSize * data.rocketInventorySlots,
        ),
      ),
    };
  })
  .filter((product) => product.capacity > 0)
  .sort((a, b) =>
    (CATALOG_META.groups[a.group]?.order ?? "").localeCompare(
      CATALOG_META.groups[b.group]?.order ?? "",
    ) ||
    (CATALOG_META.subgroups[a.subgroup ?? ""]?.order ?? "").localeCompare(
      CATALOG_META.subgroups[b.subgroup ?? ""]?.order ?? "",
    ) ||
    (a.order ?? a.id).localeCompare(b.order ?? b.id),
  );
export const ROCKET_PRODUCT_BY_ID = new Map(
  ROCKET_PRODUCTS.map((product) => [product.id, product]),
);

function shortestPath(
  from: string,
  to: string,
): { distanceKm: number; path: string[] } | undefined {
  if (from === to) return { distanceKm: 0, path: [from] };
  const unvisited = new Set(ROCKET_LOCATIONS);
  const distances = new Map(
    ROCKET_LOCATIONS.map((node) => [node, node === from ? 0 : Infinity]),
  );
  const previous = new Map<string, string>();
  while (unvisited.size) {
    const current = [...unvisited].sort(
      (a, b) => (distances.get(a) ?? Infinity) - (distances.get(b) ?? Infinity),
    )[0];
    unvisited.delete(current);
    if (current === to || !Number.isFinite(distances.get(current))) break;
    for (const connection of data.connections) {
      const next =
        connection.from === current
          ? connection.to
          : connection.to === current
            ? connection.from
            : undefined;
      if (!next || !unvisited.has(next)) continue;
      const candidate =
        (distances.get(current) ?? Infinity) + connection.lengthKm;
      if (candidate < (distances.get(next) ?? Infinity)) {
        distances.set(next, candidate);
        previous.set(next, current);
      }
    }
  }
  const distanceKm = distances.get(to) ?? Infinity;
  if (!Number.isFinite(distanceKm)) return undefined;
  const path = [to];
  while (path[0] !== from) path.unshift(previous.get(path[0])!);
  return { distanceKm, path };
}

function thrusterPerformance(input: RocketFreightInput) {
  const fill = Math.max(0, Math.min(1, input.thrusterFill));
  const t = Math.max(
    0,
    Math.min(
      1,
      (fill - data.thruster.minFluid) /
        (data.thruster.maxFluid - data.thruster.minFluid),
    ),
  );
  const relativeUsage = 0.1 + 1.9 * t;
  const efficiency = 1 - 0.49 * t;
  const relativeThrust = (relativeUsage * efficiency) / (2 * 0.51);
  const quality = QUALITY_BY_ID.get(input.thrusterQuality)?.multiplier ?? 1;
  const thrustMn =
    input.thrusterCount *
    data.thruster.maxThrustMn *
    1000 *
    quality *
    relativeThrust;
  const impulse = thrustMn / 60;
  const width = Math.max(1, input.platformWidthTiles);
  const mass = Math.max(1, input.platformMassTons);
  const inside = ((480000 * impulse) / (mass + 10000) - 480) / width + 9;
  const speedParameter = Math.max(10, 10 * Math.sqrt(Math.max(0, inside)) - 30);
  const firstHalfSpeed = Math.max(0.01, speedParameter - 10);
  const secondHalfSpeed = Math.max(0.01, speedParameter + 10);
  const usage =
    (input.thrusterCount *
      data.thruster.maxUsagePerSecond *
      quality *
      relativeUsage) /
    2;
  return {
    speedKms: (firstHalfSpeed + secondHalfSpeed) / 2,
    firstHalfSpeed,
    secondHalfSpeed,
    usage,
  };
}

export function calculateRocketFreight(
  input: RocketFreightInput,
): RocketFreightResult {
  const loadingMode = input.loadingMode ?? "robots";
  const loadingInserterId = input.loadingInserterId ?? "stack-inserter";
  const loadingInserterQuality = input.loadingInserterQuality ?? "normal";
  const loadingInserterCapacityBonus = input.loadingInserterCapacityBonus ?? 7;
  const cargoBayQuality =
    QUALITY_BY_ID.get(input.cargoBayQuality ?? "normal") ??
    QUALITY_BY_ID.get("normal")!;
  const cargoBaySlotsEach = Math.round(
    data.cargoBaySlots * cargoBayQuality.multiplier,
  );
  const product = ROCKET_PRODUCT_BY_ID.get(input.productId);
  const route = shortestPath(input.source, input.destination);
  if (!product || !route || input.source === input.destination) {
    return {
      valid: false,
      message:
        "Select a launchable item and two different connected locations.",
      distanceKm: 0,
      path: [],
      speedKms: 0,
      flightSeconds: 0,
      roundTripSeconds: 0,
      cargoCapacityPerRocket: 0,
      launchesPerTransit: 0,
      launchesPerMinute: 0,
      siloCycleSeconds: 0,
      rocketPartCraftSeconds: 0,
      sustainedSilos: 0,
      waveSilos: 0,
      silosToPlace: 0,
      speedLimitedSilos: 0,
      payloadWaveSilos: 0,
      siloBottleneck: "throughput",
      loadingSeconds: 0,
      payloadInsertionSeconds: 0,
      payloadInsertionRate: 0,
      inserterInsertionSeconds: 0,
      robotFlightSeconds: 0,
      robotSupplySeconds: 0,
      robotSupplyRate: 0,
      robotCargoCapacity: 0,
      logisticRobotCount: 0,
      landingSeconds: 0,
      landingWaves: 0,
      platformsRequired: 0,
      requestedCargoPerTransit: 0,
      effectiveCargoPerTransit: 0,
      platformCargoCapacity: 0,
      cargoBaySlotsEach,
      minimumCargoBays: 0,
      cargoStorageLimited: false,
      rocketPartIngredientsPerSecond: [],
      thrusterFuelPerSecond: 0,
      thrusterOxidizerPerSecond: 0,
      electricKw: 0,
      siloLayout: calculateBeaconDesign(silo, 0, 0),
      notes: [],
    };
  }
  const perSecond =
    Math.max(0, input.targetRate) /
    (input.unit === "s" ? 1 : input.unit === "min" ? 60 : 3600);
  const requestedCargoPerTransit = Math.max(1, input.cargoPerTransit);
  const cargoBayCount = Math.max(0, Math.floor(input.cargoBays));
  const platformCargoSlots =
    data.spacePlatformHubSlots + cargoBayCount * cargoBaySlotsEach;
  const platformCargoCapacity = platformCargoSlots * product.stackSize;
  const cargoPerTransit = Math.min(
    requestedCargoPerTransit,
    platformCargoCapacity,
  );
  const cargoStorageLimited = cargoPerTransit < requestedCargoPerTransit;
  const requestedSlots = Math.ceil(requestedCargoPerTransit / product.stackSize);
  const minimumCargoBays = Math.max(
    0,
    Math.ceil(
      (requestedSlots - data.spacePlatformHubSlots) / cargoBaySlotsEach,
    ),
  );
  const launchesPerTransit = Math.ceil(cargoPerTransit / product.capacity);
  const transitRate = perSecond / cargoPerTransit;
  const launchRate = transitRate * launchesPerTransit;
  const machineEffects = effectsForModules(input.siloModules, silo, rocketPart);
  const maximumBeacons = Math.max(
    0,
    silo.maxBeacons - (loadingMode === "ignore" ? 0 : 1),
  );
  const beaconCount = Math.max(
    0,
    Math.min(maximumBeacons, Math.floor(input.beaconCount)),
  );
  const beaconFx = beaconEffects(
    beaconCount,
    input.beaconModules,
    silo,
    rocketPart,
    input.beaconQuality,
  );
  const quality =
    QUALITY_BY_ID.get(input.siloQuality) ?? QUALITY_BY_ID.get("normal")!;
  const speed =
    silo.speed *
    quality.multiplier *
    Math.max(0.2, 1 + machineEffects.speed + beaconFx.speed);
  const productivityBonus = Math.min(
    3,
    Math.max(
      0,
      machineEffects.productivity + input.rocketPartProductivityLevel * 0.1,
    ),
  );
  const craftsPerRocket = data.rocketPartsRequired / (1 + productivityBonus);
  const rocketPartCraftSeconds =
    (craftsPerRocket * data.rocketPartSeconds) / Math.max(speed, 1e-12);
  const baseSiloCycleSeconds = Math.max(
    QUALITY_CYCLE_SECONDS[input.siloQuality],
    rocketPartCraftSeconds,
  );
  const averageCargoPerLaunch = cargoPerTransit / launchesPerTransit;
  const usesPhysicalLoading = loadingMode !== "ignore";
  const inserterRate = usesPhysicalLoading
    ? itemThroughput({
        inserterId: loadingInserterId,
        inserterQuality: loadingInserterQuality,
        capacityBonus: Math.max(0, Math.floor(loadingInserterCapacityBonus)),
        handoff: "container-machine",
      })
    : 0;
  const inserterInsertionSeconds =
    inserterRate > 0 ? averageCargoPerLaunch / inserterRate : 0;
  const logisticRobotCount =
    loadingMode === "robots"
      ? Math.max(1, Math.min(300, Math.floor(input.logisticRobotCount ?? 50)))
      : 0;
  const robotCargoCapacity =
    loadingMode === "robots"
      ? 1 + Math.max(0, Math.min(3, Math.floor(input.workerRobotCargoLevel ?? 0)))
      : 0;
  const robotSpeed =
    loadingMode === "robots"
      ? LOGISTIC_ROBOT_BASE_SPEED *
        (1 + workerRobotSpeedBonus(input.workerRobotSpeedLevel ?? 0))
      : 0;
  const robotFlightSeconds =
    robotSpeed > 0 ? ROBOPORT_TO_CHEST_DISTANCE / robotSpeed : 0;
  const robotWaves =
    loadingMode === "robots"
      ? Math.max(
          1,
          Math.ceil(
            averageCargoPerLaunch /
              Math.max(1, logisticRobotCount * robotCargoCapacity),
          ),
        )
      : 0;
  const robotSupplySeconds =
    loadingMode === "robots"
      ? robotFlightSeconds + Math.max(0, robotWaves - 1) * robotFlightSeconds * 2
      : 0;
  const robotSupplyRate =
    robotSupplySeconds > 0 ? averageCargoPerLaunch / robotSupplySeconds : 0;
  const payloadInsertionSeconds =
    loadingMode === "robots"
      ? Math.max(inserterInsertionSeconds, robotSupplySeconds)
      : loadingMode === "inserters"
        ? inserterInsertionSeconds
        : 0;
  const payloadInsertionRate =
    payloadInsertionSeconds > 0
      ? averageCargoPerLaunch / payloadInsertionSeconds
      : 0;
  // Payload insertion is intentionally conservative: it is serialized after
  // a rocket is prepared. A preloaded wave can hide the first fill from the
  // arriving platform, but not from sustained silo throughput.
  const siloCycleSeconds = baseSiloCycleSeconds + payloadInsertionSeconds;
  const sustainedSilos =
    launchRate > 0
      ? Math.max(1, Math.ceil(launchRate * siloCycleSeconds - 1e-12))
      : 0;
  const waveSilos = Math.max(sustainedSilos, launchesPerTransit);
  const siloBottleneck =
    launchesPerTransit >= sustainedSilos && launchesPerTransit > 0
      ? "payload-wave"
      : "throughput";
  const loadingSeconds =
    data.launchToPlatformSeconds +
    Math.max(0, Math.ceil(launchesPerTransit / Math.max(1, waveSilos)) - 1) *
      siloCycleSeconds;
  const stacks = Math.ceil(cargoPerTransit / product.stackSize);
  const landingWaves = Math.ceil(
    stacks /
      (data.cargoLandingPadHatches + cargoBayCount),
  );
  const landingSeconds = landingWaves * data.cargoPodLandingSeconds;
  const thruster = thrusterPerformance(input);
  const speedKms =
    input.speedMode === "manual"
      ? Math.max(0.01, input.manualSpeedKms)
      : thruster.speedKms;
  const flightSeconds =
    input.speedMode === "manual"
      ? route.distanceKm / speedKms
      : route.distanceKm / 2 / thruster.firstHalfSpeed +
        route.distanceKm / 2 / thruster.secondHalfSpeed;
  const roundTripSeconds = flightSeconds * 2 + loadingSeconds + landingSeconds;
  const platformsRequired =
    transitRate > 0
      ? Math.max(1, Math.ceil(transitRate * roundTripSeconds - 1e-12))
      : 0;
  const ingredientsPerSecond = launchRate * craftsPerRocket;
  const siloLayout = calculateRocketSiloBeaconDesign(
    silo,
    waveSilos,
    beaconCount,
    { mode: loadingMode, inserterId: loadingInserterId },
  );
  const energyModifier = Math.max(
    0.2,
    1 + machineEffects.consumption + beaconFx.consumption,
  );
  const beaconQuality =
    QUALITY_BY_ID.get(input.beaconQuality) ?? QUALITY_BY_ID.get("normal")!;
  const electricKw =
    waveSilos *
      (silo.energyKw * energyModifier + (silo.auxiliaryActiveKw ?? 0)) +
    siloLayout.uniqueBeacons * 480 * beaconQuality.beaconPowerMultiplier +
    (usesPhysicalLoading
      ? waveSilos * inserterPowerKw(loadingInserterId)
      : 0) +
    (loadingMode === "robots" ? waveSilos * ROBOPORT_DRAIN_KW : 0);
  return {
    valid: true,
    distanceKm: route.distanceKm,
    path: route.path,
    speedKms,
    flightSeconds,
    roundTripSeconds,
    cargoCapacityPerRocket: product.capacity,
    launchesPerTransit,
    launchesPerMinute: launchRate * 60,
    siloCycleSeconds,
    rocketPartCraftSeconds,
    sustainedSilos,
    waveSilos,
    silosToPlace: waveSilos,
    speedLimitedSilos: sustainedSilos,
    payloadWaveSilos: launchesPerTransit,
    siloBottleneck,
    loadingSeconds,
    payloadInsertionSeconds,
    payloadInsertionRate,
    inserterInsertionSeconds,
    robotFlightSeconds,
    robotSupplySeconds,
    robotSupplyRate,
    robotCargoCapacity,
    logisticRobotCount,
    landingSeconds,
    landingWaves,
    platformsRequired,
    requestedCargoPerTransit,
    effectiveCargoPerTransit: cargoPerTransit,
    platformCargoCapacity,
    cargoBaySlotsEach,
    minimumCargoBays,
    cargoStorageLimited,
    rocketPartIngredientsPerSecond: [
      "processing-unit",
      "low-density-structure",
      "rocket-fuel",
    ].map((id) => ({
      id,
      amount: ingredientsPerSecond,
      icon: iconFor(id),
    })),
    thrusterFuelPerSecond: input.speedMode === "thrusters" ? thruster.usage : 0,
    thrusterOxidizerPerSecond:
      input.speedMode === "thrusters" ? thruster.usage : 0,
    electricKw,
    siloLayout,
    notes: [
      "Optimal loading assumes every required rocket is already built and preloaded when the platform arrives.",
      "Thruster-derived speed assumes a compact platform; width affects top speed far more than mass.",
      ...(siloBottleneck === "payload-wave"
        ? [
            `payload-wave limit: this cargo batch needs ${launchesPerTransit} rockets at once. The calculator therefore reserves ${launchesPerTransit} preloaded silos; the sustained launch rate only needs ${sustainedSilos}. Speed upgrades begin reducing placement only after sustained throughput becomes the larger requirement, or after the cargo batch is reduced.`,
          ]
        : []),
      ...(cargoStorageLimited
        ? [
            `Platform storage is limited to ${platformCargoCapacity} items with ${cargoBayCount} cargo bays; ${minimumCargoBays} cargo bays are required for the requested ${requestedCargoPerTransit}-item batch.`,
          ]
        : []),
      ...(loadingMode === "inserters"
        ? [
            "Experimental inserter loading uses a full buffer chest and one inserter per silo, reserves the bottom-centre beacon position, limits each silo to 15 beacons, and adds insertion time to sustained launch throughput.",
          ]
        : []),
      ...(loadingMode === "robots"
        ? [
            `Experimental robot loading assumes a full local buffer chest, ${logisticRobotCount} robots in the displayed roboport and only the ${ROBOPORT_TO_CHEST_DISTANCE}-tile local flight leg. Upstream provider distance, congestion and charging queues are not included.`,
          ]
        : []),
      ...(loadingMode === "ignore"
        ? [
            "Insertion is ignored: cargo is treated as already inside each prepared rocket. No chest, inserter or robot loading time is included, and the full 16-beacon silo ring remains available.",
          ]
        : []),
    ],
  };
}
