import recyclingJson from "../data/recycling-recipes.json" with { type: "json" };
import { calculateBeaconDesign } from "./beacons.ts";
import {
  beaconEffects,
  effectsForModules,
  qualityDistribution,
} from "./calculator.ts";
import {
  CATALOG_META,
  MACHINE_BY_ID,
  QUALITY_BY_ID,
  catalogPlacement,
  displayName,
  iconFor,
} from "./factorio-data.ts";
import type {
  ItemKind,
  MaterialFlow,
  RecyclingInput,
  RecyclingResult,
  RecyclingRoute,
  Recipe,
} from "./types.ts";

type GeneratedRecycleRecipe = {
  id: string;
  productId: string;
  sourceRecipeId?: string;
  energy: number;
  results: Array<{ id: string; type: ItemKind; amount: number }>;
  lostFluids?: Array<{ id: string; amount: number }>;
  qualityEligible: boolean;
};
const recycler = MACHINE_BY_ID.get("recycler")!;
const RECYCLING_RECIPE: Recipe = {
  id: "recycling",
  name: "Recycling",
  categories: ["recycling"],
  energy: 0.5,
  ingredients: [],
  products: [],
  allowProductivity: false,
  allowQuality: true,
  icon: recycler.icon,
};

const generated = (
  recyclingJson as { recipes: Record<string, GeneratedRecycleRecipe> }
).recipes;
const routes: RecyclingRoute[] = Object.values(generated).map((recipe) => {
  const outputs = recipe.results
    .filter((result) => result.type !== "fluid")
    .map((result) => ({
      id: result.id,
      type: "item" as const,
      amount: result.amount,
      icon: iconFor(result.id),
      name: displayName(result.id),
    }));
  const lostFluids = [
    ...recipe.results.filter((result) => result.type === "fluid"),
    ...(recipe.lostFluids ?? []).map((result) => ({ ...result, type: "fluid" as const })),
  ]
    .map((result) => ({
      id: result.id,
      amount: result.amount,
      icon: iconFor(result.id, "fluid"),
      name: displayName(result.id),
    }));
  return {
    id: recipe.id,
    productId: recipe.productId,
    productName: displayName(recipe.productId),
    productIcon: iconFor(recipe.productId),
    sourceRecipeId: recipe.sourceRecipeId ?? recipe.id,
    sourceRecipeName: displayName(recipe.sourceRecipeId ?? recipe.id),
    energy: recipe.energy,
    outputs,
    lostFluids,
    qualityEligible: recipe.qualityEligible,
  };
});

export const RECYCLING_ROUTES = routes.sort(
  (a, b) =>
    a.productName.localeCompare(b.productName) ||
    (a.sourceRecipeName ?? "").localeCompare(b.sourceRecipeName ?? ""),
);
export const RECYCLING_ROUTE_BY_ID = new Map(
  RECYCLING_ROUTES.map((route) => [route.id, route]),
);
export type RecyclingProduct = {
  id: string;
  name: string;
  icon: string;
  routeIds: string[];
};
function productList(qualityOnly: boolean): RecyclingProduct[] {
  const map = new Map<string, RecyclingProduct>();
  for (const route of RECYCLING_ROUTES) {
    if (qualityOnly && !route.qualityEligible) continue;
    const current = map.get(route.productId);
    if (current) current.routeIds.push(route.id);
    else
      map.set(route.productId, {
        id: route.productId,
        name: route.productName,
        icon: route.productIcon,
        routeIds: [route.id],
      });
  }
  return [...map.values()].sort((a, b) => {
    const first = catalogPlacement(a.id, "item");
    const second = catalogPlacement(b.id, "item");
    return (
      (CATALOG_META.groups[first.group]?.order ?? "").localeCompare(
        CATALOG_META.groups[second.group]?.order ?? "",
      ) ||
      (CATALOG_META.subgroups[first.subgroup ?? ""]?.order ?? "").localeCompare(
        CATALOG_META.subgroups[second.subgroup ?? ""]?.order ?? "",
      ) ||
      (first.order ?? a.id).localeCompare(second.order ?? b.id)
    );
  });
}
export const RECYCLING_PRODUCTS = productList(false);
export const QUALITY_RECYCLING_PRODUCTS = productList(true);

