/**
 * Bot zoos: same indicators and formulas as player (coins, plotLevel, conveyorLevel, truckLevel).
 * Decisions (buy, sell, upgrade) follow a randomly chosen profile (fast / slow / balanced).
 * Egg color choice is weighted by neighboring zoos' animalWeights.
 */

import { GameConfig } from "./config.js";
import { LootTables, getColorNames, zeroAnimalWeights } from "./loot-tables.js";
import { getPlotUpgradeCost, getSchoolUpgradeCost, getTruckUpgradeCost, getConveyorUpgradeCost, getSellValue } from "./economy.js";
import { getIncomeMultiplier } from "./mutation-rules.js";
import { pickId } from "./weighted-random.js";
import { tryUpgradePlot } from "./zoo.js";
import { tryUpgrade, tryUpgradeTruck } from "./conveyor.js";
import { getEffectiveProfileId, getProfileParams, LEGACY_PROFILE_TO_ID } from "./auto-mode-profiles.js";

const PROFILE_OPTIONS = ["fast", "slow", "balanced"];

/**
 * Max levels for plot, conveyor/school, and truck from config.
 * @returns {{ plotMax: number, skillMax: number, truckMax: number }}
 */
function getUpgradeMaxLevels() {
  return {
    plotMax: GameConfig.Plot?.MaxLevel ?? 8,
    skillMax: GameConfig.Conveyor?.MaxLevel ?? 8,
    truckMax: (GameConfig.Truck && GameConfig.Truck.MaxLevel) ?? 5,
  };
}

/**
 * @returns {import("./types.js").BotState}
 */
export function createInitialBotState() {
  const cfg = GameConfig.Bots || {};
  const minC = cfg.InitialCoinsMin ?? 150;
  const maxC = cfg.InitialCoinsMax ?? 450;
  const coins = minC + Math.floor(Math.random() * (maxC - minC + 1));
  const profile = PROFILE_OPTIONS[Math.floor(Math.random() * PROFILE_OPTIONS.length)];
  return {
    coins,
    plotLevel: 1,
    conveyorLevel: 1,
    truckLevel: 1,
    profile,
    lastTickAt: 0,
  };
}

/**
 * @param {import("./types.js").WorldZooEntry} zoo
 * @returns {number}
 */
export function getZooSkillLevel(zoo) {
  const b = zoo.botState;
  return b ? b.conveyorLevel : 1;
}

/**
 * @param {Record<string, number>} out
 * @param {Record<string, number>} neighborWeights
 * @param {string[]} colorNames
 * @returns {void}
 */
function addNeighborWeights(out, neighborWeights, colorNames) {
  for (const c of colorNames) out[c] = (out[c] ?? 0) + (neighborWeights[c] ?? 0);
}

/**
 * Neighbor color weights (sum of animalWeights of zoos within maxDistance on the map).
 * @param {import("./types.js").GameState} state
 * @param {string} zooId
 * @returns {Record<string, number>}
 */
export function getNeighborColorWeights(state, zooId) {
  const zoos = state.worldZoos ?? [];
  const self = zoos.find((z) => z.id === zooId);
  if (!self) return {};
  const maxD = (GameConfig.Bots && GameConfig.Bots.NeighborMaxDistance) ?? 35;
  const colorNames = getColorNames();
  const out = zeroAnimalWeights();
  for (const z of zoos) {
    if (z.id !== zooId) {
      const dx = (z.x - self.x) / 100;
      const dy = (z.y - self.y) / 100;
      const dist = Math.sqrt(dx * dx + dy * dy) * 100;
      if (dist <= maxD) addNeighborWeights(out, z.animalWeights ?? {}, colorNames);
    }
  }
  return out;
}

