(() => {
  "use strict";

  const qs = (sel) => document.querySelector(sel);

  const DIFFICULTY_ORDER = {
    Normal: 0,
    Hard: 1,
    "Very Hard": 2,
    Ultimate: 3,
  };

  const NUMERIC_COLUMNS = new Set([
    "hp", "atp", "dfp", "mst", "ata", "evp", "lck", "esp", "exp",
    "efr", "eic", "eth", "elt", "edk",
  ]);

  const BP_TIERS = {
    0: { hp: 1.00, atp: 1.00, exp: 1.00 },
    1: { hp: 1.10, atp: 1.01, exp: 1.10 },
    2: { hp: 1.15, atp: 1.02, exp: 1.15 },
    3: { hp: 1.20, atp: 1.03, exp: 1.20 },
    4: { hp: 1.30, atp: 1.04, exp: 1.30 },
    5: { hp: 1.40, atp: 1.05, exp: 1.40 },
    6: { hp: 1.50, atp: 1.06, exp: 1.50 },
    7: { hp: 1.75, atp: 1.07, exp: 1.75 },
    8: { hp: 2.00, atp: 1.08, exp: 2.00 },
    9: { hp: 2.50, atp: 1.09, exp: 2.50 },
    10: { hp: 3.00, atp: 1.10, exp: 3.00 },
    11: { hp: 4.00, atp: 1.10, exp: 3.00 },
  };

  const COLUMN_GROUPS = {
    stats: [
      ["enemy", "Enemy"],
      ["mode", "Mode"],
      ["episode", "Episode"],
      ["difficulty", "Difficulty"],
      ["hp", "HP"],
      ["atp", "ATP"],
      ["dfp", "DFP"],
      ["ata", "ATA"],
      ["evp", "EVP"],
      ["lck", "LCK"],
      ["exp", "EXP"],
    ],
    resists: [
      ["enemy", "Enemy"],
      ["mode", "Mode"],
      ["episode", "Episode"],
      ["difficulty", "Difficulty"],
      ["efr", "EFR"],
      ["eic", "EIC"],
      ["eth", "ETH"],
      ["elt", "ELT"],
      ["edk", "EDK"],
      ["esp", "ESP"],
    ],
    all: [
      ["enemy", "Enemy"],
      ["mode", "Mode"],
      ["episode", "Episode"],
      ["difficulty", "Difficulty"],
      ["hp", "HP"],
      ["atp", "ATP"],
      ["dfp", "DFP"],
      ["mst", "MST"],
      ["ata", "ATA"],
      ["evp", "EVP"],
      ["lck", "LCK"],
      ["esp", "ESP"],
      ["exp", "EXP"],
      ["efr", "EFR"],
      ["eic", "EIC"],
      ["eth", "ETH"],
      ["elt", "ELT"],
      ["edk", "EDK"],
    ],
  };

  function currentColumns() {
    const view = qs("#bestiary-view")?.value || "stats";
    return COLUMN_GROUPS[view] || COLUMN_GROUPS.stats;
  }

  const state = {
    rows: [],
    index: null,
    table: null,
    page: 1,
    pageSize: 100,
    sort: {
      key: "enemy",
      dir: "asc",
    },
    filters: {
      mode: "",
      episode: "",
      difficulty: "",
      search: "",
    },
  };

  function esc(value) {
    return String(value ?? "")
      .replaceAll("&", "&amp;")
      .replaceAll("<", "&lt;")
      .replaceAll(">", "&gt;")
      .replaceAll('"', "&quot;")
      .replaceAll("'", "&#039;");
  }

  async function fetchJson(path) {
    const res = await fetch(path, { cache: "no-store" });
    if (!res.ok) throw new Error(`${path}: HTTP ${res.status}`);
    return res.json();
  }

  function setStatus(message, kind = "") {
    const box = qs("#bestiary-placeholder");
    if (!box) return;
    box.innerHTML = `<div class="drops-status ${kind ? `drops-status--${kind}` : ""}">${esc(message)}</div>`;
  }

  function uniqueSorted(rows, key) {
    return [...new Set(rows.map((row) => row[key]).filter(Boolean))]
      .sort((a, b) => String(a).localeCompare(String(b), undefined, { numeric: true }));
  }

  function fillSelect(select, values, allLabel) {
    if (!select) return;
    const previous = select.value;
    select.innerHTML = `<option value="">${esc(allLabel)}</option>`;
    for (const value of values) {
      const opt = document.createElement("option");
      opt.value = value;
      opt.textContent = value;
      select.appendChild(opt);
    }
    if ([...select.options].some((opt) => opt.value === previous)) {
      select.value = previous;
    }
  }

  function orderedDifficulties(rows) {
    const present = new Set(rows.map((row) => row.difficulty).filter(Boolean));
    return Object.keys(DIFFICULTY_ORDER).filter((value) => present.has(value));
  }

  function bpTier() {
    const select = qs("#bestiary-tier");
    const raw = String(select?.value || "");
    const label = String(select?.selectedOptions?.[0]?.textContent || "");
    const match = raw.match(/(\d+)$/) || label.match(/(\d+)/);
    const tier = match ? Number(match[1]) : 0;
    return BP_TIERS[tier] || BP_TIERS[0];
  }

  function xpBonusMultiplier() {
    return Number(qs("#bestiary-xp-bonus")?.value || 1);
  }

  function displayValue(row, key) {
    const value = row[key];

    if (key === "exp" && row.difficulty !== "Ultimate") {
      return Math.round(Number(value || 0) * xpBonusMultiplier());
    }

    if (row.difficulty !== "Ultimate") {
      return value ?? "";
    }

    const tier = bpTier();

    if (key === "hp") {
      return Math.round(Number(value || 0) * tier.hp);
    }

    if (key === "atp") {
      return Math.round(Number(value || 0) * tier.atp);
    }

    if (key === "exp" && tier.exp !== null) {
      return Math.round(Number(value || 0) * tier.exp * xpBonusMultiplier());
    }

    return value ?? "";
  }

  function hasAnyBestiaryValue(row) {
    const keys = ["hp", "atp", "dfp", "mst", "ata", "evp", "lck", "esp", "exp", "efr", "eic", "eth", "elt", "edk"];
    return keys.some((key) => Number(row[key] || 0) !== 0);
  }

  function visibleRows() {
    const search = state.filters.search.trim().toLowerCase();

    return state.rows.filter((row) => {
      if (!hasAnyBestiaryValue(row)) return false;
      if (state.filters.mode && row.mode !== state.filters.mode) return false;
      if (state.filters.episode && row.episode !== state.filters.episode) return false;
      if (state.filters.difficulty && row.difficulty !== state.filters.difficulty) return false;

      if (search) {
        const haystack = [row.enemy, row.enemy_key, row.mode, row.episode, row.difficulty]
          .join(" ")
          .toLowerCase();
        if (!haystack.includes(search)) return false;
      }

      return true;
    });
  }

  function sortValue(row, key) {
    if (key === "difficulty") return DIFFICULTY_ORDER[row.difficulty] ?? 999;
    if (NUMERIC_COLUMNS.has(key)) return Number(displayValue(row, key) || 0);
    return row[key] || "";
  }

  function sortedRows(rows) {
    const { key, dir } = state.sort;
    const factor = dir === "desc" ? -1 : 1;

    return [...rows].sort((a, b) => {
      const av = sortValue(a, key);
      const bv = sortValue(b, key);

      if (typeof av === "number" && typeof bv === "number") {
        return (av - bv) * factor;
      }

      return String(av).localeCompare(String(bv), undefined, {
        numeric: true,
        sensitivity: "base",
      }) * factor;
    });
  }

  function sortHeader(key, label) {
    const active = state.sort.key === key;
    const arrow = active ? (state.sort.dir === "asc" ? "▲" : "▼") : "";
    const ariaSort = active ? (state.sort.dir === "asc" ? "ascending" : "descending") : "none";

    return `<th aria-sort="${ariaSort}">
      <button class="drops-sort-button ${active ? "is-active" : ""}" type="button" data-bestiary-sort="${esc(key)}">
        <span>${esc(label)}</span>
        <span class="drops-sort-arrow" aria-hidden="true">${arrow}</span>
      </button>
    </th>`;
  }

  function renderTable() {
    const box = qs("#bestiary-placeholder");
    if (!box) return;

    const rows = sortedRows(visibleRows());
    const totalPages = Math.max(1, Math.ceil(rows.length / state.pageSize));
    state.page = Math.min(Math.max(1, state.page), totalPages);

    const start = (state.page - 1) * state.pageSize;
    const shown = rows.slice(start, start + state.pageSize);
    const end = start + shown.length;

    const columns = currentColumns();
    if (!columns.some(([key]) => key === state.sort.key)) {
      state.sort.key = "enemy";
      state.sort.dir = "asc";
    }

    const body = shown.map((row) => `
      <tr>
        ${columns.map(([key]) => `<td data-label="${esc(key)}">${esc(displayValue(row, key))}</td>`).join("")}
      </tr>
    `).join("");

    const rangeText = rows.length
      ? ` Showing ${Number(start + 1).toLocaleString()}-${Number(end).toLocaleString()}.`
      : "";

    box.innerHTML = `
      <div class="drops-summary">
        <div>
          <strong>BB Bestiary</strong>
          <span>${rows.length.toLocaleString()} matching rows.${rangeText}</span>
        </div>
        <span>${state.rows.length.toLocaleString()} total rows</span>
      </div>
      <div class="drops-table-wrap">
        <table class="drops-table bestiary-table">
          <thead>
            <tr>${columns.map(([key, label]) => sortHeader(key, label)).join("")}</tr>
          </thead>
          <tbody>${body || `<tr><td colspan="${columns.length}">No enemies match these filters.</td></tr>`}</tbody>
        </table>
      </div>
      <div class="leaderboard-pager drops-pager">
        <button type="button" data-bestiary-page="prev" ${state.page <= 1 ? "disabled" : ""}>Previous</button>
        <span>Page ${state.page} of ${totalPages}</span>
        <button type="button" data-bestiary-page="next" ${state.page >= totalPages ? "disabled" : ""}>Next</button>
      </div>
    `;
  }

  function populateFilters() {
    fillSelect(qs("#bestiary-mode"), uniqueSorted(state.rows, "mode"), "All modes");
    fillSelect(qs("#bestiary-episode"), uniqueSorted(state.rows, "episode"), "All episodes");
    fillSelect(qs("#bestiary-difficulty"), orderedDifficulties(state.rows), "All difficulties");
  }

  async function loadBestiary() {
    setStatus("Loading BB bestiary...");

    state.index = await fetchJson("generated/bestiary/bb/index.json");
    state.table = state.index.tables?.[0] || null;
    state.rows = await fetchJson(`generated/bestiary/bb/${state.table.path}`);

    populateFilters();
    renderTable();
  }

  document.addEventListener("DOMContentLoaded", () => {
    qs("#bestiary-mode")?.addEventListener("change", (event) => {
      state.filters.mode = event.target.value;
      state.page = 1;
      renderTable();
    });

    qs("#bestiary-episode")?.addEventListener("change", (event) => {
      state.filters.episode = event.target.value;
      state.page = 1;
      renderTable();
    });

    qs("#bestiary-difficulty")?.addEventListener("change", (event) => {
      state.filters.difficulty = event.target.value;
      state.page = 1;
      renderTable();
    });

    qs("#bestiary-view")?.addEventListener("change", () => {
      state.page = 1;
      renderTable();
    });

    qs("#bestiary-search")?.addEventListener("input", (event) => {
      state.filters.search = event.target.value;
      state.page = 1;
      renderTable();
    });

    qs("#bestiary-tier")?.addEventListener("change", () => {
      state.page = 1;
      renderTable();
    });

    qs("#bestiary-xp-bonus")?.addEventListener("change", () => {
      renderTable();
    });

    qs("#bestiary-placeholder")?.addEventListener("click", (event) => {
      const pageButton = event.target.closest("[data-bestiary-page]");
      if (pageButton) {
        state.page += pageButton.dataset.bestiaryPage === "next" ? 1 : -1;
        renderTable();
        return;
      }

      const button = event.target.closest("[data-bestiary-sort]");
      if (!button) return;

      const key = button.dataset.bestiarySort;
      if (state.sort.key === key) {
        state.sort.dir = state.sort.dir === "asc" ? "desc" : "asc";
      } else {
        state.sort.key = key;
        state.sort.dir = "asc";
      }

      state.page = 1;
      renderTable();
    });

    loadBestiary().catch((err) => {
      setStatus(err?.message || "Unable to load bestiary.", "error");
    });
  });
})();
