/**
 * Reproduction: pairs of same-type animals (at least one from another zoo) in proximity
 * produce a baby after a delay. Delay is reduced by zoo reproduction score and biome/temperature fit.
 */

import { GameConfig } from "./config.js";
import { LootTables } from "./loot-tables.js";
import { cellKey, isOriginCell } from "./grid-utils.js";
import { getBlockKeysFromCell } from "./placement.js";
import { getDisplayBiome, getDisplayTemperature } from "./biome-rules.js";
import { addPendingBaby } from "./zoo.js";
import { getCurrentSeason, getSeasonReproductionBonus, getSeasonTemperatureModifier } from "./seasons.js";

/**
 * Reproduction season bonus. In winter, cold-adapted biomes (Mountain) are exempt from the -50% malus (spec: sauf espèces adaptées).
 * @param {string} season
 * @param {import("./loot-tables.js").LootTables["Animals"][string]} def
 * @returns {number}
 */
function getEffectiveReproductionSeasonBonus(season, def) {
  const base = getSeasonReproductionBonus(season);
  if (season === "winter" && base < 0 && def?.biome === "Mountain") return 0;
  return base;
}

/**
 * Zoo reproduction score (stub for phase 7). Higher = shorter delay until baby.
 * @param {import("./types.js").GameState} state
 * @returns {number}
 */
export function getReproductionScore(state) {
  const birthCount = state.birthCount ?? 0;
  const feedingRate = state.feedingRate ?? 1;
  return Math.max(0.5, 1 + birthCount * 0.05 + feedingRate * 0.3);
}

/**
 * Reproduction factor from animal's fit to cell biome (from loot-tables).
 * @param {import("./loot-tables.js").LootTables["Animals"][string]} def
 * @param {string} cellBiome
 * @returns {number}
 */
export function getBiomeReproductionFactor(def, cellBiome) {
  if (!def || !def.reproductionScoreByBiome) return 0.5;
  return def.reproductionScoreByBiome[cellBiome] ?? 0.5;
}

/**
 * Temperature factor: 1 when within ideal ± tolerance, else reduced.
 * @param {import("./loot-tables.js").LootTables["Animals"][string]} def
 * @param {number} displayTemp
 * @returns {number}
 */
export function getTemperatureFactor(def, displayTemp) {
  const ideal = def?.idealTemperature ?? 18;
  const tolerance = def?.temperatureTolerance ?? 5;
  const dist = Math.abs(displayTemp - ideal);
  if (dist <= tolerance) return 1;
  return Math.max(0.3, 1 - 0.2 * (dist / tolerance));
}

/**
 * Neighbor keys (edge-adjacent) for a cell key "x_y". Does not check bounds.
 * @param {string} key
 * @returns {string[]}
 */
function getNeighborKeys(key) {
  const m = key.match(/^(\d+)_(\d+)$/);
  if (!m) return [];
  const x = Number(m[1]);
  const y = Number(m[2]);
  return [cellKey(x - 1, y), cellKey(x + 1, y), cellKey(x, y - 1), cellKey(x, y + 1)];
}

/**
 * True if the two blocks (by origin key) are adjacent (any cell of one touches any cell of the other).
 * @param {import("./types.js").GameState} state
 * @param {string} keyA origin key "ox_oy"
 * @param {string} keyB origin key "ox_oy"
 * @returns {boolean}
 */
function blocksAreAdjacent(state, keyA, keyB) {
  const m1 = keyA.match(/^(\d+)_(\d+)$/);
  const m2 = keyB.match(/^(\d+)_(\d+)$/);
  if (!m1 || !m2) return false;
  const setA = new Set(getBlockKeysFromCell(state, Number(m1[1]), Number(m1[2])));
  const setB = new Set(getBlockKeysFromCell(state, Number(m2[1]), Number(m2[2])));
  for (const k of setA) {
    for (const neighbor of getNeighborKeys(k)) {
      if (setB.has(neighbor)) return true;
    }
  }
  return false;
}

/**
 * Collect origin animal entries (key, animalId, fromOtherZoo) from grid.
 * @param {import("./types.js").GameState} state
 * @returns {Array<{ key: string, animalId: string, fromOtherZoo: boolean }>}
 */
function collectOriginAnimals(state) {
  const cells = state.grid.cells;
  const origins = [];
  for (const [key, cell] of Object.entries(cells)) {
    if (cell === null || cell === undefined || cell.kind !== "animal" || !isOriginCell(key, cell)) {
      // skip
    } else {
      const def = LootTables.Animals[cell.id];
      if (def !== null && def !== undefined) {
        origins.push({
          key,
          animalId: cell.id,
          fromOtherZoo: cell.fromOtherZoo === true,
        });
      }
    }
  }
  return origins;
}

/**
 * Form unique pairs from origins (same animalId, at least one fromOtherZoo, adjacent).
 * @param {import("./types.js").GameState} state
 * @param {Array<{ key: string, animalId: string, fromOtherZoo: boolean }>} origins
 * @returns {Array<{ keyA: string, keyB: string, animalId: string }>}
 */
function formReproductionPairs(state, origins) {
  const pairs = [];
  for (let i = 0; i < origins.length; i++) {
    for (let j = i + 1; j < origins.length; j++) {
      const a = origins[i];
      const b = origins[j];
      if (a.animalId === b.animalId && (a.fromOtherZoo || b.fromOtherZoo) && blocksAreAdjacent(state, a.key, b.key)) {
        const keyA = a.key < b.key ? a.key : b.key;
        const keyB = a.key < b.key ? b.key : a.key;
        pairs.push({ keyA, keyB, animalId: a.animalId });
      }
    }
  }
  return pairs;
}