function getAverageIncomePerSecondForEggType(eggType) {
  const def = LootTables.EggTypes[eggType];
  if (!def || !def.loot.length) return 0;
  let sum = 0;
  let totalWeight = 0;
  for (const e of def.loot) {
    const a = LootTables.Animals[e.id];
    if (a) {
      const w = e.weight ?? 1;
      sum += a.baseIncomePerSecond * w;
      totalWeight += w;
    }
  }
  return totalWeight > 0 ? sum / totalWeight : 0;
}

function getAverageSellValueForEggType(eggType) {
  const def = LootTables.EggTypes[eggType];
  if (!def || !def.loot.length) return 0;
  let sum = 0;
  let totalWeight = 0;
  const mutMult = getIncomeMultiplier("none");
  for (const e of def.loot) {
    const a = LootTables.Animals[e.id];
    if (a) {
      const w = e.weight ?? 1;
      const v = getSellValue(a.baseIncomePerSecond, 1, mutMult, a.sellFactor);
      sum += v * w;
      totalWeight += w;
    }
  }
  return totalWeight > 0 ? Math.floor(sum / totalWeight) : 0;
}

/**
 * Add income for a bot from its animalWeights (same formula as player: income per "animal" per color).
 * @param {import("./types.js").WorldZooEntry} zoo
 * @param {number} dt seconds
 */
function tickBotIncome(zoo, dt) {
  const b = zoo.botState;
  if (!b) return;
  const weights = zoo.animalWeights ?? {};
  const colorNames = getColorNames();
  let total = 0;
  for (const eggType of colorNames) {
    const count = weights[eggType] ?? 0;
    if (count > 0) {
      const avgIncome = getAverageIncomePerSecondForEggType(eggType);
      total += count * avgIncome * dt;
    }
  }
  const visitorPart = 0;
  b.coins = Math.max(0, b.coins + total + visitorPart);
}

/**
 * Ensure zoo has botState (for non-player zoos). Mutates zoo.
 * @param {import("./types.js").WorldZooEntry} zoo
 * @param {boolean} isPlayer
 */
export function ensureBotState(zoo, isPlayer) {
  if (isPlayer) return;
  if (zoo.botState) return;
  zoo.botState = createInitialBotState();
}

/**
 * @param {import("./types.js").BotState} b
 * @param {string} choice
 * @param {{ plotCost: number, skillCost: number, truckCost: number }} costs
 * @returns {boolean}
 */
function applyBotUpgradeChoice(b, choice, costs) {
  const { plotCost, skillCost, truckCost } = costs;
  if (choice === "plot" && b.coins >= plotCost) {
    b.coins -= plotCost;
    b.plotLevel += 1;
    return true;
  }
  if (choice === "skill" && b.coins >= skillCost) {
    b.coins -= skillCost;
    b.conveyorLevel += 1;
    return true;
  }
  if (choice === "truck" && b.coins >= truckCost) {
    b.coins -= truckCost;
    b.truckLevel += 1;
    return true;
  }
  return false;
}

/**
 * @param {import("./types.js").GameState} state
 * @param {import("./types.js").WorldZooEntry} zoo
 * @param {{ b: import("./types.js").BotState, rng: () => number, params: { spendThreshold: number, upgradeChance: number } }} opts
 * @returns {boolean}
 */
function botDecideUpgrade(state, zoo, opts) {
  const { b, rng, params } = opts;
  const { spendThreshold, upgradeChance } = params;
  const { plotMax, skillMax, truckMax } = getUpgradeMaxLevels();
  const plotCost = getPlotUpgradeCost(b.plotLevel);
  const skillCost = getSchoolUpgradeCost(b.conveyorLevel);
  const truckCost = getTruckUpgradeCost(b.truckLevel);
  const upgradeChoices = [];
  if (b.plotLevel < plotMax && b.coins >= plotCost * spendThreshold) upgradeChoices.push("plot");
  if (b.conveyorLevel < skillMax && b.coins >= skillCost * spendThreshold) upgradeChoices.push("skill");
  if (b.truckLevel < truckMax && b.coins >= truckCost * spendThreshold) upgradeChoices.push("truck");
  if (upgradeChoices.length === 0 || rng() >= upgradeChance) return false;
  const choice = upgradeChoices[Math.floor(rng() * upgradeChoices.length)];
  return applyBotUpgradeChoice(b, choice, { plotCost, skillCost, truckCost });
}

