import { iconFor } from "./factorio-data.ts";
import type { BlueprintBlockSettings } from "./blueprint-contract.ts";
import type {
  CalculationResult,
  CalculatorInput,
  ItemKind,
  LogisticsResult,
  LogisticsSettings,
  Machine,
  MaterialFlow,
  ProductOption,
  ProductionChainNode,
  ProductionChainResult,
  QualityAmount,
  QualityId,
  RateUnit,
  Recipe,
} from "./types.ts";

const EPSILON = 1e-12;
const QUALITY_ORDER: Array<QualityId | null> = [
  null,
  "normal",
  "uncommon",
  "rare",
  "epic",
  "legendary",
];

export interface FactoryTotalsSnapshot {
  id: string;
  product: ProductOption;
  input: CalculatorInput;
  result: CalculationResult;
  chain?: ProductionChainResult;
  logisticsSettings?: LogisticsSettings;
  logisticsResult?: LogisticsResult;
  beltSettings?: BlueprintBlockSettings;
}

export interface FactoryInserterLoad {
  inserterId: LogisticsSettings["inserterId"];
  inserterQuality: QualityId;
  inputTotal: number;
  outputTotal: number;
  inputFlows: FactoryInserterFlow[];
  outputFlows: FactoryInserterFlow[];
}

export interface FactoryInserterFlow {
  id: string;
  name: string;
  icon: string;
  count: number;
}

export interface FactoryContribution {
  targetId: string;
  product: ProductOption;
  amountPerSecond: number;
  machinesToBuild?: number;
  fullChain: boolean;
  inserterLoads?: FactoryInserterLoad[];
}

export interface FactoryResourceTotal extends MaterialFlow {
  contributions: FactoryContribution[];
}

export interface FactoryProcessTotal {
  key: string;
  product: ProductOption;
  recipe: Recipe;
  machine: Machine;
  machineQuality: QualityId;
  machinesToBuild: number;
  theoreticalMachines: number;
  ratePerSecond: number;
  depth: number;
  contributions: FactoryContribution[];
}

export interface FactoryBuildItemTotal {
  key: string;
  id: string;
  icon: string;
  quality: QualityId;
  kind: "source" | "machine" | "module" | "beacon" | "inserter";
  amount: number;
}

export interface FactoryTotals {
  resources: FactoryResourceTotal[];
  materials: FactoryResourceTotal[];
  processes: FactoryProcessTotal[];
  buildItems: FactoryBuildItemTotal[];
}

export function mostUsedRateUnit(
  units: RateUnit[],
  fallback: RateUnit = "s",
): RateUnit {
  if (!units.length) return fallback;
  const counts = new Map<RateUnit, number>();
  for (const unit of units) counts.set(unit, (counts.get(unit) ?? 0) + 1);
  const maximum = Math.max(...counts.values());
  return units.find((unit) => counts.get(unit) === maximum) ?? fallback;
}

function moduleSignature(input: CalculatorInput): string {
  const slot = (selection: CalculatorInput["machineModules"][number]) =>
    selection
      ? `${selection.family}:${selection.tier}:${selection.quality}`
      : "empty";
  return [
    input.machineQuality,
    input.machineModules.map(slot).join(","),
    input.beaconCount,
    input.beaconQuality,
    input.beaconModules.map(slot).join(","),
  ].join("|");
}

function flowAmounts(
  type: ItemKind,
  total: number,
  amounts?: QualityAmount[],
  quality?: QualityId,
): QualityAmount[] {
  if (amounts?.length) return amounts.map((entry) => ({ ...entry }));
  return [{ quality: type === "fluid" ? null : quality ?? "normal", amount: total }];
}

