import catalogMetaJson from "../data/catalog-meta.json" with { type: "json" };
import iconManifest from "../data/icon-manifest.json" with { type: "json" };
import rawData from "../data/recipes.json" with { type: "json" };

import type {
  EffectKind,
  ItemGroupId,
  ItemKind,
  Machine,
  ModuleDefinition,
  ModuleFamily,
  ProductOption,
  Quality,
  QualityId,
  Recipe,
  RecipeIngredient,
  RecipeProduct,
} from "./types.ts";

type RawPart = {
  name: string;
  type?: ItemKind;
  amount?: number;
  independent_probability?: number;
  shared_probability?: { min?: number; max?: number };
  ignored_by_productivity?: number;
  ignored_by_stats?: number;
  fluidbox_index?: number;
};
type RawRecipe = {
  name: string;
  categories?: string[];
  category?: string;
  energy_required?: number;
  ingredients?: RawPart[] | Record<string, never>;
  results?: RawPart[] | Record<string, never>;
  allow_productivity?: boolean;
  allow_quality?: boolean;
  hidden?: boolean;
  subgroup?: string;
  order?: string;
  surface_conditions?: Array<{ property: string; min?: number; max?: number }>;
};
type CatalogItem = {
  prototypeType: string;
  subgroup: string;
  order: string;
  stackSize: number;
  weight: number;
  spoilResult?: string;
  spoilTicks?: number;
};
type CatalogMeta = {
  items: Record<string, CatalogItem>;
  fluids: Record<string, { subgroup: string; order: string }>;
  subgroups: Record<string, { group: string; order: string }>;
  groups: Record<string, { order: string }>;
};

const manifest = iconManifest as Record<string, string>;
export const CATALOG_META = catalogMetaJson as CatalogMeta;

const ICON_ALIASES: Record<string, string> = {
  barrel: "barreling_empty-barrel",
  "capture-robot-rocket": "icons_capture-bot",
  "defender-capsule": "icons_defender",
  "destroyer-capsule": "icons_destroyer",
  "distractor-capsule": "icons_distractor",
  "heat-exchanger": "icons_heat-boiler",
  "landing-pad-unloading-bay": "icons_cargo-landing-pad",
  "raw-fish": "icons_fish",
};

const SPECIAL_NAMES: Record<string, string> = {
  "low-density-structure": "Low density structure",
  "uranium-235": "Uranium-235",
  "uranium-238": "Uranium-238",
  "raw-fish": "Raw fish",
  "electromagnetic-plant": "EMP",
  "passive-spoilage": "Passive spoilage",
};

export function displayName(id: string): string {
  if (SPECIAL_NAMES[id]) return SPECIAL_NAMES[id];
  const value = id.replace(/-mk(\d)/g, " MK$1").replace(/-/g, " ");
  return value.charAt(0).toUpperCase() + value.slice(1);
}

function manifestPath(key: string): string | undefined {
  const url = manifest[key];
  if (!url) return undefined;
  const marker = "/factorio-icons/";
  const index = url.indexOf(marker);
  return index >= 0 ? `/factorio-icons/${url.slice(index + marker.length)}` : undefined;
}

export function iconFor(id: string, type: ItemKind = "item"): string {
  const alias = ICON_ALIASES[id];
  if (alias) {
    const path = manifestPath(alias);
    if (path) return path;
  }
  // Filled-barrel icons are tint-layer composites in Factorio rather than
  // standalone files. The bundled icon mirror contains the rendered crude-oil
  // barrel and the official neutral fill layer, so these real recycling items
  // still receive a recognizable in-game barrel symbol instead of a fallback.
  if (id === "crude-oil-barrel") {
    const path = manifestPath("barreling_barrel-crude-oil");
    if (path) return path;
  }
  if (id.endsWith("-barrel")) {
    const path = manifestPath("barreling_barrel-fill");
    if (path) return path;
  }
  const keys = type === "fluid"
    ? [`fluid_${id}`, `icons_${id}`]
    : [`icons_${id}`, `equipment_${id}`, `fluid_${id}`, `technology_${id}`];
  for (const key of keys) {
    const path = manifestPath(key);
    if (path) return path;
  }
  return "/favicon.svg";
}
export function iconForGroup(id: ItemGroupId): string {
  return manifestPath(`item-group_${id === "combat" ? "military" : id}`) ?? iconFor(id);
}

