/**
 * Logique métier pure du site public : normalisation, filtrage, tri.
 *
 * Extraite d'app.js pour être testable. Toutes les fonctions ici sont sans
 * effet de bord et ne lisent AUCUN état global : ce qu'elles filtrent leur est
 * passé en argument. app.js conserve l'état (les Map de cases cochées) et
 * délègue la décision à ce module.
 *
 * Chargé par <script> avant app.js (donc via la globale `BMPure`), et importé
 * en CommonJS par les tests — d'où le pied de fichier. Pas d'étape de build.
 */
// `var` volontaire (et non const) : dans un script classique, seul `var` crée
// une propriété sur l'objet global. app.js s'en sortirait avec `const`, mais
// pas le harnais de test, qui évalue ce fichier dans un contexte `vm` et lit
// `sandbox.BMPure` — exactement comme le navigateur lit `window.BMPure`.
// eslint-disable-next-line no-var
var BMPure = (function () {
  "use strict";

  function escapeHtml(s) {
    return String(s ?? "").replace(
      /[&<>"']/g,
      (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#039;" }[c])
    );
  }

  function norm(s) {
    if (s == null || s === "") return "";
    return String(s).trim();
  }

  /** Extrait une année d'un nombre ou d'un texte libre ("formé en 1991"). */
  function parseYear(val) {
    if (val == null) return null;
    const n = Number(val);
    if (Number.isFinite(n)) return n;
    const m = String(val).match(/(19|20)\d{2}/);
    return m ? Number(m[0]) : null;
  }

  function splitThemes(val) {
    if (!val) return [];
    if (Array.isArray(val)) return val.map(norm).filter(Boolean);
    return String(val).split(/[,;/]/).map(norm).filter(Boolean);
  }

  function matchesQuery(b, q) {
    if (!q) return true;
    const themes = Array.isArray(b.themes) ? b.themes.join(" ") : "";
    const hay = `${b.name} ${b.genre} ${b.status} ${b.country} ${b.location_text} ${themes}`.toLowerCase();
    return hay.includes(q);
  }

  /**
   * Logique tri-état partagée par les filtres statut / pays / genre.
   *
   * Une valeur ABSENTE de la Map passe (la facette n'existe pas dans le jeu de
   * données courant : on ne veut pas masquer des groupes à cause d'une facette
   * inconnue). Une valeur présente ne passe que si elle est à true.
   * Les valeurs vides retombent sur une clé de repli ("Unknown", "??").
   */
  function matchesFacet(rawValue, enabledMap, fallbackKey) {
    const v = norm(rawValue);
    const key = v || fallbackKey;
    if (!enabledMap.has(key)) return true;
    return enabledMap.get(key) === true;
  }

  function matchesMacroGenre(b, macroGenres, macroGenreEnabled, macroActiveCount) {
    if (macroActiveCount === 0) return true;
    const g = norm(b.genre).toLowerCase();
    if (!g) return false;
    return macroGenres.some(
      (m) => macroGenreEnabled.get(m.key) === true && m.terms.some((t) => g.includes(t))
    );
  }

  function matchesTheme(b, themeEnabled) {
    if (!themeEnabled.size) return true;
    const allOn = Array.from(themeEnabled.values()).every((v) => v === true);
    if (allOn) return true;
    const tokens = Array.isArray(b.themes) ? b.themes : [];
    if (!tokens.length) return false;
    return tokens.some((t) => themeEnabled.get(t) === true);
  }

  /**
   * Un groupe sans année reste visible tant que l'utilisateur n'a pas resserré
   * la plage ; dès qu'il la resserre, l'absence d'année devient un motif
   * d'exclusion (sinon la sélection « 1990-1995 » ramènerait des groupes sans
   * date, ce qui est incompréhensible).
   */
  function matchesYear(b, yearFilter, yearRange) {
    if (yearFilter.min == null || yearFilter.max == null) return true;
    if (yearRange.min == null || yearRange.max == null) return true;

    const isFullRange = yearFilter.min === yearRange.min && yearFilter.max === yearRange.max;
    if (isFullRange) return true;

    const y = b.formed_year;
    if (!Number.isFinite(y)) return false;
    return y >= yearFilter.min && y <= yearFilter.max;
  }

  /** Clé d'agrégation des groupes partageant exactement les mêmes coordonnées. */
  function keyFromLatLon(lat, lon) {
    return `${Number(lat).toFixed(6)},${Number(lon).toFixed(6)}`;
  }

  const byName = (x, y) =>
    (x.name || "").localeCompare(y.name || "", "fr", { sensitivity: "base" });

  /** Tri stable, sans effet de bord : renvoie une nouvelle liste. */
  function sortBands(arr, mode) {
    const a = [...arr];
    const byField = (f) => (x, y) =>
      (x[f] || "").localeCompare(y[f] || "", "fr", { sensitivity: "base" }) || byName(x, y);

    if (mode === "status") a.sort(byField("status"));
    else if (mode === "genre") a.sort(byField("genre"));
    else if (mode === "country") a.sort(byField("country"));
    else if (mode === "year") {
      // Les groupes sans année partent en fin de liste (-1).
      a.sort(
        (x, y) =>
          (Number.isFinite(y.formed_year) ? y.formed_year : -1) -
          (Number.isFinite(x.formed_year) ? x.formed_year : -1)
      );
    } else a.sort(byName);

    return a;
  }

  return {
    escapeHtml,
    norm,
    parseYear,
    splitThemes,
    matchesQuery,
    matchesFacet,
    matchesMacroGenre,
    matchesTheme,
    matchesYear,
    keyFromLatLon,
    sortBands,
  };
})();

if (typeof module !== "undefined" && module.exports) module.exports = BMPure;
