import { QUALITY_BY_ID, iconFor } from "./factorio-data.ts";
import type {
  BeaconLayoutResult,
  EnergyInput,
  EnergyResult,
  TilePlacement,
} from "./types.ts";

const entity = (id: string, amount: number, detail?: string) => ({
  id,
  name: id,
  icon: iconFor(id),
  amount,
  detail,
});
const resource = (id: string, perSecond: number) => ({
  id,
  name: id,
  icon: iconFor(
    id,
    id.includes("water") || id.includes("fluoroketone") ? "fluid" : "item",
  ),
  perSecond,
});
const ceil = (value: number) => Math.max(0, Math.ceil(value - 1e-12));
const STEAM_500_MJ = 0.097;
const WATER_PER_MW_500 = 1 / STEAM_500_MJ / 10;
const ENGINE_500_EFFICIENCY = 0.03 / STEAM_500_MJ;
const MAX_DRAWN_THERMAL_MACHINES = 240;
type SteamGenerator = NonNullable<EnergyInput["steamGenerator"]>;

function blueprint(
  placements: TilePlacement[],
  label: string,
  extrapolated = false,
): BeaconLayoutResult {
  const minX = placements.length ? Math.min(...placements.map((p) => p.x)) : 0;
  const maxX = placements.length
    ? Math.max(...placements.map((p) => p.x + p.width))
    : 1;
  const minY = placements.length ? Math.min(...placements.map((p) => p.y)) : 0;
  const maxY = placements.length
    ? Math.max(...placements.map((p) => p.y + p.height))
    : 1;
  return {
    requestedPerMachine: 0,
    machineCount: 0,
    counts: [],
    average: 0,
    min: 0,
    max: 0,
    uniqueBeacons: 0,
    exact: !extrapolated,
    extrapolated,
    corridor: 0,
    placements: placements.map((placement, index) => ({
      ...placement,
      label: placement.label ?? `${label} ${index + 1}`,
    })),
    bounds: { minX, maxX, minY, maxY },
  };
}

const placed = (
  entityId: string,
  x: number,
  y: number,
  width: number,
  height: number,
  label?: string,
  tone: TilePlacement["tone"] = "power",
  direction?: TilePlacement["direction"],
): TilePlacement => ({
  kind: "entity",
  entityId,
  x,
  y,
  width,
  height,
  label,
  tone,
  direction,
});

function steamBlueprint(
  generator: SteamGenerator,
): BeaconLayoutResult {
  const generators = generator === "steam-engine" ? 2 : 1;
  const placements: TilePlacement[] = [
    placed("boiler", 0, 2, 3, 2, "1 × Boiler", "heat"),
  ];
  for (let index = 0; index < generators; index += 1)
    placements.push(
      placed(
        generator,
        5 + index * 5,
        1,
        5,
        3,
        `${generators} × ${generator}`,
        "power",
      ),
    );
  return blueprint(placements, "Boiler steam block");
}

function solarBlueprint(accumulatorRatio: number): BeaconLayoutResult {
  const placements: TilePlacement[] = [];
  for (let index = 0; index < 25; index += 1)
    placements.push(
      placed("solar-panel", (index % 5) * 3, Math.floor(index / 5) * 3, 3, 3),
    );
  const accumulatorCount = Math.round(25 * accumulatorRatio);
  for (let index = 0; index < accumulatorCount; index += 1)
    placements.push(
      placed(
        "accumulator",
        17 + (index % 7) * 2,
        Math.floor(index / 7) * 2 + 4,
        2,
        2,
      ),
    );
  return blueprint(
    placements,
    accumulatorCount ? `25 : ${accumulatorCount}` : "continuous sunlight",
  );
}

