import {
  BeatGamePhase,
  PortalStrategy,
  type BeatGameInventory,
  type BeatGameItemRequirement,
  type BeatGameStrategy,
} from "./model.js";

export interface BeatGameRequirementDefinition {
  readonly key: string;
  readonly itemIds?: readonly string[];
  readonly tags?: readonly string[];
  readonly target: (strategy: BeatGameStrategy) => number;
  readonly priority: number;
}

export const COOKED_FOOD_ITEM_IDS = [
  "minecraft:cooked_beef",
  "minecraft:cooked_porkchop",
  "minecraft:cooked_mutton",
  "minecraft:cooked_chicken",
  "minecraft:cooked_rabbit",
  "minecraft:cooked_cod",
  "minecraft:cooked_salmon",
  "minecraft:bread",
  "minecraft:baked_potato",
] as const;

export const RAW_FOOD_TO_COOKED = {
  "minecraft:beef": "minecraft:cooked_beef",
  "minecraft:porkchop": "minecraft:cooked_porkchop",
  "minecraft:mutton": "minecraft:cooked_mutton",
  "minecraft:chicken": "minecraft:cooked_chicken",
  "minecraft:rabbit": "minecraft:cooked_rabbit",
  "minecraft:cod": "minecraft:cooked_cod",
  "minecraft:salmon": "minecraft:cooked_salmon",
  "minecraft:potato": "minecraft:baked_potato",
} as const;

export const EDIBLE_FOOD_ITEM_IDS = [
  ...COOKED_FOOD_ITEM_IDS,
  ...Object.keys(RAW_FOOD_TO_COOKED),
  "minecraft:carrot",
  "minecraft:apple",
  "minecraft:beetroot",
  "minecraft:beetroot_soup",
  "minecraft:cookie",
  "minecraft:dried_kelp",
  "minecraft:glow_berries",
  "minecraft:golden_apple",
  "minecraft:golden_carrot",
  "minecraft:honey_bottle",
  "minecraft:melon_slice",
  "minecraft:mushroom_stew",
  "minecraft:pumpkin_pie",
  "minecraft:rabbit_stew",
  "minecraft:sweet_berries",
  "minecraft:tropical_fish",
] as const;

export const EMERGENCY_FOOD_ITEM_IDS = [
  "minecraft:rotten_flesh",
] as const;

export const CRITICAL_HUNGER_FOOD_LEVEL = 6;
export const URGENT_HUNGER_FOOD_LEVEL = 10;
const PORTAL_LOG_RESERVE = 4;

export const LOG_ITEM_IDS = [
  "minecraft:oak_log",
  "minecraft:spruce_log",
  "minecraft:birch_log",
  "minecraft:jungle_log",
  "minecraft:acacia_log",
  "minecraft:dark_oak_log",
  "minecraft:mangrove_log",
  "minecraft:cherry_log",
  "minecraft:pale_oak_log",
  "minecraft:crimson_stem",
  "minecraft:warped_stem",
] as const;

export const PLANK_ITEM_IDS = [
  "minecraft:oak_planks",
  "minecraft:spruce_planks",
  "minecraft:birch_planks",
  "minecraft:jungle_planks",
  "minecraft:acacia_planks",
  "minecraft:dark_oak_planks",
  "minecraft:mangrove_planks",
  "minecraft:cherry_planks",
  "minecraft:pale_oak_planks",
  "minecraft:crimson_planks",
  "minecraft:warped_planks",
] as const;

const REQUIREMENTS: Readonly<
  Record<BeatGamePhase, readonly BeatGameRequirementDefinition[]>