/**
 * @param {import("./types.js").WorldZooEntry} zoo
 * @param {() => number} rng
 * @param {{ sellChance: number }} params
 * @returns {boolean}
 */
function botDecideSell(zoo, rng, params) {
  const weights = zoo.animalWeights ?? {};
  const colorNames = getColorNames();
  const totalAnimals = colorNames.reduce((s, c) => s + (weights[c] ?? 0), 0);
  if (totalAnimals <= 0 || rng() >= params.sellChance) return false;
  const withWeight = colorNames.filter((c) => (weights[c] ?? 0) > 0).map((c) => ({ id: c, weight: weights[c] ?? 0 }));
  if (withWeight.length === 0) return false;
  const soldColor = pickId(rng, withWeight);
  weights[soldColor] = (weights[soldColor] ?? 1) - 1;
  if (weights[soldColor] <= 0) delete weights[soldColor];
  const b = zoo.botState;
  if (b) b.coins += getAverageSellValueForEggType(soldColor);
  return true;
}

/**
 * @param {import("./types.js").GameState} state
 * @param {import("./types.js").WorldZooEntry} zoo
 * @param {() => number} rng
 * @param {{ spendThreshold: number }} params
 * @returns {void}
 */
function botDecideBuy(state, zoo, rng, params) {
  const b = zoo.botState;
  if (!b) return;
  const spendThreshold = params.spendThreshold;
  const colorNames = getColorNames();
  const neighborWeights = getNeighborColorWeights(state, zoo.id);
  const weights = zoo.animalWeights ?? {};
  const eligibleTypes = colorNames.filter((c) => {
    const def = LootTables.EggTypes[c];
    return def && b.conveyorLevel >= def.minConveyorLevel;
  });
  if (eligibleTypes.length === 0) return;
  const neighborEntries = eligibleTypes.map((c) => ({
    id: c,
    weight: Math.max(1, (neighborWeights[c] ?? 0) + (weights[c] ?? 0) * 2),
  }));
  const totalN = neighborEntries.reduce((s, e) => s + e.weight, 0);
  if (totalN <= 0) return;
  const eggType = pickId(rng, neighborEntries);
  const eggDef = LootTables.EggTypes[eggType];
  if (!eggDef || b.coins < eggDef.price * spendThreshold) return;
  b.coins -= eggDef.price;
  weights[eggType] = (weights[eggType] ?? 0) + 1;
}

/**
 * One decision tick for one bot: possibly buy egg, sell "animal", or upgrade.
 * @param {import("./types.js").GameState} state
 * @param {import("./types.js").WorldZooEntry} zoo
 * @param {number} nowUnix
 */
function tickBotDecisions(state, zoo, nowUnix) {
  const b = zoo.botState;
  if (!b) return;
  const cfg = GameConfig.Bots || {};
  const minInterval = cfg.TickIntervalMinSeconds ?? 8;
  const maxInterval = cfg.TickIntervalMaxSeconds ?? 25;
  if (nowUnix - b.lastTickAt < minInterval) return;
  const rng = () => Math.random();
  const intervalRange = maxInterval - minInterval + 1;
  const nextInterval = minInterval + Math.floor(rng() * intervalRange);
  if (nowUnix - b.lastTickAt < nextInterval) return;
  b.lastTickAt = nowUnix;

  const profileId = LEGACY_PROFILE_TO_ID[b.profile] ?? 25;
  const params = getProfileParams(profileId);
  if (botDecideUpgrade(state, zoo, { b, rng, params })) return;
  if (botDecideSell(zoo, rng, params)) return;
  botDecideBuy(state, zoo, rng, params);
}