function thermalBanks(
  placements: TilePlacement[],
  startY: number,
  exchangers: number,
  generators: number,
  generator: SteamGenerator,
) {
  const drawnExchangers = Math.min(exchangers, MAX_DRAWN_THERMAL_MACHINES);
  const drawnGenerators = Math.min(generators, MAX_DRAWN_THERMAL_MACHINES);
  const bankRows = Math.max(
    1,
    Math.ceil(drawnExchangers / 12),
    Math.ceil(drawnGenerators / 18),
  );
  const distribute = (total: number) => {
    let remaining = total;
    return Array.from({ length: bankRows }, (_, row) => {
      const amount = Math.ceil(remaining / (bankRows - row));
      remaining -= amount;
      return amount;
    });
  };
  const exchangersByRow = distribute(drawnExchangers);
  const generatorsByRow = distribute(drawnGenerators);
  let exchangerIndex = 0;
  let generatorIndex = 0;
  for (let row = 0; row < bankRows; row += 1) {
    const rowExchangers = exchangersByRow[row];
    const rowGenerators = generatorsByRow[row];
    const generatorY = startY + row * 8;
    const exchangerY = generatorY + 5;
    for (let column = 0; column < rowGenerators; column += 1) {
      placements.push(
        placed(
          generator,
          column * 3,
          generatorY,
          3,
          5,
          generatorIndex === 0 ? `${generators} × ${generator}` : undefined,
          "power",
        ),
      );
      generatorIndex += 1;
    }
    for (let column = 0; column < rowExchangers; column += 1) {
      placements.push(
        placed(
          "heat-exchanger",
          column * 3,
          exchangerY,
          3,
          2,
          exchangerIndex === 0
            ? `${exchangers} × Heat exchanger`
            : undefined,
          "heat",
        ),
      );
      exchangerIndex += 1;
    }
  }
  return exchangers > drawnExchangers || generators > drawnGenerators;
}

function nuclearBlueprint(
  width: number,
  height: number,
  exchangers: number,
  generators: number,
  generator: SteamGenerator,
): BeaconLayoutResult {
  const placements: TilePlacement[] = [];
  for (let y = 0; y < height; y += 1)
    for (let x = 0; x < width; x += 1)
      placements.push(
        placed(
          "nuclear-reactor",
          x * 5,
          y * 5,
          5,
          5,
          `Reactor ${x + 1},${y + 1}`,
          "heat",
        ),
      );
  const capped = thermalBanks(
    placements,
    height * 5 + 2,
    exchangers,
    generators,
    generator,
  );
  return blueprint(placements, `${width}×${height} nuclear block`, capped);
}

function heatingBlueprint(
  exchangers: number,
  generators: number,
  generator: SteamGenerator,
): BeaconLayoutResult {
  const placements: TilePlacement[] = [
    placed("heating-tower", 0, 0, 5, 5, "1 × Heating tower", "heat"),
  ];
  const capped = thermalBanks(
    placements,
    7,
    exchangers,
    generators,
    generator,
  );
  return blueprint(placements, "Heating tower block", capped);
}

function fusionBlueprint(
  reactors: number,
  generators: number,
): BeaconLayoutResult {
  const placements: TilePlacement[] = [];
  const reactorPositions = [
    { x: 0, y: 5 },
    { x: 6, y: 5 },
    { x: 3, y: 11 },
    { x: 9, y: 11 },
  ].slice(0, reactors);
  reactorPositions.forEach((position, index) =>
    placements.push(
      placed(
        "fusion-reactor",
        position.x,
        position.y,
        6,
        6,
        `Reactor ${index + 1}`,
        "heat",
      ),
    ),
  );
  const reactorBottom = Math.max(
    ...reactorPositions.map((position) => position.y + 6),
  );
  const columns = Math.min(14, generators);
  const generatorStartY = reactorBottom + 3;
  for (let index = 0; index < generators; index += 1) {
    const column = index % columns;
    const row = Math.floor(index / columns);
    placements.push(
      placed(
        "fusion-generator",
        column * 3,
        generatorStartY + row * 5,
        3,
        5,
        index === 0 ? `${generators} × Fusion generator` : undefined,
        "power",
      ),
    );
  }
  const generatorRows = Math.ceil(generators / columns);
  const cryoY = generatorStartY + generatorRows * 5 + 3;
  for (let index = 0; index < reactors; index += 1) {
    placements.push(
      placed(
        "cryogenic-plant",
        index * 5,
        cryoY,
        5,
        5,
        index === 0 ? `${reactors} × Cryogenic plant` : undefined,
        "fluid",
      ),
    );
  }
  return blueprint(placements, `${reactors} reactor fusion block`);
}