> = {
  [BeatGamePhase.PREPARE_OVERWORLD]: [
    itemRequirement(
      "logs",
      LOG_ITEM_IDS,
      ({ targetLogCount }) => targetLogCount,
      120,
    ),
    itemRequirement(
      "basic-melee-weapon",
      [
        "minecraft:netherite_sword",
        "minecraft:diamond_sword",
        "minecraft:iron_sword",
        "minecraft:stone_sword",
        "minecraft:wooden_sword",
      ],
      () => 1,
      115,
    ),
    itemRequirement(
      "food-supply",
      EDIBLE_FOOD_ITEM_IDS,
      ({ targetFoodCount }) => Math.min(targetFoodCount, 4),
      112,
    ),
    itemRequirement(
      "cobblestone",
      ["minecraft:cobblestone"],
      ({ targetCobblestoneCount }) => targetCobblestoneCount,
      110,
    ),
    itemRequirement(
      "melee-weapon",
      [
        "minecraft:netherite_sword",
        "minecraft:diamond_sword",
        "minecraft:iron_sword",
        "minecraft:stone_sword",
      ],
      () => 1,
      105,
    ),
    itemRequirement(
      "food",
      COOKED_FOOD_ITEM_IDS,
      ({ targetFoodCount }) => Math.min(targetFoodCount, 8),
      104,
    ),
    itemRequirement(
      "iron",
      ["minecraft:iron_ingot"],
      ({ targetIronCount }) => targetIronCount,
      103,
    ),
    itemRequirement(
      "shield",
      ["minecraft:shield"],
      () => 1,
      102,
    ),
    itemRequirement(
      "pickaxe",
      ["minecraft:iron_pickaxe", "minecraft:stone_pickaxe"],
      () => 1,
      68,
    ),
    itemRequirement(
      "water-bucket",
      ["minecraft:water_bucket"],
      () => 1,
      65,
    ),
    itemRequirement(
      "ignition",
      ["minecraft:flint_and_steel", "minecraft:fire_charge"],
      () => 1,
      60,
    ),
  ],
  [BeatGamePhase.ENTER_NETHER]: [
    itemRequirement(
      "obsidian",
      ["minecraft:obsidian"],
      ({ targetObsidianCount }) => targetObsidianCount,
      100,
    ),
    itemRequirement(
      "ignition",
      ["minecraft:flint_and_steel", "minecraft:fire_charge"],
      () => 1,
      90,
    ),
  ],
  [BeatGamePhase.COLLECT_NETHER_RESOURCES]: [
    itemRequirement(
      "blaze-rods",
      ["minecraft:blaze_rod"],
      ({ targetBlazeRodCount }) => targetBlazeRodCount,
      100,
    ),
    itemRequirement(
      "ender-pearls",
      ["minecraft:ender_pearl"],
      ({ targetEnderPearlCount }) => targetEnderPearlCount,
      90,
    ),
  ],
  [BeatGamePhase.RETURN_TO_OVERWORLD]: [],
  [BeatGamePhase.LOCATE_STRONGHOLD]: [
    itemRequirement(
      "eyes-of-ender",
      ["minecraft:ender_eye"],
      ({ targetEyeCount }) => targetEyeCount,
      100,
    ),
  ],
  [BeatGamePhase.ACTIVATE_END_PORTAL]: [
    itemRequirement(
      "eyes-of-ender",
      ["minecraft:ender_eye"],
      ({ targetEyeCount }) => targetEyeCount,
      100,
    ),
  ],
  [BeatGamePhase.FIGHT_ENDER_DRAGON]: [
    itemRequirement(
      "food",
      COOKED_FOOD_ITEM_IDS,
      ({ targetFoodCount }) => targetFoodCount,
      100,
    ),
    itemRequirement(
      "ranged-weapon",
      ["minecraft:bow", "minecraft:crossbow"],
      () => 1,
      80,
    ),
    itemRequirement(
      "arrows",
      ["minecraft:arrow"],
      () => 32,
      75,
    ),
  ],
  [BeatGamePhase.COLLECT_DRAGON_EGG]: [
    itemRequirement(
      "torch",
      ["minecraft:torch"],
      () => 1,
      100,
    ),
    itemRequirement(
      "dragon-egg",
      ["minecraft:dragon_egg"],
      () => 1,
      90,
    ),
  ],
  [BeatGamePhase.EXIT_END]: [],
  [BeatGamePhase.COMPLETE]: [],
};

export function requirementsForPhase(
  phase: BeatGamePhase,
  inventory: BeatGameInventory,
  strategy: BeatGameStrategy,
): readonly BeatGameItemRequirement[] {
  const definitions = phase === BeatGamePhase.ENTER_NETHER
    ? portalRequirements(inventory, strategy)
    : phase === BeatGamePhase.PREPARE_OVERWORLD
    ? prepareOverworldRequirements(inventory)
    : REQUIREMENTS[phase];
  return definitions
    .map((definition) =>
      materializeRequirement(definition, inventory, strategy)
    )
    .sort((left, right) =>
      right.priority - left.priority || left.key.localeCompare(right.key)
    );
}

function prepareOverworldRequirements(
  inventory: BeatGameInventory,
): readonly BeatGameRequirementDefinition[] {
  const definitions = REQUIREMENTS[BeatGamePhase.PREPARE_OVERWORLD];
  const logTarget = overworldLogTarget(inventory);
  return definitions.map((definition) =>
    definition.key === "logs"
      ? {
        ...definition,
        target: ({ targetLogCount }) => Math.min(targetLogCount, logTarget),
      }
      : definition
  );
}

function overworldLogTarget(inventory: BeatGameInventory): number {
  if ((inventory.counts["minecraft:shield"] ?? 0) > 0) {
    return 2;
  }
  if (
    (inventory.counts["minecraft:raw_iron"] ?? 0) > 0
    || (inventory.counts["minecraft:iron_ingot"] ?? 0) > 0
  ) {
    return 2;
  }
  const hasCookedFood = COOKED_FOOD_ITEM_IDS.some((itemId) =>
    (inventory.counts[itemId] ?? 0) > 0
  );
  if (hasCookedFood) {
    return 3;
  }
  const hasMiningPickaxe = [
    "minecraft:wooden_pickaxe",
    "minecraft:stone_pickaxe",
    "minecraft:iron_pickaxe",
    "minecraft:diamond_pickaxe",
    "minecraft:netherite_pickaxe",
  ].some((itemId) => (inventory.counts[itemId] ?? 0) > 0);
  return hasMiningPickaxe ? 4 : Number.POSITIVE_INFINITY;
}