function expectedProbability(part: RawPart): number {
  if (typeof part.independent_probability === "number") return part.independent_probability;
  if (part.shared_probability) {
    return Math.max(0, (part.shared_probability.max ?? 1) - (part.shared_probability.min ?? 0));
  }
  return 1;
}
function normalizeIngredient(part: RawPart): RecipeIngredient {
  return { name: part.name, type: part.type ?? "item", amount: part.amount ?? 1, ignoredByStats: part.ignored_by_stats ?? 0, ...(part.fluidbox_index ? { fluidboxIndex: part.fluidbox_index } : {}) };
}
function normalizeProduct(part: RawPart): RecipeProduct {
  return {
    name: part.name,
    type: part.type ?? "item",
    amount: part.amount ?? 1,
    probability: expectedProbability(part),
    ignoredByProductivity: part.ignored_by_productivity ?? 0,
    ignoredByStats: part.ignored_by_stats ?? 0,
    ...(part.fluidbox_index ? { fluidboxIndex: part.fluidbox_index } : {}),
  };
}
function parts(value: RawPart[] | Record<string, never> | undefined): RawPart[] {
  return Array.isArray(value) ? value : [];
}

export const RAW_RECIPES = (rawData as { recipes: Record<string, RawRecipe> }).recipes;
const prototypeRecipes: Recipe[] = Object.entries(RAW_RECIPES)
  .filter(([, recipe]) => {
    const categories = recipe.categories ?? (recipe.category ? [recipe.category] : ["crafting"]);
    return recipe.hidden !== true && !categories.includes("parameters") &&
      (parts(recipe.ingredients).length > 0 || parts(recipe.results).length > 0);
  })
  .map(([id, recipe]) => {
    const products = parts(recipe.results).map(normalizeProduct);
    const primary = products[0];
    const ownIcon = iconFor(id, primary?.type ?? "item");
    return {
      id,
      name: displayName(id),
      categories: recipe.categories ?? (recipe.category ? [recipe.category] : ["crafting"]),
      energy: recipe.energy_required ?? 0.5,
      ingredients: parts(recipe.ingredients).map(normalizeIngredient),
      products,
      allowProductivity: recipe.allow_productivity === true,
      allowQuality: recipe.allow_quality !== false,
      subgroup: recipe.subgroup,
      order: recipe.order,
      icon: ownIcon === "/favicon.svg" && primary ? iconFor(primary.name, primary.type) : ownIcon,
      surfaceConditions: recipe.surface_conditions,
    };
  });

const spoilageRecipes: Recipe[] = Object.entries(CATALOG_META.items)
  .filter(([, item]) => item.spoilResult && item.spoilTicks)
  .map(([id, item]) => ({
    id: `spoil-${id}`,
    name: `${displayName(id)} spoilage`,
    categories: ["passive-spoilage"],
    energy: (item.spoilTicks ?? 60) / 60,
    ingredients: [{ name: id, type: "item", amount: 1, ignoredByStats: 0 }],
    products: [{
      name: item.spoilResult!,
      type: "item",
      amount: 1,
      probability: 1,
      ignoredByProductivity: 1,
      ignoredByStats: 0,
    }],
    allowProductivity: false,
    allowQuality: false,
    subgroup: item.subgroup,
    order: item.order,
    icon: iconFor(item.spoilResult!),
    passive: true,
  }));

export const RECIPES: Recipe[] = [...prototypeRecipes, ...spoilageRecipes]
  .sort((a, b) => a.name.localeCompare(b.name));
export const RECIPE_BY_ID = new Map(RECIPES.map((recipe) => [recipe.id, recipe]));

export function catalogPlacement(id: string, type: ItemKind): { group: ItemGroupId; subgroup?: string; order?: string } {
  if (type === "fluid") {
    const meta = CATALOG_META.fluids[id];
    return { group: "fluids", subgroup: meta?.subgroup, order: meta?.order };
  }
  const meta = CATALOG_META.items[id];
  const rawGroup = meta ? CATALOG_META.subgroups[meta.subgroup]?.group : undefined;
  const group: ItemGroupId =
    rawGroup === "logistics" || rawGroup === "production" || rawGroup === "intermediate-products" ||
    rawGroup === "space" || rawGroup === "combat"
      ? rawGroup
      : rawGroup === "other" ? "production" : "intermediate-products";
  return { group, subgroup: meta?.subgroup, order: meta?.order };
}