const SOLAR = {
  nauvis: { multiplier: 1, average: 0.7, accumulatorRatio: 0.84672 },
  vulcanus: { multiplier: 4, average: 0.7, accumulatorRatio: 0.72576 },
  gleba: { multiplier: 0.5, average: 0.7, accumulatorRatio: 0.6048 },
  fulgora: { multiplier: 0.2, average: 0.7, accumulatorRatio: 0.072576 },
  aquilo: { multiplier: 0.01, average: 0.7, accumulatorRatio: 0.024192 },
  space: { multiplier: 1.5, average: 1, accumulatorRatio: 0 },
} as const;
const ACCUMULATOR_CAPACITY_MULTIPLIER = {
  normal: 1,
  uncommon: 2,
  rare: 3,
  epic: 4,
  legendary: 6,
} as const;

export const FUSION_BLOCKS = {
  1: { neighborBonuses: [0], generators: 2, grossNormalMw: 100 },
  2: { neighborBonuses: [1, 1], generators: 8, grossNormalMw: 400 },
  3: { neighborBonuses: [2, 2, 2], generators: 18, grossNormalMw: 900 },
  4: { neighborBonuses: [3, 3, 2, 2], generators: 28, grossNormalMw: 1400 },
} as const;

function steam(input: EnergyInput, target: number): EnergyResult {
  const q = QUALITY_BY_ID.get(input.quality)?.multiplier ?? 1;
  const generator = input.steamGenerator ?? "steam-engine";
  const generatorMw =
    generator === "steam-engine" ? 0.9 * q : 1.8 * q;
  const generators = ceil(target / generatorMw);
  const boilers =
    generator === "steam-engine" ? ceil(generators / 2) : generators;
  const pumps = ceil(boilers / 200);
  const grossMw = generators * generatorMw;
  return {
    netMw: grossMw,
    grossMw,
    blueprint: steamBlueprint(generator),
    entities: [
      entity(generator, generators, `${generatorMw.toFixed(2)} MW`),
      entity(
        "boiler",
        boilers,
        generator === "steam-engine" ? "1 : 2" : "1 : 1",
      ),
      entity("offshore-pump", pumps, "1 : 200 boilers"),
    ],
    inputs: [resource("coal", (boilers * 1.8 * q) / 4)],
    notes: [
      "Fuel rate uses coal at 4 MJ/item. Substitute another fuel by energy value.",
      generator === "steam-turbine"
        ? "A steam turbine supplied with 165 °C boiler steam produces 1.8 MW: exactly the output of two steam engines."
        : "One boiler supplies two steam engines at full load.",
    ],
  };
}

function solar(input: EnergyInput, target: number): EnergyResult {
  const q = QUALITY_BY_ID.get(input.quality)?.multiplier ?? 1;
  const planet = SOLAR[input.planet];
  const peakMw = 0.06 * q * planet.multiplier;
  const averageMw = peakMw * planet.average;
  const panels = ceil(target / Math.max(averageMw, 1e-12));
  const normalRatio = planet.accumulatorRatio;
  const accumulatorRatio =
    input.planet === "vulcanus" && input.quality !== "normal"
      ? 0.56
      : (normalRatio * q) / ACCUMULATOR_CAPACITY_MULTIPLIER[input.quality];
  const accumulators = ceil(panels * accumulatorRatio);
  const grossMw = panels * averageMw;
  return {
    netMw: grossMw,
    grossMw,
    blueprint: solarBlueprint(accumulatorRatio),
    entities: [
      entity("solar-panel", panels, `≤ ${(peakMw * 1000).toFixed(1)} kW`),
      entity(
        "accumulator",
        accumulators,
        accumulatorRatio ? `${accumulatorRatio.toFixed(6)} : 1` : "0 : 1",
      ),
    ],
    inputs: [],
    notes: [
      "The result is average sustained power; peak production is higher.",
      "Solar intensity and quality are applied before day/night averaging.",
    ],
  };
}

