import { calculate } from "../../app/lib/calculator.ts";
import { calculateLogistics } from "../../app/lib/logistics.ts";
import { calculateBeaconDesignWithGap } from "../../app/lib/beacons.ts";
import type { BlueprintBlockSettings, BlueprintChainSource } from "../../app/lib/blueprint-contract.ts";
import type { Locale, TilePlacement } from "../../app/lib/types.ts";
import { blueprintNodes, generateBlueprintPlan, type BlueprintNodeView, type BlueprintPlan, type BlueprintEntity } from "./generator.ts";
import { substationWires } from "./power-grid.ts";
import { placeFlowLabels } from "./flow-labels.ts";

/** Merge demand, not already-rounded machine counts. Keep incompatible
 * qualities, recipes, modules, beacons and inserter setups independent. */
export function aggregateChainNodes(source: BlueprintChainSource) {
  const groups = new Map<string, { node: BlueprintNodeView; rate: number; sources: number }>();
  if (source.result.valid) {
    const nodes = blueprintNodes(source.input, source.result, source.logistics, source.logisticsSettings, source.chain);
    for (const node of nodes) {
      if (!node.result.valid || node.result.ratePerSecond <= 0) continue;
      const i = node.input;
      const modules = (slots: typeof i.machineModules) => slots.map((m) => m ? `${m.family}:${m.tier}:${m.quality}` : "empty").sort();
      const key = JSON.stringify([
        i.product.key, i.recipe.id, i.machine.id, i.machineQuality,
        i.inputQuality, i.inputQualityDistribution, i.outputQuality, i.maxQuality,
        i.calculationMode, modules(i.machineModules), modules(i.beaconModules),
        i.beaconCount, i.beaconQuality, i.isolatedBeacons,
        node.logisticsSettings.inserterId, node.logisticsSettings.inserterQuality,
        node.logisticsSettings.capacityBonus, node.logisticsSettings.handoff,
      ]);
      const current = groups.get(key);
      if (current) {
        current.rate += node.result.ratePerSecond;
        current.sources++;
        current.node.depth = Math.max(current.node.depth, node.depth);
      } else groups.set(key, { node: { ...node, key }, rate: node.result.ratePerSecond, sources: 1 });
    }
  }
  return [...groups.values()].map(({ node, rate, sources }) => {
    const input = { ...node.input, mode: "output" as const, unit: "s" as const, amount: rate, inputConstraints: [] };
    const result = calculate(input);
    return { ...node, input, result, logistics: calculateLogistics(result, input, node.logisticsSettings), sources };
  }).sort((a, b) => b.depth - a.depth || a.input.product.key.localeCompare(b.input.product.key));
}