/**
 * Run income and decision ticks for all bot zoos.
 * @param {import("./types.js").GameState} state
 * @param {number} nowUnix
 * @param {number} dt seconds since last tick
 */
export function tickBotZoos(state, nowUnix, dt) {
  const zoos = state.worldZoos ?? [];
  for (const zoo of zoos) {
    if (zoo.id !== "player") {
      ensureBotState(zoo, false);
      tickBotIncome(zoo, dt);
      tickBotDecisions(state, zoo, nowUnix);
    }
  }
}

const PLAYER_AUTO_MIN_INTERVAL = 10;
const PLAYER_AUTO_MAX_INTERVAL = 28;

/** @param {string[]} choices
 * @param {import("./types.js").GameState} state
 * @param {{ level: number, maxLevel: number, costFn: (n: number) => number, key: string, st: number }} opts
 * @returns {void}
 */
function maybePushUpgradeChoice(choices, state, opts) {
  const { level, maxLevel, costFn, key, st } = opts;
  if (level < maxLevel && state.coins >= costFn(level) * st) choices.push(key);
}

/** @param {import("./types.js").GameState} state
 * @param {{ spendThreshold: number }} params
 * @returns {string[]}
 */
function getPlayerUpgradeChoices(state, params) {
  const { plotMax, skillMax, truckMax } = getUpgradeMaxLevels();
  const st = params.spendThreshold;
  const choices = [];
  maybePushUpgradeChoice(choices, state, { level: state.plotLevel ?? 1, maxLevel: plotMax, costFn: getPlotUpgradeCost, key: "plot", st });
  maybePushUpgradeChoice(choices, state, { level: state.conveyorLevel ?? 1, maxLevel: skillMax, costFn: getConveyorUpgradeCost, key: "skill", st });
  maybePushUpgradeChoice(choices, state, { level: state.truckLevel ?? 1, maxLevel: truckMax, costFn: getTruckUpgradeCost, key: "truck", st });
  return choices;
}

/** @param {import("./types.js").GameState} state
 * @param {string} choice
 * @returns {void}
 */
function applyPlayerUpgradeChoice(state, choice) {
  if (choice === "plot") tryUpgradePlot(state);
  else if (choice === "skill") tryUpgrade(state);
  else if (choice === "truck") tryUpgradeTruck(state);
}

/**
 * Apply one upgrade (plot, skill, truck) for player auto mode.
 * @param {import("./types.js").GameState} state
 * @param {{ spendThreshold: number, upgradeChance: number }} params
 * @param {() => number} rng
 * @returns {void}
 */
function playerAutoDoOneUpgrade(state, params, rng) {
  const choices = getPlayerUpgradeChoices(state, params);
  if (choices.length === 0 || rng() >= params.upgradeChance) return;
  const choice = choices[Math.floor(rng() * choices.length)];
  applyPlayerUpgradeChoice(state, choice);
}

/**
 * When auto mode is on, apply one bot-style upgrade decision for the player (plot, conveyor, or truck).
 * @param {import("./types.js").GameState} state
 * @param {number} nowUnix
 * @returns {void}
 */
export function tickPlayerAutoMode(state, nowUnix) {
  if (!state.autoMode) return;
  const cfg = GameConfig.Bots || {};
  const minInterval = cfg.TickIntervalMinSeconds ?? PLAYER_AUTO_MIN_INTERVAL;
  const maxInterval = cfg.TickIntervalMaxSeconds ?? PLAYER_AUTO_MAX_INTERVAL;
  const last = state.lastPlayerAutoTickAt ?? 0;
  if (nowUnix - last < minInterval) return;
  const rng = () => Math.random();
  const nextInterval = minInterval + Math.floor(rng() * (maxInterval - minInterval + 1));
  if (nowUnix - last < nextInterval) return;
  state.lastPlayerAutoTickAt = nowUnix;
  const profileId = getEffectiveProfileId(state);
  const params = getProfileParams(profileId);
  playerAutoDoOneUpgrade(state, params, rng);
}