function cardinalNeighborSum(width: number, height: number): number {
  let sum = 0;
  for (let y = 0; y < height; y += 1)
    for (let x = 0; x < width; x += 1) {
      if (x > 0) sum += 1;
      if (x < width - 1) sum += 1;
      if (y > 0) sum += 1;
      if (y < height - 1) sum += 1;
    }
  return sum;
}

function thermalGeneratorStats(
  heatMw: number,
  q: number,
  generator: SteamGenerator,
) {
  if (generator === "steam-turbine")
    return {
      netMw: heatMw,
      generators: ceil(heatMw / (5.82 * q)),
    };
  return {
    netMw: heatMw * ENGINE_500_EFFICIENCY,
    generators: ceil(heatMw / ((0.9 / ENGINE_500_EFFICIENCY) * q)),
  };
}

function nuclear(input: EnergyInput, target: number): EnergyResult {
  const q = QUALITY_BY_ID.get(input.quality)?.multiplier ?? 1;
  const generator = input.steamGenerator ?? "steam-turbine";
  const width = Math.max(1, Math.floor(input.nuclearWidth));
  const height = Math.max(1, Math.floor(input.nuclearHeight));
  const reactorsPerBlock = width * height;
  const neighborSum = cardinalNeighborSum(width, height);
  const heatPerBlockMw = 40 * q * (reactorsPerBlock + neighborSum);
  const exchangersPerBlock = ceil(heatPerBlockMw / (10 * q));
  const generatorStats = thermalGeneratorStats(
    heatPerBlockMw,
    q,
    generator,
  );
  const waterPerBlock = heatPerBlockMw * WATER_PER_MW_500;
  const pumpsPerBlock = ceil(waterPerBlock / (1200 * q));
  const blocks = ceil(target / Math.max(generatorStats.netMw, 1e-12));
  const grossMw = generatorStats.netMw * blocks;
  return {
    netMw: grossMw,
    grossMw,
    blueprint: nuclearBlueprint(
      width,
      height,
      exchangersPerBlock,
      generatorStats.generators,
      generator,
    ),
    entities: [
      entity(
        "nuclear-reactor",
        reactorsPerBlock * blocks,
        `${blocks} × ${width}×${height} · Σ ${neighborSum}`,
      ),
      entity("heat-exchanger", exchangersPerBlock * blocks),
      entity(generator, generatorStats.generators * blocks),
      entity("offshore-pump", pumpsPerBlock * blocks),
    ],
    inputs: [
      resource("uranium-fuel-cell", (reactorsPerBlock * blocks * q) / 200),
      resource("water", waterPerBlock * blocks),
    ],
    notes: [
      `One ${width}×${height} block produces ${heatPerBlockMw.toFixed(1)} MW heat and ${generatorStats.netMw.toFixed(1)} MW electricity with the selected generator.`,
      "Every cardinally adjacent reactor grants +100% of base heat output to that reactor.",
      ...(generator === "steam-engine"
        ? ["Steam engines discard the energy in 500 °C steam above their 165 °C limit."]
        : []),
    ],
  };
}