const productMap = new Map<string, ProductOption>();
for (const recipe of RECIPES) {
  const ingredients = new Map<string, number>();
  for (const ingredient of recipe.ingredients) {
    const key = `${ingredient.type}:${ingredient.name}`;
    ingredients.set(key, (ingredients.get(key) ?? 0) + ingredient.amount);
  }
  const totals = new Map<string, number>();
  const definitions = new Map<string, RecipeProduct>();
  for (const product of recipe.products) {
    const key = `${product.type}:${product.name}`;
    totals.set(key, (totals.get(key) ?? 0) + product.amount * product.probability);
    definitions.set(key, product);
  }
  for (const [key, gross] of totals) {
    if (gross - (ingredients.get(key) ?? 0) <= 1e-12) continue;
    const product = definitions.get(key)!;
    const current = productMap.get(key);
    if (current) current.recipeIds.push(recipe.id);
    else {
      productMap.set(key, {
        key,
        id: product.name,
        type: product.type,
        name: displayName(product.name),
        icon: iconFor(product.name, product.type),
        recipeIds: [recipe.id],
        ...catalogPlacement(product.name, product.type),
      });
    }
  }
}
export const PRODUCTS = [...productMap.values()].sort((a, b) =>
  (CATALOG_META.groups[a.group]?.order ?? "").localeCompare(CATALOG_META.groups[b.group]?.order ?? "") ||
  (CATALOG_META.subgroups[a.subgroup ?? ""]?.order ?? "").localeCompare(CATALOG_META.subgroups[b.subgroup ?? ""]?.order ?? "") ||
  (a.order ?? a.id).localeCompare(b.order ?? b.id),
);
export const PRODUCT_BY_KEY = new Map(PRODUCTS.map((product) => [product.key, product]));
const PRODUCT_ORDER = new Map(PRODUCTS.map((product, index) => [product.key, index]));

const downstreamKeys = new Map<string, Set<string>>();
for (const recipe of RECIPES) {
  const outputs = PRODUCTS.filter((product) =>
    product.recipeIds.includes(recipe.id),
  ).map((product) => product.key);
  if (!outputs.length) continue;
  for (const ingredient of recipe.ingredients) {
    const ingredientKey = `${ingredient.type}:${ingredient.name}`;
    const set = downstreamKeys.get(ingredientKey) ?? new Set<string>();
    for (const output of outputs) if (output !== ingredientKey) set.add(output);
    downstreamKeys.set(ingredientKey, set);
  }
}
export function downstreamProducts(productKey: string): ProductOption[] {
  return [...(downstreamKeys.get(productKey) ?? [])]
    .map((key) => PRODUCT_BY_KEY.get(key))
    .filter((product): product is ProductOption => Boolean(product))
    .sort(
      (a, b) =>
        (PRODUCT_ORDER.get(a.key) ?? Infinity) -
        (PRODUCT_ORDER.get(b.key) ?? Infinity),
    );
}

const allEffects: EffectKind[] = ["consumption", "speed", "productivity", "pollution", "quality"];
const noQualityEffects: EffectKind[] = ["consumption", "speed", "productivity", "pollution"];