export function buildFactoryTotals(
  snapshots: FactoryTotalsSnapshot[],
): FactoryTotals {
  type AccumulatedFlow = FactoryResourceTotal & {
    qualityTotals: Map<QualityId | null, number>;
  };
  const resources = new Map<
    string,
    AccumulatedFlow
  >();
  const materials = new Map<string, AccumulatedFlow>();
  const processes = new Map<string, FactoryProcessTotal>();
  const buildItems = new Map<string, FactoryBuildItemTotal>();

  function addBuildItem(
    id: string,
    icon: string,
    quality: QualityId,
    kind: FactoryBuildItemTotal["kind"],
    amount: number,
  ) {
    if (amount <= EPSILON) return;
    const key = `${kind}:${id}:${quality}`;
    const current = buildItems.get(key);
    if (current) current.amount += amount;
    else buildItems.set(key, { key, id, icon, quality, kind, amount });
  }

  function mergeInserterFlows(
    target: FactoryInserterFlow[],
    logistics: LogisticsResult,
    direction: "input" | "output",
  ) {
    for (const flow of logistics.flows) {
      if (flow.type !== "item" || flow.direction !== direction) continue;
      let current = target.find((entry) => entry.id === flow.id);
      if (!current) {
        current = {
          id: flow.id,
          name: flow.name,
          icon: flow.icon,
          count: 0,
        };
        target.push(current);
      }
      current.count += flow.totalInserters ?? 0;
    }
  }

  function addContribution(
    list: FactoryContribution[],
    snapshot: FactoryTotalsSnapshot,
    amountPerSecond: number,
    machinesToBuild?: number,
    logistics?: LogisticsResult,
    logisticsSettings?: LogisticsSettings,
  ) {
    const current = list.find((entry) => entry.targetId === snapshot.id);
    if (current) {
      current.amountPerSecond += amountPerSecond;
      current.machinesToBuild =
        (current.machinesToBuild ?? 0) + (machinesToBuild ?? 0);
      if (logistics && logisticsSettings) {
        const loads = (current.inserterLoads ??= []);
        let load = loads.find(
          (entry) =>
            entry.inserterId === logisticsSettings.inserterId &&
            entry.inserterQuality === logisticsSettings.inserterQuality,
        );
        if (!load) {
          load = {
            inserterId: logisticsSettings.inserterId,
            inserterQuality: logisticsSettings.inserterQuality,
            inputTotal: 0,
            outputTotal: 0,
            inputFlows: [],
            outputFlows: [],
          };
          loads.push(load);
        }
        for (const flow of logistics.flows) {
          if (flow.type !== "item") continue;
          if (flow.direction === "input")
            load.inputTotal += flow.totalInserters ?? 0;
          else load.outputTotal += flow.totalInserters ?? 0;
        }
        mergeInserterFlows(load.inputFlows, logistics, "input");
        mergeInserterFlows(load.outputFlows, logistics, "output");
      }
      return;
    }
    const contribution: FactoryContribution = {
      targetId: snapshot.id,
      product: snapshot.product,
      amountPerSecond,
      machinesToBuild,
      fullChain: Boolean(snapshot.chain),
    };
    if (logistics && logisticsSettings) {
      contribution.inserterLoads = [
        {
          inserterId: logisticsSettings.inserterId,
          inserterQuality: logisticsSettings.inserterQuality,
          inputTotal: logistics.flows
            .filter(
              (flow) => flow.type === "item" && flow.direction === "input",
            )
            .reduce((sum, flow) => sum + (flow.totalInserters ?? 0), 0),
          outputTotal: logistics.flows
            .filter(
              (flow) => flow.type === "item" && flow.direction === "output",
            )
            .reduce((sum, flow) => sum + (flow.totalInserters ?? 0), 0),
          inputFlows: [],
          outputFlows: [],
        },
      ];
      mergeInserterFlows(
        contribution.inserterLoads[0].inputFlows,
        logistics,
        "input",
      );
      mergeInserterFlows(
        contribution.inserterLoads[0].outputFlows,
        logistics,
        "output",
      );
    }
    list.push(contribution);
  }

  function addFlow(
    collection: Map<string, AccumulatedFlow>,
    snapshot: FactoryTotalsSnapshot,
    source: {
      id: string;
      name: string;
      icon: string;
      type: ItemKind;
      total: number;
      amounts?: QualityAmount[];
      quality?: QualityId;
    },
  ) {
    const key = `${source.type}:${source.id}`;
    let total = collection.get(key);
    if (!total) {
      total = {
        id: source.id,
        name: source.name,
        icon: source.icon,
        type: source.type,
        total: 0,
        amounts: [],
        contributions: [],
        qualityTotals: new Map(),
      };
      collection.set(key, total);
    }
    total.total += source.total;
    for (const amount of flowAmounts(
      source.type,
      source.total,
      source.amounts,
      source.quality,
    )) {
      total.qualityTotals.set(
        amount.quality,
        (total.qualityTotals.get(amount.quality) ?? 0) + amount.amount,
      );
    }
    addContribution(total.contributions, snapshot, source.total);
  }

  function addResource(
    snapshot: FactoryTotalsSnapshot,
    source: {
      id: string;
      name: string;
      icon: string;
      type: ItemKind;
      total: number;
      amounts?: QualityAmount[];
      quality?: QualityId;
    },
  ) {
    addFlow(resources, snapshot, source);
    addFlow(materials, snapshot, source);
  }

  function addProcess(
    snapshot: FactoryTotalsSnapshot,
    product: ProductOption,
    input: CalculatorInput,
    result: CalculationResult,
    depth: number,
    logistics?: LogisticsResult,
    logisticsSettings?: LogisticsSettings,
  ) {
    const key = [
      product.key,
      input.recipe.id,
      input.machine.id,
      moduleSignature(input),
    ].join("|");
    let process = processes.get(key);
    if (!process) {
      process = {
        key,
        product,
        recipe: input.recipe,
        machine: input.machine,
        machineQuality: input.machineQuality,
        machinesToBuild: 0,
        theoreticalMachines: 0,
        ratePerSecond: 0,
        depth,
        contributions: [],
      };
      processes.set(key, process);
    }
    process.machinesToBuild += result.machinesToBuild;
    process.theoreticalMachines += result.theoreticalMachines;
    process.ratePerSecond += result.ratePerSecond;
    process.depth = Math.min(process.depth, depth);
    addContribution(
      process.contributions,
      snapshot,
      result.ratePerSecond,
      result.machinesToBuild,
      logistics,
      logisticsSettings,
    );
    addBuildItem(
      input.machine.id,
      input.machine.icon,
      input.machineQuality,
      "machine",
      result.machinesToBuild,
    );
    for (const selection of input.machineModules) {
      if (!selection) continue;
      const id = `${selection.family}-module${
        selection.tier === 1 ? "" : `-${selection.tier}`
      }`;
      addBuildItem(
        id,
        iconFor(id),
        selection.quality,
        "module",
        result.machinesToBuild,
      );
    }
    const beaconCount = result.beaconLayout.uniqueBeacons;
    addBuildItem(
      "beacon",
      iconFor("beacon"),
      input.beaconQuality,
      "beacon",
      beaconCount,
    );
    for (const selection of input.beaconModules) {
      if (!selection || !beaconCount) continue;
      const id = `${selection.family}-module${
        selection.tier === 1 ? "" : `-${selection.tier}`
      }`;
      addBuildItem(
        id,
        iconFor(id),
        selection.quality,
        "module",
        beaconCount,
      );
    }
    if (logistics && logisticsSettings) {
      const inserters = logistics.flows
        .filter((flow) => flow.type === "item")
        .reduce((sum, flow) => sum + (flow.totalInserters ?? 0), 0);
      addBuildItem(
        logisticsSettings.inserterId,
        iconFor(logisticsSettings.inserterId),
        logisticsSettings.inserterQuality,
        "inserter",
        inserters,
      );
    }
    for (const flow of result.outputs) {
      addFlow(materials, snapshot, {
        id: flow.id,
        name: flow.name,
        icon: flow.icon,
        type: flow.type,
        total: flow.total,
        amounts: flow.amounts,
      });
    }
  }

  function visitProcess(
    snapshot: FactoryTotalsSnapshot,
    node: ProductionChainNode,
  ) {
    addProcess(
      snapshot,
      node.product,
      node.calculatorInput,
      node.result,
      node.depth,
      node.logistics,
      node.logisticsSettings,
    );
    for (const child of node.inputs) {
      if (child.kind === "recipe") visitProcess(snapshot, child);
    }
  }

  for (const snapshot of snapshots) {
    if (!snapshot.result.valid) continue;
    if (snapshot.chain) {
      for (const source of snapshot.chain.rawSources) {
        addResource(snapshot, {
          id: source.id,
          name: source.name,
          icon: source.icon,
          type: source.type,
          total: source.ratePerSecond,
          amounts: source.outputAmounts ?? source.requiredAmounts,
          quality: source.requiredQuality,
        });
        if (
          source.entityId &&
          [
            "offshore-pump",
            "pumpjack",
            "burner-mining-drill",
            "electric-mining-drill",
            "big-mining-drill",
            "agricultural-tower",
            "asteroid-collector",
          ].includes(source.entityId)
        )
          addBuildItem(
            source.entityId,
            iconFor(source.entityId),
            "normal",
            "source",
            Math.ceil(source.entities - EPSILON),
          );
        const sourceEntities = Math.ceil(source.entities - EPSILON);
        for (const selection of source.modules ?? []) {
          if (!selection || !sourceEntities) continue;
          const id = `${selection.family}-module${
            selection.tier === 1 ? "" : `-${selection.tier}`
          }`;
          addBuildItem(
            id,
            iconFor(id),
            selection.quality,
            "module",
            sourceEntities,
          );
        }
      }
      visitProcess(snapshot, snapshot.chain.root);
    } else {
      for (const flow of snapshot.result.inputs) {
        addResource(snapshot, {
          id: flow.id,
          name: flow.name,
          icon: flow.icon,
          type: flow.type,
          total: flow.total,
          amounts: flow.amounts,
        });
      }
      addProcess(
        snapshot,
        snapshot.product,
        snapshot.input,
        snapshot.result,
        0,
        snapshot.logisticsResult,
        snapshot.logisticsSettings,
      );
    }
  }

  function finalizeFlows(collection: Map<string, AccumulatedFlow>) {
    const list = [...collection.values()].map(
      ({ qualityTotals, ...resource }) => ({
        ...resource,
        amounts: QUALITY_ORDER.flatMap((quality) => {
          const amount = qualityTotals.get(quality) ?? 0;
          return amount > EPSILON ? [{ quality, amount }] : [];
        }),
        contributions: resource.contributions.sort((a, b) =>
          a.product.name.localeCompare(b.product.name),
        ),
      }),
    );
    list.sort(
      (a, b) =>
        Number(a.type === "fluid") - Number(b.type === "fluid") ||
        a.name.localeCompare(b.name),
    );
    return list;
  }

  const resourceList = finalizeFlows(resources);
  const materialList = finalizeFlows(materials);

  const processList = [...processes.values()].sort(
    (a, b) =>
      a.depth - b.depth ||
      a.product.name.localeCompare(b.product.name) ||
      a.machine.name.localeCompare(b.machine.name),
  );
  return {
    resources: resourceList,
    materials: materialList,
    processes: processList,
    buildItems: [...buildItems.values()].sort(
      (a, b) =>
        a.kind.localeCompare(b.kind) ||
        a.id.localeCompare(b.id) ||
        QUALITY_ORDER.indexOf(a.quality) - QUALITY_ORDER.indexOf(b.quality),
    ),
  };
}
