import { readFile, writeFile } from "node:fs/promises";
import path from "node:path";

const project = path.resolve(new URL("..", import.meta.url).pathname);
const dumpPath =
  process.env.FACTORIO_DUMP ??
  path.resolve(
    project,
    "../../vendor-official/runtime/script-output/data-raw-dump.json",
  );
const dump = JSON.parse(await readFile(dumpPath, "utf8"));

const itemLikeTypes = [
  "item",
  "ammo",
  "gun",
  "capsule",
  "module",
  "item-with-entity-data",
  "armor",
  "copy-paste-tool",
  "rail-planner",
  "repair-tool",
  "blueprint",
  "upgrade-item",
  "deconstruction-item",
  "blueprint-book",
  "spidertron-remote",
  "selection-tool",
  "space-platform-starter-pack",
];

const items = {};
for (const type of itemLikeTypes) {
  for (const [id, prototype] of Object.entries(dump[type] ?? {})) {
    if (
      !prototype.subgroup ||
      prototype.hidden === true ||
      prototype.hidden_in_factoriopedia === true
    )
      continue;
    items[id] = {
      prototypeType: type,
      subgroup: prototype.subgroup,
      order: prototype.order ?? id,
      stackSize: prototype.stack_size ?? 1,
      weight: prototype.weight ?? 1000,
      spoilResult: prototype.spoil_result,
      spoilTicks: prototype.spoil_ticks,
    };
  }
}

const fluids = {};
for (const [id, prototype] of Object.entries(dump.fluid ?? {})) {
  if (prototype.hidden === true || prototype.hidden_in_factoriopedia === true)
    continue;
  fluids[id] = {
    subgroup: prototype.subgroup ?? "fluid",
    order: prototype.order ?? id,
  };
}

const subgroups = Object.fromEntries(
  Object.entries(dump["item-subgroup"] ?? {}).map(([id, value]) => [
    id,
    { group: value.group, order: value.order ?? id },
  ]),
);
const groups = Object.fromEntries(
  Object.entries(dump["item-group"] ?? {}).map(([id, value]) => [
    id,
    { order: value.order ?? id },
  ]),
);

await writeFile(
  path.join(project, "app/data/catalog-meta.json"),
  JSON.stringify({ items, fluids, subgroups, groups }, null, 2) + "\n",
);

const recipeParts = (value) => (Array.isArray(value) ? value : []);
const expectedPartAmount = (part) => {
  const base = (part.amount ?? 1) + (part.extra_count_fraction ?? 0);
  if (typeof part.independent_probability === "number")
    return base * part.independent_probability;
  if (part.shared_probability) {
    return (
      base *
      Math.max(
        0,
        (part.shared_probability.max ?? 1) - (part.shared_probability.min ?? 0),
      )
    );
  }
  return base * (part.probability ?? 1);
};

const recyclingRecipes = {};
for (const [id, recipe] of Object.entries(dump.recipe ?? {})) {
  if (!(recipe.categories ?? []).includes("recycling")) continue;
  const ingredients = recipeParts(recipe.ingredients);
  if (ingredients.length !== 1 || (ingredients[0].type ?? "item") === "fluid")
    continue;
  const productId = ingredients[0].name;
  // Factorio also generates recycling prototypes for hidden/editor-only
  // placeholders. They exist in data.raw but cannot be obtained or selected by
  // a player, so they must not leak into the public calculator catalog.
  if (!items[productId]) continue;
  const sourceRecipeId = id.endsWith("-recycling")
    ? id.slice(0, -"-recycling".length)
    : undefined;
  const sourceRecipe = sourceRecipeId ? dump.recipe?.[sourceRecipeId] : undefined;
  const sourceProductAmount = sourceRecipe
    ? recipeParts(sourceRecipe.results)
        .filter(
          (part) =>
            (part.type ?? "item") === "item" && part.name === productId,
        )
        .reduce((sum, part) => sum + expectedPartAmount(part), 0)
    : 0;
  const lostFluids =
    sourceRecipe && sourceProductAmount > 0
      ? recipeParts(sourceRecipe.ingredients)
          .filter((part) => (part.type ?? "item") === "fluid")
          .map((part) => ({
            id: part.name,
            amount: expectedPartAmount(part) / sourceProductAmount,
          }))
      : [];
  const results = recipeParts(recipe.results)
    .map((part) => ({
      id: part.name,
      type: part.type ?? "item",
      amount: expectedPartAmount(part),
    }))
    .filter((part) => part.amount > 0);
  recyclingRecipes[id] = {
    id,
    productId,
    sourceRecipeId,
    energy: recipe.energy_required ?? 0.5,
    results,
    lostFluids,
    qualityEligible:
      !results.some((part) => part.type === "fluid") &&
      !recipeParts(sourceRecipe?.results).some(
        (part) => (part.type ?? "item") === "fluid",
      ),
  };
}

await writeFile(
  path.join(project, "app/data/recycling-recipes.json"),
  JSON.stringify({ recipes: recyclingRecipes }, null, 2) + "\n",
);

const connections = Object.values(dump["space-connection"] ?? {}).map(
  (connection) => ({
    id: connection.name,
    from: connection.from,
    to: connection.to,
    lengthKm: connection.length,
  }),
);
await writeFile(
  path.join(project, "app/data/space-logistics.json"),
  JSON.stringify(
    {
      rocketLiftWeight:
        dump["utility-constants"]?.default?.default_rocket_lift_weight ??
        1_000_000,
      rocketInventorySlots: 20,
      rocketPartsRequired:
        dump["rocket-silo"]?.["rocket-silo"]?.rocket_parts_required ?? 50,
      rocketPartSeconds: dump.recipe?.["rocket-part"]?.energy_required ?? 3,
      launchToPlatformSeconds: 27.5,
      cargoPodLandingSeconds: 1100 / 60,
      cargoLandingPadHatches: 3,
      spacePlatformHubSlots:
        dump["space-platform-hub"]?.["space-platform-hub"]?.inventory_size ??
        59,
      cargoBaySlots:
        dump["cargo-bay"]?.["cargo-bay"]?.inventory_size_bonus ?? 20,
      connections,
      thruster: {
        minFluid: 0.1,
        maxFluid: 0.8,
        minUsagePerSecond: 6,
        maxUsagePerSecond: 120,
        minThrustMn: 0.599,
        maxThrustMn: 6.1,
      },
    },
    null,
    2,
  ) + "\n",
);

console.log(
  `catalog-meta.json: ${Object.keys(items).length} items, ${Object.keys(fluids).length} fluids`,
);
console.log(
  `recycling-recipes.json: ${Object.keys(recyclingRecipes).length} generated recipes`,
);
console.log(`space-logistics.json: ${connections.length} connections`);