export function calculateRecycling(input: RecyclingInput): RecyclingResult {
  const machineEffects = effectsForModules(
    input.modules,
    recycler,
    RECYCLING_RECIPE,
  );
  const requested = input.beaconEnabled
    ? input.qualityMode
      ? 1
      : Math.min(input.beaconCount ?? 0, recycler.maxBeacons)
    : 0;
  const isolatedBeaconEffects = beaconEffects(
    requested,
    input.beaconModules,
    recycler,
    RECYCLING_RECIPE,
    input.beaconQuality,
  );
  const q =
    QUALITY_BY_ID.get(input.recyclerQuality) ?? QUALITY_BY_ID.get("normal")!;
  const qualityChance = input.qualityMode
    ? Math.max(
        0,
        Math.min(1, machineEffects.quality + isolatedBeaconEffects.quality),
      )
    : 0;
  const distribution = qualityDistribution(
    input.inputQuality,
    qualityChance,
    input.maxQuality,
  );
  const requestedPerSecond =
    input.amount / (input.unit === "s" ? 1 : input.unit === "min" ? 60 : 3600);
  const selectedOutput =
    input.route.outputs.find((output) => output.id === input.targetOutputId) ??
    input.route.outputs[0];
  const selectedQualityFraction =
    !input.qualityMode || input.targetQuality === "total" || !input.targetQuality
      ? 1
      : distribution.find((entry) => entry.quality === input.targetQuality)
          ?.amount ?? 0;
  const selectedOutputPerInput =
    (selectedOutput?.amount ?? 0) * selectedQualityFraction;
  const inputPerSecond =
    input.qualityMode && input.amountMode === "target-output"
      ? selectedOutputPerInput > 0
        ? requestedPerSecond / selectedOutputPerInput
        : 0
      : requestedPerSecond;
  const targetOutputPerSecond = selectedOutputPerInput * inputPerSecond;
  const speedFor = (count: number) => {
    const effects = beaconEffects(
      count,
      input.beaconModules,
      recycler,
      RECYCLING_RECIPE,
      input.beaconQuality,
    );
    return (
      recycler.speed *
      q.multiplier *
      Math.max(0.2, 1 + machineEffects.speed + effects.speed)
    );
  };
  const isolatedThroughput = speedFor(requested) / input.route.energy;
  const theoreticalRecyclers =
    isolatedThroughput > 0 ? inputPerSecond / isolatedThroughput : 0;
  let recyclersToBuild =
    inputPerSecond > 0
      ? Math.max(1, Math.ceil(theoreticalRecyclers - 1e-12))
      : 0;
  const makeLayout = (count: number) =>
    calculateBeaconDesign(recycler, count, requested);
  let beaconLayout = makeLayout(recyclersToBuild);
  const capacityFor = (layout: typeof beaconLayout) => {
    const sample = layout.counts.reduce(
      (sum, count) => sum + speedFor(count) / input.route.energy,
      0,
    );
    return layout.extrapolated && layout.counts.length
      ? (sample / layout.counts.length) * layout.machineCount
      : sample;
  };
  let capacity = capacityFor(beaconLayout);
  for (
    let iteration = 0;
    iteration < 20 && capacity + 1e-10 < inputPerSecond;
    iteration += 1
  ) {
    recyclersToBuild += 1;
    beaconLayout = makeLayout(recyclersToBuild);
    capacity = capacityFor(beaconLayout);
  }
  const utilization = capacity > 0 ? Math.min(1, inputPerSecond / capacity) : 0;
  const averageThroughput =
    recyclersToBuild > 0 ? capacity / recyclersToBuild : 0;
  const recycleTimeSeconds = averageThroughput > 0 ? 1 / averageThroughput : 0;
  const outputs: MaterialFlow[] = input.route.outputs.map((output) => {
    const total = output.amount * inputPerSecond;
    const amounts = distribution.map((entry) => ({
      quality: entry.quality,
      amount: total * entry.amount,
    }));
    return {
      id: output.id,
      name: output.name,
      type: output.type,
      icon: output.icon,
      amounts,
      total,
    };
  });
  const lostFluids: MaterialFlow[] = input.route.lostFluids.map((fluid) => ({
    id: fluid.id,
    name: fluid.name,
    type: "fluid",
    icon: fluid.icon,
    amounts: [{ quality: null, amount: fluid.amount * inputPerSecond }],
    total: fluid.amount * inputPerSecond,
  }));
  const averageEnergyModifier = beaconLayout.counts.length
    ? beaconLayout.counts.reduce((sum, count) => {
        const effects = beaconEffects(
          count,
          input.beaconModules,
          recycler,
          RECYCLING_RECIPE,
          input.beaconQuality,
        );
        return (
          sum +
          Math.max(0.2, 1 + machineEffects.consumption + effects.consumption)
        );
      }, 0) / beaconLayout.counts.length
    : 1;
  const beaconQ =
    QUALITY_BY_ID.get(input.beaconQuality) ?? QUALITY_BY_ID.get("normal")!;
  const electricKw =
    recyclersToBuild *
      (recycler.energyKw * averageEnergyModifier * utilization +
        (recycler.drainKw ?? 0)) +
    beaconLayout.uniqueBeacons * 480 * beaconQ.beaconPowerMultiplier;
  const warnings: string[] = [];
  if (input.route.lostFluids.length)
    warnings.push("Fluid ingredients are not returned by recycling.");
  if (input.qualityMode)
    warnings.push(
      "Quality outputs are long-run expected values; individual recycling rolls vary.",
    );
  if (!input.route.qualityEligible && input.qualityMode)
    warnings.push(
      "This source recipe produces fluid and is excluded from Quality Recycling.",
    );
  return {
    inputPerSecond,
    targetOutputPerSecond,
    recyclerTimeSeconds: recycleTimeSeconds,
    throughputPerRecycler: averageThroughput,
    theoreticalRecyclers,
    recyclersToBuild,
    utilization,
    craftingSpeed: averageThroughput * input.route.energy,
    qualityChance,
    electricKw,
    outputs,
    lostFluids,
    beaconLayout,
    warnings,
  };
}