/**
 * All eligible reproduction pairs: same animalId, at least one fromOtherZoo, adjacent.
 * Returns unique pairs with keyA < keyB lexicographically.
 * @param {import("./types.js").GameState} state
 * @returns {Array<{ keyA: string, keyB: string, animalId: string }>}
 */
export function findReproductionPairs(state) {
  const origins = collectOriginAnimals(state);
  return formReproductionPairs(state, origins);
}

/**
 * Unique pair key for deduplication.
 * @param {string} keyA
 * @param {string} keyB
 * @returns {string}
 */
function pairKey(keyA, keyB) {
  return keyA < keyB ? `${keyA},${keyB}` : `${keyB},${keyA}`;
}

/**
 * Process due reproduction timers: add baby or sale listing, remove timer.
 * @param {import("./types.js").GameState} state
 * @param {number} nowUnix
 * @param {Array<{ keyA: string, keyB: string, animalId: string, dueAt: number }>} timers
 * @param {number} index
 */
function processDueTimer(state, nowUnix, timers, index) {
  const t = timers[index];
  if (t.dueAt > nowUnix) return;
  const [ok, result] = addPendingBaby(state, t.animalId, false);
  if (ok) {
    state.birthCount = (state.birthCount ?? 0) + 1;
  } else if (result === "NoFreeNursery") {
    state.saleListings = state.saleListings ?? [];
    const listingId = `sale_${state.nextTokenId}`;
    state.nextTokenId += 1;
    state.saleListings.push({
      id: listingId,
      zooId: state.myZooId ?? "player",
      animalId: t.animalId,
      isBaby: true,
      price: 50,
      endAt: nowUnix + 3600,
      reproductionScoreAtSale: getReproductionScore(state),
    });
    state.birthCount = (state.birthCount ?? 0) + 1;
  }
  timers.splice(index, 1);
}

/**
 * Remove timers whose cells are no longer valid animals.
 * @param {import("./types.js").GameState} state
 * @param {Array<{ keyA: string, keyB: string }>} timers
 */
function pruneInvalidTimers(state, timers) {
  const cells = state.grid.cells;
  for (let i = timers.length - 1; i >= 0; i--) {
    const t = timers[i];
    const cellA = cells[t.keyA];
    const cellB = cells[t.keyB];
    if (!cellA || cellA.kind !== "animal" || !cellB || cellB.kind !== "animal") {
      timers.splice(i, 1);
    }
  }
}

/**
 * Add new reproduction pairs to timers with dueAt.
 * @param {import("./types.js").GameState} state
 * @param {number} nowUnix
 * @param {Array<{ keyA: string, keyB: string, animalId: string, dueAt: number }>} timers
 * @param {Set<string>} existingSet
 */
function addNewPairsToTimers(state, nowUnix, timers, existingSet) {
  const baseSeconds = GameConfig.Reproduction?.BaseSeconds ?? 60;
  const currentPairs = findReproductionPairs(state);
  const score = getReproductionScore(state);
  const grid = state.grid;
  for (const { keyA, keyB, animalId } of currentPairs) {
    const pk = pairKey(keyA, keyB);
    if (existingSet.has(pk)) {
      // skip already tracked pair
    } else {
      const def = LootTables.Animals[animalId];
      if (def !== null && def !== undefined) {
        const m1 = keyA.match(/^(\d+)_(\d+)$/);
        const m2 = keyB.match(/^(\d+)_(\d+)$/);
        if (m1 && m2) {
          const season = getCurrentSeason(state);
          const seasonTempMod = getSeasonTemperatureModifier(season);
          const biome1 = getDisplayBiome(Number(m1[1]), Number(m1[2]), grid);
          const biome2 = getDisplayBiome(Number(m2[1]), Number(m2[2]), grid);
          const temp1 = getDisplayTemperature(Number(m1[1]), Number(m1[2]), grid) + seasonTempMod;
          const temp2 = getDisplayTemperature(Number(m2[1]), Number(m2[2]), grid) + seasonTempMod;
          const biomeFactor = (getBiomeReproductionFactor(def, biome1) + getBiomeReproductionFactor(def, biome2)) / 2;
          const tempFactor = (getTemperatureFactor(def, temp1) + getTemperatureFactor(def, temp2)) / 2;
          const seasonBonus = getEffectiveReproductionSeasonBonus(season, def);
          const factor = Math.max(0.2, score * biomeFactor * tempFactor * (1 + seasonBonus));
          const delay = Math.max(5, baseSeconds / factor);
          timers.push({ keyA, keyB, animalId, dueAt: nowUnix + Math.floor(delay) });
          existingSet.add(pk);
        }
      }
    }
  }
}

/**
 * Run reproduction tick: spawn babies for due timers, then register new pairs with dueAt.
 * @param {import("./types.js").GameState} state
 * @param {number} nowUnix
 */
export function tickReproduction(state, nowUnix) {
  const timers = state.reproductionTimers ?? [];

  for (let i = timers.length - 1; i >= 0; i--) {
    processDueTimer(state, nowUnix, timers, i);
  }

  const existingSet = new Set(timers.map((t) => pairKey(t.keyA, t.keyB)));
  pruneInvalidTimers(state, timers);
  addNewPairsToTimers(state, nowUnix, timers, existingSet);
  state.reproductionTimers = timers;
}