function heatingTower(input: EnergyInput, target: number): EnergyResult {
  const q = QUALITY_BY_ID.get(input.quality)?.multiplier ?? 1;
  const generator = input.steamGenerator ?? "steam-turbine";
  const towerHeatMw = 40 * q;
  const exchangersPerTower = ceil(towerHeatMw / (10 * q));
  const generatorStats = thermalGeneratorStats(
    towerHeatMw,
    q,
    generator,
  );
  const towers = ceil(target / generatorStats.netMw);
  const exchangers = exchangersPerTower * towers;
  const generators = generatorStats.generators * towers;
  const water = towerHeatMw * WATER_PER_MW_500 * towers;
  return {
    netMw: towers * generatorStats.netMw,
    grossMw: towers * generatorStats.netMw,
    blueprint: heatingBlueprint(
      exchangersPerTower,
      generatorStats.generators,
      generator,
    ),
    entities: [
      entity("heating-tower", towers, `${towerHeatMw.toFixed(1)} MW heat · 250%`),
      entity("heat-exchanger", exchangers),
      entity(generator, generators),
      entity("offshore-pump", ceil(water / (1200 * q))),
    ],
    inputs: [resource("coal", (towers * 16 * q) / 4), resource("water", water)],
    notes: [
      "A normal heating tower converts 16 MW of fuel into 40 MW heat (250% efficiency).",
      ...(generator === "steam-engine"
        ? ["Steam engines discard the energy in 500 °C steam above their 165 °C limit."]
        : []),
    ],
  };
}

function fusion(input: EnergyInput, target: number): EnergyResult {
  const q = QUALITY_BY_ID.get(input.quality)?.multiplier ?? 1;
  const design = FUSION_BLOCKS[input.fusionReactors];
  const grossPerBlock = design.grossNormalMw * q;
  const internalPerBlock = input.fusionReactors * 11.55;
  const netPerBlock = grossPerBlock - internalPerBlock;
  const blocks =
    input.fusionMode === "fixed"
      ? 1
      : ceil(target / Math.max(netPerBlock, 1e-12));
  const reactors = input.fusionReactors * blocks;
  const generators = design.generators * blocks;
  const grossMw = grossPerBlock * blocks;
  const internalMw = internalPerBlock * blocks;
  const operatingGrossMw =
    input.fusionMode === "fixed"
      ? grossMw
      : Math.min(grossMw, target + internalMw);
  const fullLoadCellsPerSecond = (reactors * q) / 400;
  const cellsPerSecond =
    grossMw > 0 ? (fullLoadCellsPerSecond * operatingGrossMw) / grossMw : 0;
  return {
    netMw: grossMw - internalMw,
    grossMw,
    blueprint: fusionBlueprint(input.fusionReactors, design.generators),
    entities: [
      entity("fusion-reactor", reactors, `${blocks} × ${input.fusionReactors}`),
      entity("fusion-generator", generators, `≤ ${50 * q} MW`),
      entity("cryogenic-plant", reactors, "1 : 1"),
    ],
    inputs: [
      resource("fusion-power-cell", cellsPerSecond),
      resource("fluoroketone-cold", reactors * 4 * q),
    ],
    notes: [
      input.fusionMode === "fixed"
        ? `This fixed block supports up to ${(grossMw - internalMw).toFixed(2)} MW net.`
        : "Fusion cell consumption follows actual grid demand; displayed fuel is the demand-sized operating rate.",
      "Cold fluoroketone is a closed-loop circulation rate, not continuous fluid consumption.",
      "Generators and Cryogenic plants are grouped in upright rows; plasma and fluid connections are intentionally omitted from the ratio preview.",
    ],
    fusion: {
      reactorsPerBlock: input.fusionReactors,
      blocks,
      reactors,
      generators,
      grossMw,
      internalMw,
      netMw: grossMw - internalMw,
      cellsPerSecond,
    },
  };
}

export function calculateEnergy(input: EnergyInput): EnergyResult {
  const target =
    Math.max(0, input.targetMw) * (1 + Math.max(0, input.reservePercent) / 100);
  if (input.technology === "steam") return steam(input, target);
  if (input.technology === "solar") return solar(input, target);
  if (input.technology === "nuclear") return nuclear(input, target);
  if (input.technology === "heating-tower") return heatingTower(input, target);
  return fusion(input, target);
}