export const MACHINES: Machine[] = [
  { id: "assembling-machine-1", shortName: "Assembling machine 1", name: "Assembling machine 1", icon: iconFor("assembling-machine-1"), categories: ["crafting", "advanced-crafting"], speed: .5, moduleSlots: 0, allowedEffects: ["speed", "consumption", "pollution"], baseProductivity: 0, energyKw: 75, drainKw: 2.5, heatingKw: 100, energyKind: "electric", width: 3, height: 3, maxBeacons: 0 },
  { id: "assembling-machine-2", shortName: "Assembling machine 2", name: "Assembling machine 2", icon: iconFor("assembling-machine-2"), categories: ["crafting", "advanced-crafting", "crafting-with-fluid"], speed: .75, moduleSlots: 2, allowedEffects: allEffects, baseProductivity: 0, energyKw: 150, drainKw: 5, heatingKw: 100, energyKind: "electric", width: 3, height: 3, maxBeacons: 12 },
  { id: "assembling-machine-3", shortName: "Assembling machine 3", name: "Assembling machine 3", icon: iconFor("assembling-machine-3"), categories: ["crafting", "advanced-crafting", "crafting-with-fluid"], speed: 1.25, moduleSlots: 4, allowedEffects: allEffects, baseProductivity: 0, energyKw: 375, drainKw: 12.5, heatingKw: 100, energyKind: "electric", width: 3, height: 3, maxBeacons: 12 },
  { id: "stone-furnace", shortName: "Stone furnace", name: "Stone furnace", icon: iconFor("stone-furnace"), categories: ["smelting"], speed: 1, moduleSlots: 0, allowedEffects: ["speed", "consumption", "pollution"], baseProductivity: 0, energyKw: 90, energyKind: "burner", width: 2, height: 2, maxBeacons: 0 },
  { id: "steel-furnace", shortName: "Steel furnace", name: "Steel furnace", icon: iconFor("steel-furnace"), categories: ["smelting"], speed: 2, moduleSlots: 0, allowedEffects: ["speed", "consumption", "pollution"], baseProductivity: 0, energyKw: 90, energyKind: "burner", width: 2, height: 2, maxBeacons: 0 },
  { id: "electric-furnace", shortName: "Electric furnace", name: "Electric furnace", icon: iconFor("electric-furnace"), categories: ["smelting"], speed: 2, moduleSlots: 2, allowedEffects: allEffects, baseProductivity: 0, energyKw: 180, drainKw: 6, heatingKw: 100, energyKind: "electric", width: 3, height: 3, maxBeacons: 12 },
  { id: "chemical-plant", shortName: "Chemical plant", name: "Chemical plant", icon: iconFor("chemical-plant"), categories: ["chemistry"], speed: 1, moduleSlots: 3, allowedEffects: allEffects, baseProductivity: 0, energyKw: 210, drainKw: 7, heatingKw: 100, energyKind: "electric", width: 3, height: 3, maxBeacons: 12 },
  { id: "oil-refinery", shortName: "Oil refinery", name: "Oil refinery", icon: iconFor("oil-refinery"), categories: ["oil-processing"], speed: 1, moduleSlots: 3, allowedEffects: noQualityEffects, baseProductivity: 0, energyKw: 420, drainKw: 14, heatingKw: 200, energyKind: "electric", width: 5, height: 5, maxBeacons: 16 },
  { id: "centrifuge", shortName: "Centrifuge", name: "Centrifuge", icon: iconFor("centrifuge"), categories: ["centrifuging"], speed: 1, moduleSlots: 2, allowedEffects: allEffects, baseProductivity: 0, energyKw: 350, drainKw: 350 / 30, heatingKw: 100, energyKind: "electric", width: 3, height: 3, maxBeacons: 12 },
  { id: "rocket-silo", shortName: "Rocket silo", name: "Rocket silo", icon: iconFor("rocket-silo"), categories: ["rocket-building"], speed: 1, moduleSlots: 4, allowedEffects: noQualityEffects, baseProductivity: 0, energyKw: 250, drainKw: 250 / 30, heatingKw: 300, auxiliaryActiveKw: 3990, energyKind: "electric", width: 9, height: 9, maxBeacons: 16 },
  { id: "foundry", shortName: "Foundry", name: "Foundry", icon: iconFor("foundry"), categories: ["metallurgy"], speed: 4, moduleSlots: 4, allowedEffects: allEffects, baseProductivity: .5, energyKw: 2500, drainKw: 2500 / 30, heatingKw: 300, energyKind: "electric", width: 5, height: 5, maxBeacons: 16 },
  { id: "electromagnetic-plant", shortName: "EMP", name: "EMP", icon: iconFor("electromagnetic-plant"), categories: ["electromagnetics"], speed: 2, moduleSlots: 5, allowedEffects: allEffects, baseProductivity: .5, energyKw: 2000, drainKw: 2000 / 30, heatingKw: 100, energyKind: "electric", width: 4, height: 4, maxBeacons: 12 },
  { id: "biochamber", shortName: "Biochamber", name: "Biochamber", icon: iconFor("biochamber"), categories: ["organic"], speed: 2, moduleSlots: 4, allowedEffects: allEffects, baseProductivity: .5, energyKw: 500, heatingKw: 100, energyKind: "nutrients", fuelItem: "nutrients", fuelValueKj: 2000, width: 3, height: 3, maxBeacons: 12 },
  { id: "cryogenic-plant", shortName: "Cryogenic plant", name: "Cryogenic plant", icon: iconFor("cryogenic-plant"), categories: ["cryogenics"], speed: 2, moduleSlots: 8, allowedEffects: allEffects, baseProductivity: 0, energyKw: 1500, drainKw: 1500 / 30, heatingKw: 100, energyKind: "electric", width: 5, height: 5, maxBeacons: 16 },
  { id: "crusher", shortName: "Crusher", name: "Crusher", icon: iconFor("crusher"), categories: ["crushing"], speed: 1, moduleSlots: 2, allowedEffects: allEffects, baseProductivity: 0, energyKw: 540, drainKw: 18, heatingKw: 200, energyKind: "electric", width: 2, height: 3, maxBeacons: 12 },
  { id: "recycler", shortName: "Recycler", name: "Recycler", icon: iconFor("recycler"), categories: ["recycling"], speed: .5, moduleSlots: 4, allowedEffects: ["consumption", "speed", "pollution", "quality"], baseProductivity: 0, energyKw: 180, drainKw: 6, heatingKw: 100, energyKind: "electric", width: 2, height: 4, maxBeacons: 12 },
  { id: "captive-biter-spawner", shortName: "Captive biter spawner", name: "Captive biter spawner", icon: iconFor("captive-biter-spawner"), categories: ["captive-spawner-process"], speed: 1, moduleSlots: 0, allowedEffects: [], baseProductivity: 0, energyKw: 100, energyKind: "food", fuelItem: "bioflux", fuelValueKj: 6000, width: 5, height: 5, maxBeacons: 0 },
  { id: "passive-spoilage", shortName: "Passive spoilage", name: "Passive spoilage", icon: iconFor("spoilage"), categories: ["passive-spoilage"], speed: 1, moduleSlots: 0, allowedEffects: [], baseProductivity: 0, energyKw: 0, energyKind: "passive", width: 1, height: 1, maxBeacons: 0 },
];
export const MACHINE_BY_ID = new Map(MACHINES.map((machine) => [machine.id, machine]));
export function machinesForRecipe(recipe: Recipe): Machine[] {
  return MACHINES.filter((machine) => machine.categories.some((category) => recipe.categories.includes(category)))
    .sort((a, b) => b.baseProductivity - a.baseProductivity || b.speed - a.speed || b.moduleSlots - a.moduleSlots);
}