function portalRequirements(
  inventory: BeatGameInventory,
  strategy: BeatGameStrategy,
): readonly BeatGameRequirementDefinition[] {
  const survivalRequirements = REQUIREMENTS[
    BeatGamePhase.PREPARE_OVERWORLD
  ]
    .filter(({ key }) =>
      key !== "iron"
      && key !== "water-bucket"
      && key !== "ignition"
    )
    .map((definition) => {
      if (definition.key === "logs") {
        return {
          ...definition,
          target: ({ targetLogCount }: BeatGameStrategy) =>
            Math.min(targetLogCount, PORTAL_LOG_RESERVE),
        };
      }
      if (definition.key === "food-supply") {
        return {
          ...definition,
          target: ({ targetFoodCount }: BeatGameStrategy) =>
            Math.min(targetFoodCount, 8),
        };
      }
      return definition;
    });
  const hasCompleteObsidianFrame =
    (inventory.counts["minecraft:obsidian"] ?? 0)
      >= strategy.targetObsidianCount;
  const useObsidian = strategy.portalStrategy === PortalStrategy.OBSIDIAN
    || (
      strategy.portalStrategy === PortalStrategy.AUTO
      && hasCompleteObsidianFrame
    );
  const missingLiquidBuckets = Number(
    (inventory.counts["minecraft:water_bucket"] ?? 0) === 0,
  ) + Number(
    !useObsidian
      && (inventory.counts["minecraft:lava_bucket"] ?? 0) === 0,
  );
  const missingBucketIron = Math.max(
    0,
    missingLiquidBuckets - (inventory.counts["minecraft:bucket"] ?? 0),
  );
  const portalEquipmentIronTarget = missingBucketIron
    + Number((inventory.counts["minecraft:shield"] ?? 0) === 0)
    + Number(
      (inventory.counts["minecraft:flint_and_steel"] ?? 0) === 0
        && (inventory.counts["minecraft:fire_charge"] ?? 0) === 0,
    );
  return [
    ...survivalRequirements,
    ...(
      useObsidian
        && !hasCompleteObsidianFrame
        && (inventory.counts["minecraft:diamond_pickaxe"] ?? 0) === 0
        ? [itemRequirement(
          "diamond-pickaxe",
          ["minecraft:diamond_pickaxe"],
          () => 1,
          110,
        )]
        : []
    ),
    ...(
      portalEquipmentIronTarget > 0
        ? [itemRequirement(
          "iron",
          ["minecraft:iron_ingot"],
          () => portalEquipmentIronTarget,
          103,
        )]
        : []
    ),
    useObsidian
      ? itemRequirement(
        "obsidian",
        ["minecraft:obsidian"],
        ({ targetObsidianCount }) => targetObsidianCount,
        100,
      )
      : itemRequirement(
        "lava-bucket",
        ["minecraft:lava_bucket"],
        () => 1,
        100,
      ),
    itemRequirement(
      "water-bucket",
      ["minecraft:water_bucket"],
      () => 1,
      101,
    ),
    itemRequirement(
      "ignition",
      ["minecraft:flint_and_steel", "minecraft:fire_charge"],
      () => 1,
      90,
    ),
  ];
}

export function unsatisfiedRequirements(
  phase: BeatGamePhase,
  inventory: BeatGameInventory,
  strategy: BeatGameStrategy,
): readonly BeatGameItemRequirement[] {
  return requirementsForPhase(phase, inventory, strategy)
    .filter(({ satisfied }) => !satisfied);
}

export function requirementCount(
  inventory: BeatGameInventory,
  definition: Pick<
    BeatGameRequirementDefinition,
    "itemIds" | "tags"
  >,
): number {
  const direct = (definition.itemIds ?? []).reduce(
    (total, itemId) => total + (inventory.counts[itemId] ?? 0),
    0,
  );
  const tagged = (definition.tags ?? []).reduce(
    (total, tag) => total + (inventory.counts[`#${tag}`] ?? 0),
    0,
  );
  return Math.max(direct, tagged);
}

function materializeRequirement(
  definition: BeatGameRequirementDefinition,
  inventory: BeatGameInventory,
  strategy: BeatGameStrategy,
): BeatGameItemRequirement {
  const targetCount = nonNegativeInteger(
    definition.target(strategy),
    `${definition.key}.target`,
  );
  const currentCount = requirementCount(inventory, definition);
  return {
    key: definition.key,
    itemIds: definition.itemIds ?? [],
    tags: definition.tags ?? [],
    targetCount,
    currentCount,
    priority: definition.priority,
    satisfied: currentCount >= targetCount,
  };
}

function itemRequirement(
  key: string,
  itemIds: readonly string[],
  target: BeatGameRequirementDefinition["target"],
  priority: number,
): BeatGameRequirementDefinition {
  return { key, itemIds, target, priority };
}

function nonNegativeInteger(value: number, name: string): number {
  if (!Number.isFinite(value) || value < 0) {
    throw new TypeError(`${name} must be a non-negative finite number`);
  }
  return Math.floor(value);
}