export function generateChainBlueprintPlan(source: BlueprintChainSource, settings: BlueprintBlockSettings, locale: Locale): BlueprintPlan {
  const nodes = aggregateChainNodes(source);
  if (nodes.some((node) => node.input.machine.id === "passive-spoilage")) throw new Error(locale === "de"
    ? "Diese Produktkette enthält passive Verderbnisprozesse ohne platzierbare Maschine. Bitte diese Prozesse separat behandeln; es wird kein String mit ungültigen Maschinen ausgegeben."
    : "This production chain includes passive spoilage processes without a placeable machine. Handle these processes separately; no string with invalid machines is exported.");
  const total = nodes.reduce((sum, node) => sum + node.result.machinesToBuild, 0);
  if (!nodes.length || total > 4096) throw new Error(locale === "de"
    ? "Für eine Produktkette werden 1 bis 4096 Maschinen unterstützt. Bitte die Menge anpassen; es wird kein unvollständiger String ausgegeben."
    : "A production chain supports 1 to 4096 machines. Adjust the amount; no incomplete string is exported.");
  const parts: BlueprintPlan[] = [];
  // Every block fits around the same fixed support lattice. Shared boundary
  // poles and Roboports therefore have identical positions when blocks merge.
  const partSettings = settings;
  for (const node of nodes) {
    const template = generateBlueprintPlan(node, partSettings, locale);
    let remaining = node.result.machinesToBuild;
    while (remaining > 0) {
      if (remaining >= template.machineCountInBlock) {
        parts.push(template);
        remaining -= template.machineCountInBlock;
      } else {
        const share = remaining / node.result.machinesToBuild;
        const beaconLayout = calculateBeaconDesignWithGap(node.input.machine, remaining, node.input.beaconCount,
          node.logistics.flows.some((f) => f.type === "fluid") && node.input.beaconCount > 0 ? 2 : 0);
        const tail = generateBlueprintPlan({ ...node,
          result: { ...node.result, machinesToBuild: remaining, beaconLayout },
          logistics: { ...node.logistics, flows: node.logistics.flows.map((f) => ({ ...f, totalPerSecond: f.totalPerSecond * share })) },
        }, partSettings, locale);
        parts.push(tail);
        remaining -= tail.machineCountInBlock;
      }
    }
  }
  if (parts.reduce((sum, part) => sum + part.entityCount, 0) > 25000) throw new Error(locale === "de"
    ? "Die Produktkette ist für eine interaktive Vorschau zu groß. Bitte die Menge verringern oder einzelne Prozesse exportieren."
    : "The production chain is too large for an interactive preview. Reduce the amount or export individual processes.");
  const vertical = settings.orientation === "vertical";
  const shelfLength = Math.max(...parts.map((p) => vertical ? p.grid.y : p.grid.x), Math.ceil(Math.sqrt(parts.reduce((sum, p) => sum + p.grid.x * p.grid.y, 0)) * 1.4));
  const entities: BlueprintEntity[] = [];
  const placements: TilePlacement[] = [];
  const counts: number[] = [];
  const seenEntities = new Set<string>();
  const seenPlacements = new Set<string>();
  let along = 0, across = 0, shelfDepth = 0, length = 0;
  const fluidRoutes: BlueprintPlan["fluidRoutes"] = [];
  for (const part of parts) {
    const partLength = vertical ? part.grid.y : part.grid.x;
    const partDepth = vertical ? part.grid.x : part.grid.y;
    if (along > 0 && along + partLength > shelfLength) { across += shelfDepth; along = 0; shelfDepth = 0; }
    const x = vertical ? across : along, y = vertical ? along : across;
    const machineOffset = counts.length;
    for (const entity of part.blueprint.blueprint.entities as BlueprintEntity[]) {
      const next = { ...entity, position: { x: entity.position.x + x, y: entity.position.y + y } };
      const key = JSON.stringify([next.name, next.position, next.quality, next.recipe, next.direction, next.type]);
      if (seenEntities.has(key)) continue;
      seenEntities.add(key);
      entities.push({ ...next, entity_number: entities.length + 1 });
    }
    for (const p of part.layout.placements) {
      const next = { ...p, x: p.x + x, y: p.y + y,
        ...(p.labelAnchor ? { labelAnchor: { x: p.labelAnchor.x + x, y: p.labelAnchor.y + y } } : {}),
        ...(p.machineIndex !== undefined ? { machineIndex: p.machineIndex + machineOffset } : {}),
      };
      const key = `${next.entityId}:${next.x}:${next.y}:${next.kind}:${next.flowDirection ?? ""}`;
      if (seenPlacements.has(key)) continue;
      seenPlacements.add(key);
      placements.push(next);
    }
    fluidRoutes.push(...part.fluidRoutes.map((route) => ({ ...route, lane: route.lane + (route.axis === "vertical" ? x : y), ports: route.ports.map((port) => ({ x: port.x + x, y: port.y + y })) })));
    counts.push(...part.layout.counts);
    along += partLength;
    length = Math.max(length, along);
    shelfDepth = Math.max(shelfDepth, partDepth);
  }
  const grid = vertical ? { x: across + shelfDepth, y: length } : { x: length, y: across + shelfDepth };
  const roboportCoverageComplete = parts.every((p) => p.roboportCoverageComplete);
  placeFlowLabels(placements);
  const base = parts[0];
  const blueprint = { blueprint: {
    ...base.blueprint.blueprint,
    label: `FactorioCalculatorApp · ${source.input.product.name} · ${locale === "de" ? "Produktkette" : "Production chain"}`,
    description: `Production chain for ${source.input.product.name}. Inter-process belt and pipe connections must be completed in game.`,
    icons: [{ signal: { type: source.input.product.type, name: source.input.product.id }, index: 1 }],
    entities,
    ...(settings.substationPriority ? { wires: substationWires(entities) } : {}),
    ...(settings.snapToGrid !== false ? { "snap-to-grid": grid } : {}),
  } };
  const warnings = [...new Map(parts.flatMap((p) => p.warnings.filter((w) => w.code !== "preview-block")).map((w) => [w.message, w])).values()];
  warnings.unshift({ code: "chain-connections", message: locale === "de"
    ? "Die aktivierten Teilprozesse dieser Produktkette sind mit allen benötigten Blöcken enthalten; gleiche Konfigurationen teilen sich den addierten Bedarf. Bänder und Rohre zwischen den Prozessblöcken müssen im Spiel verbunden werden. Rohstoffabbau gehört nicht zu diesem Blueprint."
    : "The enabled processes in this production chain are included with all required blocks; matching configurations share their combined demand. Connect belts and pipes between process blocks in game. Resource extraction is not included." });
  return { ...base, blueprint, blueprintJson: JSON.stringify(blueprint), warnings, grid,
    machineCountInBlock: total, totalMachineCount: total, repeatCount: 1, entityCount: entities.length,
    beltLoads: parts.flatMap((p) => p.beltLoads), fluidRoutes,
    requestedBeltOverloaded: parts.some((p) => p.requestedBeltOverloaded),
    beaconCoverageComplete: parts.every((p) => p.beaconCoverageComplete),
    substationCoverageComplete: parts.every((p) => p.substationCoverageComplete),
    roboportCoverageComplete,
    chainProcesses: nodes.map((node) => ({ productId: node.input.product.id, recipeId: node.input.recipe.id, machineId: node.input.machine.id, machines: node.result.machinesToBuild, sources: node.sources })),
    layout: { ...base.layout, machineCount: total, placements, counts, exact: parts.every((p) => p.beaconCoverageComplete), extrapolated: false,
      min: Math.min(...counts), max: Math.max(...counts), average: counts.reduce((sum, n) => sum + n, 0) / counts.length,
      uniqueBeacons: placements.filter((p) => p.kind === "beacon").length,
      bounds: { minX: Math.min(...placements.map((p) => p.x)), minY: Math.min(...placements.map((p) => p.y)), maxX: Math.max(...placements.map((p) => p.x + p.width)), maxY: Math.max(...placements.map((p) => p.y + p.height)) },
    },
  };
}