export const QUALITIES: Quality[] = [
  { id: "normal", name: "Normal", level: 0, multiplier: 1, beaconPowerMultiplier: 1, color: "#a8adb3", icon: iconFor("quality-normal") },
  { id: "uncommon", name: "Uncommon", level: 1, multiplier: 1.3, beaconPowerMultiplier: 5 / 6, color: "#43a047", icon: iconFor("quality-uncommon") },
  { id: "rare", name: "Rare", level: 2, multiplier: 1.6, beaconPowerMultiplier: 4 / 6, color: "#2f7fc1", icon: iconFor("quality-rare") },
  { id: "epic", name: "Epic", level: 3, multiplier: 1.9, beaconPowerMultiplier: 3 / 6, color: "#9c3db4", icon: iconFor("quality-epic") },
  { id: "legendary", name: "Legendary", level: 5, multiplier: 2.5, beaconPowerMultiplier: 1 / 6, color: "#e5a12b", icon: iconFor("quality-legendary") },
];
export const QUALITY_BY_ID = new Map<QualityId, Quality>(QUALITIES.map((quality) => [quality.id, quality]));

const moduleRows: Array<[ModuleFamily, 1 | 2 | 3, Partial<Record<EffectKind, number>>]> = [
  ["speed", 1, { speed: .2, consumption: .5, quality: -.01 }],
  ["speed", 2, { speed: .3, consumption: .6, quality: -.015 }],
  ["speed", 3, { speed: .5, consumption: .7, quality: -.025 }],
  ["efficiency", 1, { consumption: -.3 }],
  ["efficiency", 2, { consumption: -.4 }],
  ["efficiency", 3, { consumption: -.5 }],
  ["productivity", 1, { productivity: .04, consumption: .4, pollution: .05, speed: -.05 }],
  ["productivity", 2, { productivity: .06, consumption: .6, pollution: .07, speed: -.1 }],
  ["productivity", 3, { productivity: .1, consumption: .8, pollution: .1, speed: -.15 }],
  ["quality", 1, { quality: .01, speed: -.05 }],
  ["quality", 2, { quality: .02, speed: -.05 }],
  ["quality", 3, { quality: .025, speed: -.05 }],
];
export const MODULES: ModuleDefinition[] = moduleRows.map(([family, tier, effects]) => ({
  family,
  tier,
  name: `${displayName(family)} module ${tier}`,
  icon: iconFor(`${family}-module${tier === 1 ? "" : `-${tier}`}`),
  effects,
}));
export const MODULE_BY_KEY = new Map(MODULES.map((module) => [`${module.family}:${module.tier}`, module]));
export const MODULE_PRIORITY: ModuleFamily[] = ["productivity", "speed", "quality", "efficiency"];

export function routeScore(recipe: Recipe, product: ProductOption): number {
  const machine = machinesForRecipe(recipe)[0];
  const gross = recipe.products
    .filter((entry) => entry.name === product.id && entry.type === product.type)
    .reduce((sum, entry) => {
      const eligible = Math.max(0, entry.amount - entry.ignoredByProductivity);
      return sum + (entry.amount + eligible * (machine?.baseProductivity ?? 0)) * entry.probability;
    }, 0);
  const consumed = recipe.ingredients
    .filter((entry) => entry.name === product.id && entry.type === product.type)
    .reduce((sum, entry) => sum + entry.amount, 0);
  const throughput = Math.max(0, gross - consumed) * (machine?.speed ?? 0) / recipe.energy;
  return throughput * 1e6 + (machine?.baseProductivity ?? 0) * 100 + (machine?.moduleSlots ?? 0);
}

const CANONICAL_RECIPE_BY_PRODUCT: Partial<Record<string, string>> = {
  "fluid:petroleum-gas": "advanced-oil-processing",
  "fluid:light-oil": "advanced-oil-processing",
  "fluid:heavy-oil": "advanced-oil-processing",
  "fluid:molten-iron": "iron-ore-melting",
  "fluid:molten-copper": "copper-ore-melting",
  "fluid:fluoroketone-hot": "fluoroketone",
  "item:solid-fuel": "solid-fuel-from-light-oil",
  "item:uranium-235": "uranium-processing",
  "item:uranium-238": "uranium-processing",
  "item:nutrients": "nutrients-from-bioflux",
};

export function preferredRecipeForProduct(product: ProductOption): Recipe {
  const routes = product.recipeIds
    .map((id) => RECIPE_BY_ID.get(id))
    .filter((recipe): recipe is Recipe => Boolean(recipe));
  return routes.sort(
    (a, b) =>
      routeScore(b, product) - routeScore(a, product) ||
      a.name.localeCompare(b.name),
  )[0];
}

export function preferredChainRecipeForProduct(product: ProductOption): Recipe {
  const routes = product.recipeIds
    .map((id) => RECIPE_BY_ID.get(id))
    .filter((recipe): recipe is Recipe => Boolean(recipe));
  const canonicalId = CANONICAL_RECIPE_BY_PRODUCT[product.key] ?? product.id;
  return (
    routes.find((recipe) => recipe.id === canonicalId) ??
    preferredRecipeForProduct(product)
  );
}
export function preferredMachineForRecipe(recipe: Recipe): Machine {
  return machinesForRecipe(recipe)[0];
}

export const DATA_META = {
  factorioVersion: (rawData as { factorio_version: string }).factorio_version,
  sourceCommit: (rawData as { source_commit: string }).source_commit,
  prototypeRecipes: prototypeRecipes.length,
  spoilageRoutes: spoilageRecipes.length,
  visibleRecipes: RECIPES.length,
  products: PRODUCTS.length,
};
