/* Black Metal Map UI — clusters/heat/timeline/filter/sort/highlight */
/* Performance-optimized with viewport-based loading */

const API_BASE = "https://bm.nicolasfryder.ovh";

// --- i18n ---
const SUPPORTED_LANGS = ["fr", "en", "de", "es", "it", "pl", "nl", "ro", "pt", "cs", "sv", "hu", "da", "fi", "sk", "hr", "sl", "lt", "lv", "et", "nb"];

function detectLang() {
  const langs = navigator.languages?.length ? navigator.languages : [navigator.language || "en"];
  for (const l of langs) {
    const code = l.split("-")[0].toLowerCase();
    if (SUPPORTED_LANGS.includes(code)) return code;
  }
  return "en";
}

let currentLang = localStorage.getItem("lang") || detectLang();

function t(key) {
  return window.LOCALES?.[currentLang]?.[key] ?? window.LOCALES?.["en"]?.[key] ?? key;
}

function applyI18n() {
  document.documentElement.lang = currentLang;
  document.querySelectorAll("[data-i18n]").forEach(el => {
    el.textContent = t(el.dataset.i18n);
  });
  document.querySelectorAll("[data-i18n-placeholder]").forEach(el => {
    el.placeholder = t(el.dataset.i18nPlaceholder);
  });
  // Sort select options (can't use data-i18n on options in all browsers)
  const sel = document.getElementById("sortSelect");
  if (sel) {
    [["sort_az",0],["sort_status",1],["sort_genre",2],["sort_country",3],["sort_year",4]].forEach(([k,i]) => {
      if (sel.options[i]) sel.options[i].text = t(k);
    });
  }
}
const DARK_TILES = "https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png";
const DARK_ATTRIB = '&copy; OpenStreetMap contributors &copy; CARTO';

// --- Configuration ---
const USE_VIEWPORT_LOADING = true;  // Enable viewport-based loading
const INITIAL_LOAD_LIMIT = 5000;    // Initial bands to load for filters
const VIEWPORT_DEBOUNCE_MS = 300;   // Debounce for viewport changes

// --- State ---
let allBands = [];          // all loaded bands (geocoded + non geocoded)
let geocodedBands = [];     // subset lat/lon ok
let noLocationBands = [];   // subset missing coords
let viewportBands = [];     // bands in current viewport

let filtered = [];
let markersById = new Map();   // ma_id -> marker
let listElById = new Map();    // ma_id -> element
let coordIndex = new Map();    // "lat,lon" -> bands[]

let heatLayer = null;
let clusterLayer = null;
let currentMode = "clusters";

// All heat points (for full heatmap even when viewing clusters)
let allHeatPoints = [];
let heatPointsLoaded = false;

const statusEnabled = new Map();
const genreEnabled = new Map();
const countryEnabled = new Map();
const macroGenreEnabled = new Map();
const themeEnabled = new Map();

let yearRange = { min: null, max: null };     // global
let yearFilter = { min: null, max: null };    // current

let macroActiveCount = 0;

// Facet data from server (for filters)
let facetData = null;

// Loading state
let isLoadingViewport = false;
let pendingViewportLoad = false;
let currentViewportData = [];

// --- Utils ---
const $ = (id) => document.getElementById(id);
const appEl = $("app");

async function loadGoatCounterFooterStats() {
  const totalEl = document.getElementById("gcTotal");
  const monthEl = document.getElementById("gcMonth");
  if (!totalEl || !monthEl) return;

  // 1er jour du mois courant (ce mois-ci)
  const d = new Date();
  const yyyy = d.getFullYear();
  const mm = String(d.getMonth() + 1).padStart(2, "0");
  const startOfMonth = `${yyyy}-${mm}-01`;

  const base = "https://metalfromeurope.goatcounter.com/counter/";
  const totalUrl = `${base}TOTAL.json`;
  const monthUrl = `${base}TOTAL.json?start=${encodeURIComponent(startOfMonth)}`;

  try {
    const [t, m] = await Promise.all([
      fetch(totalUrl, { cache: "no-store" }).then(r => (r.ok ? r.json() : Promise.reject(r.status))),
      fetch(monthUrl, { cache: "no-store" }).then(r => (r.ok ? r.json() : Promise.reject(r.status))),
    ]);

    // GoatCounter renvoie `count` comme string formatée (séparateurs milliers). :contentReference[oaicite:2]{index=2}
    totalEl.textContent = t.count ?? "—";
    monthEl.textContent = m.count ?? "—";
  } catch (e) {
    console.warn("GoatCounter footer stats failed:", e);
    totalEl.textContent = "—";
    monthEl.textContent = "—";
  }
}

// Debounce function for search
let searchDebounceTimer = null;
function debounce(func, delay) {
  return function(...args) {
    clearTimeout(searchDebounceTimer);
    searchDebounceTimer = setTimeout(() => func.apply(this, args), delay);
  };
}

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

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

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 currentQuery() {
  return norm($("q").value).toLowerCase();
}

function bandMatchesQuery(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);
}

function bandMatchesStatus(b) {
  const s = norm(b.status);
  if (!s) {
    // If no status, check if "Unknown" is enabled
    if (!statusEnabled.has("Unknown")) return true;
    return statusEnabled.get("Unknown") === true;
  }
  if (!statusEnabled.has(s)) return true;
  return statusEnabled.get(s) === true;
}

function bandMatchesCountry(b) {
  const c = norm(b.country);
  if (!c) {
    // If no country, check if "??" is enabled
    if (!countryEnabled.has("??")) return true;
    return countryEnabled.get("??") === true;
  }
  if (!countryEnabled.has(c)) return true;
  return countryEnabled.get(c) === true;
}

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

function bandMatchesGenre(b) {
  if (!bandMatchesMacroGenre(b)) return false;
  const g = norm(b.genre);
  if (!g) {
    // If no genre, check if "Unknown" is enabled
    if (!genreEnabled.has("Unknown")) return true;
    return genreEnabled.get("Unknown") === true;
  }
  if (!genreEnabled.has(g)) return true;
  return genreEnabled.get(g) === true;
}

function bandMatchesTheme(b) {
  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);
}

function bandMatchesYear(b) {
  const y = b.formed_year;

  // If no filter is set, pass all
  if (yearFilter.min == null || yearFilter.max == null) return true;

  // If yearRange not initialized yet, pass all
  if (yearRange.min == null || yearRange.max == null) return true;

  // If filter is at full range, pass all (including those without years)
  const isFullRange = (yearFilter.min === yearRange.min && yearFilter.max === yearRange.max);

  if (isFullRange) {
    return true;
  }

  // If user has narrowed the range, only show bands with valid years in range
  if (!Number.isFinite(y)) {
    // Band has no year and user has filtered by year -> exclude
    return false;
  }

  return y >= yearFilter.min && y <= yearFilter.max;
}

function keyFromLatLon(lat, lon) {
  const la = Number(lat).toFixed(6);
  const lo = Number(lon).toFixed(6);
  return `${la},${lo}`;
}

const MACRO_GENRES = [
  { key: "Black", terms: ["black"] },
  { key: "Death", terms: ["death"] },
  { key: "Doom/Stoner/Sludge", terms: ["doom", "stoner", "sludge"] },
  { key: "Electronic/Industrial", terms: ["electronic", "industrial", "electro"] },
  { key: "Experimental/Avant-garde", terms: ["experimental", "avant", "avant-garde", "avantgarde"] },
  { key: "Folk/Viking/Pagan", terms: ["folk", "viking", "pagan"] },
  { key: "Gothic", terms: ["gothic"] },
  { key: "Grindcore", terms: ["grind"] },
  { key: "Groove", terms: ["groove"] },
  { key: "Heavy", terms: ["heavy"] },
  { key: "Metalcore/Deathcore", terms: ["metalcore", "deathcore"] },
  { key: "Power", terms: ["power"] },
  { key: "Progressive", terms: ["progressive", "prog"] },
  { key: "Speed", terms: ["speed"] },
  { key: "Symphonic", terms: ["symphonic"] },
  { key: "Thrash", terms: ["thrash"] },
];

// --- API ---
async function apiGet(path) {
  const url = `${API_BASE}${path}`;
  const r = await fetch(url, { mode: "cors" });
  if (!r.ok) throw new Error(`API ${path} failed: ${r.status}`);
  return await r.json();
}

async function loadStats() {
  // tolérant : si /api/stats n'existe pas encore, on ignore
  try {
    const j = await apiGet("/api/stats");
    if (j && j.ok) {
      $("total").textContent = String(j.total || 0);
      $("geocoded").textContent = String(j.geocoded || 0);
      $("noLocCount").textContent = String(j.no_location || 0);
    }
  } catch (_) {
    // Silently ignore stats errors
  }
}

/**
 * Load facet data for filters (fast, minimal data)
 */
async function loadFacets() {
  try {
    const j = await apiGet("/api/facets");
    if (j.ok) {
      facetData = j;
      console.log("Facets loaded:", {
        statuses: j.statuses?.length,
        countries: j.countries?.length,
        genres: j.genres?.length,
        yearRange: j.year_range
      });
    }
  } catch (e) {
    console.warn("Failed to load facets, will compute from bands:", e);
  }
}

/**
 * Parse band data from API response
 */
function parseBandData(x) {
  let status = x.status || x.data?.status || x.data?.band_status || x.data?.status_label;

  return {
    ma_id: Number(x.ma_id),
    name: norm(x.name),
    url: x.url || (x.data?.url) || (x.data?.band_page?.url) || null,
    country: norm(x.country || x.data?.country),
    status: norm(status),
    genre: norm(x.genre || x.data?.genre || x.data?.genres || x.data?.style),
    themes: splitThemes(x.themes || x.data?.themes || x.data?.theme || x.data?.thematic),
    location_text: norm(x.location_text || x.data?.location_text || x.data?.location),
    formed_year: parseYear(x.formed_year ?? x.data?.formed_year ?? x.data?.formed ?? x.data?.formation_year ?? x.data?.year_formed),
    lat: (x.lat == null ? null : Number(x.lat)),
    lon: (x.lon == null ? null : Number(x.lon)),
    // Localisation précise (band_locations) : présent quand le point vient
    // d'un step géocodé plutôt que du point unique du groupe.
    location_raw: x.location_raw != null ? norm(x.location_raw) : null,
    step_label: x.step_label != null ? norm(x.step_label) : null,
    data: x.data || {},
    geocoded_at: x.geocoded_at || null,
  };
}

/**
 * Load initial sample of bands for quick startup
 */
async function loadBands() {
  // Load a limited set initially for faster startup
  const limit = USE_VIEWPORT_LOADING ? INITIAL_LOAD_LIMIT : 200000;
  const j = await apiGet(`/api/bands?limit=${limit}`);
  const items = j.items || j.bands || [];

  console.log(`Loaded ${items.length} bands initially`);

  allBands = items.map(parseBandData);

  geocodedBands = allBands.filter(b => Number.isFinite(b.lat) && Number.isFinite(b.lon));
  noLocationBands = allBands.filter(b => !(Number.isFinite(b.lat) && Number.isFinite(b.lon)));

  // Update counts
  $("total").textContent = String(allBands.length);
  $("geocoded").textContent = String(geocodedBands.length);
  $("noLocCount").textContent = String(noLocationBands.length);
}

/**
 * Load all heat points for the full heatmap (done once in background)
 */
async function loadAllHeatPoints() {
  if (heatPointsLoaded) return;

  try {
    console.log("Loading all bands for heatmap...");
    const j = await apiGet("/api/bands?limit=200000");
    const items = j.items || [];

    allHeatPoints = [];

    // If allBands is empty, populate it
    if (!allBands.length) {
      allBands = items.map(parseBandData);
      geocodedBands = allBands.filter(b => Number.isFinite(b.lat) && Number.isFinite(b.lon));
      noLocationBands = allBands.filter(b => !(Number.isFinite(b.lat) && Number.isFinite(b.lon)));
    }

    // Index by coords for heatmap intensity
    const coordCounts = new Map();

    for (const x of items) {
      const lat = x.lat == null ? null : Number(x.lat);
      const lon = x.lon == null ? null : Number(x.lon);

      if (Number.isFinite(lat) && Number.isFinite(lon)) {
        const key = `${lat.toFixed(4)},${lon.toFixed(4)}`;
        coordCounts.set(key, (coordCounts.get(key) || 0) + 1);
      }
    }

    // Convert to heat points
    for (const [key, count] of coordCounts) {
      const [lat, lon] = key.split(",").map(Number);
      allHeatPoints.push([lat, lon, count]);
    }

    heatPointsLoaded = true;
    console.log(`Loaded ${allHeatPoints.length} unique heat points from ${items.length} bands`);

    // Update stats
    $("total").textContent = String(items.length);
    $("geocoded").textContent = String(geocodedBands.length);
    $("noLocCount").textContent = String(noLocationBands.length);

    // Rebuild heatmap if in heat mode
    if (currentMode === "heat") {
      buildHeatLayer(allHeatPoints);
    }
  } catch (e) {
    console.error("Failed to load heat points:", e);
  }
}

/**
 * Load bands for current viewport from server
 */
async function loadViewportBands() {
  if (!USE_VIEWPORT_LOADING) return;
  if (isLoadingViewport) {
    pendingViewportLoad = true;
    return;
  }

  isLoadingViewport = true;

  try {
    const bounds = map.getBounds();
    const zoom = map.getZoom();

    // Expand bounds slightly to preload edges
    const expandFactor = 0.2;
    const latDiff = (bounds.getNorth() - bounds.getSouth()) * expandFactor;
    const lonDiff = (bounds.getEast() - bounds.getWest()) * expandFactor;

    const bbox = [
      bounds.getWest() - lonDiff,
      bounds.getSouth() - latDiff,
      bounds.getEast() + lonDiff,
      bounds.getNorth() + latDiff
    ].join(",");

    // Build query params for filters
    const params = new URLSearchParams({
      bbox,
      zoom: String(Math.floor(zoom))
    });

    // Add active filters
    const activeStatuses = Array.from(statusEnabled.entries())
      .filter(([k, v]) => v)
      .map(([k]) => k);
    if (activeStatuses.length && activeStatuses.length < statusEnabled.size) {
      params.set("status", activeStatuses.join(","));
    }

    const activeCountries = Array.from(countryEnabled.entries())
      .filter(([k, v]) => v)
      .map(([k]) => k);
    if (activeCountries.length && activeCountries.length < countryEnabled.size) {
      params.set("countries", activeCountries.join(","));
    }

    if (yearFilter.min != null && yearFilter.max != null) {
      if (yearFilter.min !== yearRange.min) params.set("year_min", String(yearFilter.min));
      if (yearFilter.max !== yearRange.max) params.set("year_max", String(yearFilter.max));
    }

    // Add genre filter if macro genres are active
    if (macroActiveCount > 0) {
      const activeTerms = MACRO_GENRES
        .filter(m => macroGenreEnabled.get(m.key) === true)
        .flatMap(m => m.terms);
      if (activeTerms.length) {
        params.set("genre", activeTerms[0]); // API supports single genre filter
      }
    }

    const url = `/api/clusters?${params.toString()}`;
    console.log("Loading viewport:", url);

    const j = await apiGet(url);

    if (j.ok) {
      currentViewportData = j.items || [];

      if (j.type === "bands") {
        // High zoom: got individual bands
        viewportBands = currentViewportData.map(parseBandData);
        rebuildLayersFromBands(viewportBands);
      } else {
        // Low zoom: got clusters
        rebuildLayersFromClusters(currentViewportData);
      }

      $("count").textContent = String(j.count || currentViewportData.length || 0);
    }
  } catch (e) {
    console.error("Failed to load viewport bands:", e);
  } finally {
    isLoadingViewport = false;

    if (pendingViewportLoad) {
      pendingViewportLoad = false;
      setTimeout(loadViewportBands, 100);
    }
  }
}

/**
 * Create a cluster marker (zoomable, not final location)
 * These have a dashed border to indicate they can be zoomed
 * Burgundy/dark red color, always white text
 */
function createClusterMarker(lat, lon, count, bands) {
  // Size based on count (logarithmic scale)
  const size = Math.min(24 + Math.log2(count + 1) * 8, 55);

  // Constant burgundy/dark red color for all clusters
  const color = 'rgba(120, 40, 60, 0.92)';
  const borderColor = 'rgba(180, 80, 100, 0.9)';
  const textColor = '#fff';

  // Create custom div icon with count - dashed border indicates "zoomable"
  const icon = L.divIcon({
    className: 'cluster-marker cluster-zoomable',
    html: `<div style="
      width: ${size}px;
      height: ${size}px;
      background: ${color};
      border: 3px dashed ${borderColor};
      border-radius: 50%;
      display: flex;
      align-items: center;
      justify-content: center;
      font-size: ${Math.max(11, size / 3)}px;
      font-weight: bold;
      color: ${textColor};
      box-shadow: 0 3px 12px rgba(0,0,0,0.4);
      cursor: zoom-in;
      transition: transform 0.15s ease;
    " onmouseover="this.style.transform='scale(1.1)'" onmouseout="this.style.transform='scale(1)'">${count > 999 ? Math.round(count/1000) + 'k' : count}</div>`,
    iconSize: [size, size],
    iconAnchor: [size/2, size/2]
  });

  const marker = L.marker([lat, lon], { icon });

  // Store data for click handling
  marker.__bands = bands;
  marker.__count = count;
  marker.__isCluster = true; // Flag to identify as zoomable cluster

  // Click handler - always zoom for clusters
  marker.on("click", (e) => {
    L.DomEvent.stopPropagation(e);
    map.setView([lat, lon], map.getZoom() + 2, { animate: true });
  });

  return marker;
}

/**
 * Create a location marker (final level - shows popup with all bands)
 * These have a solid border and bright red color to indicate they show details
 */
function createLocationMarker(lat, lon, count, bands, locationName) {
  // Size based on count (smaller than clusters)
  const size = Math.min(18 + Math.log2(count + 1) * 6, 42);

  // Bright red color for final locations
  let color, borderColor;
  if (count === 1) {
    color = 'rgba(180, 30, 30, 0.9)';
    borderColor = 'rgba(255, 100, 100, 0.9)';
  } else if (count < 5) {
    color = 'rgba(198, 26, 26, 0.9)';
    borderColor = 'rgba(255, 120, 120, 0.9)';
  } else if (count < 20) {
    color = 'rgba(210, 50, 50, 0.9)';
    borderColor = 'rgba(255, 150, 150, 0.9)';
  } else {
    color = 'rgba(220, 80, 80, 0.9)';
    borderColor = 'rgba(255, 180, 180, 0.95)';
  }

  // Create custom div icon with count - solid border indicates "clickable for details"
  const icon = L.divIcon({
    className: 'cluster-marker cluster-location',
    html: `<div style="
      width: ${size}px;
      height: ${size}px;
      background: ${color};
      border: 2px solid ${borderColor};
      border-radius: 50%;
      display: flex;
      align-items: center;
      justify-content: center;
      font-size: ${Math.max(10, size / 3.2)}px;
      font-weight: bold;
      color: #fff;
      box-shadow: 0 2px 8px rgba(0,0,0,0.5);
      cursor: pointer;
      transition: transform 0.15s ease, box-shadow 0.15s ease;
    " onmouseover="this.style.transform='scale(1.15)';this.style.boxShadow='0 4px 16px rgba(198,26,26,0.6)'" onmouseout="this.style.transform='scale(1)';this.style.boxShadow='0 2px 8px rgba(0,0,0,0.5)'">${count}</div>`,
    iconSize: [size, size],
    iconAnchor: [size/2, size/2]
  });

  const marker = L.marker([lat, lon], { icon });

  // Store data
  marker.__bands = bands;
  marker.__count = count;
  marker.__isCluster = false; // Flag to identify as final location
  marker.__locationName = locationName;

  // Build popup HTML with ALL bands (scrollable)
  const bandListHtml = bands.map(b => {
    const maUrl = b.url || (b.ma_id ? `https://www.metal-archives.com/bands/x/${b.ma_id}` : null);
    const genre = b.genre || '—';
    const status = b.status || '—';
    const year = Number.isFinite(b.formed_year) ? b.formed_year : '—';

    if (maUrl) {
      return `<a href="${escapeHtml(maUrl)}" target="_blank" rel="noopener noreferrer" style="display:block;padding:6px 8px;margin:2px 0;background:rgba(255,255,255,0.03);border-radius:4px;text-decoration:none;transition:background 0.15s;" onmouseover="this.style.background='rgba(198,26,26,0.15)'" onmouseout="this.style.background='rgba(255,255,255,0.03)'">
        <div style="color:#e9eef5;font-weight:600;font-size:13px;">${escapeHtml(b.name || 'Unknown')}</div>
        <div style="color:#888;font-size:11px;margin-top:2px;">${escapeHtml(genre)} · ${escapeHtml(status)} · ${year}</div>
      </a>`;
    }
    return `<div style="display:block;padding:6px 8px;margin:2px 0;background:rgba(255,255,255,0.03);border-radius:4px;">
      <div style="color:#e9eef5;font-weight:600;font-size:13px;">${escapeHtml(b.name || 'Unknown')}</div>
      <div style="color:#888;font-size:11px;margin-top:2px;">${escapeHtml(genre)} · ${escapeHtml(status)} · ${year}</div>
    </div>`;
  }).join("");

  const popupHtml = `
    <div style="min-width:280px; max-width:350px;">
      <div style="font-weight:900; margin-bottom:8px; padding-bottom:8px; font-size:14px; color:#c61a1a; border-bottom:1px solid rgba(255,255,255,0.1);">
        📍 ${escapeHtml(locationName || t('location_fallback'))}
        <span style="font-weight:normal;color:#a2b0c2;font-size:12px;margin-left:8px;">${count} ${count === 1 ? t('group_s') : t('group_p')}</span>
      </div>
      <div style="max-height:300px; overflow-y:auto; margin:-4px;">
        ${bandListHtml}
      </div>
    </div>
  `;

  // Bind popup
  marker.bindPopup(popupHtml, {
    maxWidth: 380,
    maxHeight: 400,
    className: 'location-popup'
  });

  // Click handler - update sidebar list AND open popup
  marker.on("click", (e) => {
    L.DomEvent.stopPropagation(e);
    const gLabel = count === 1 ? t('group_s') : t('group_p');
    const hint = locationName ? `${count} ${gLabel} — ${locationName}` : `${count} ${gLabel}`;
    renderList(bands, hint);
    // Small delay to ensure popup opens correctly
    setTimeout(() => marker.openPopup(), 10);
  });

  return marker;
}

/**
 * Check if all bands share the same location_text
 */
function bandsShareLocation(bands) {
  if (!bands || bands.length === 0) return false;
  if (bands.length === 1) return true;

  const firstLoc = norm(bands[0]?.location_text || '');
  if (!firstLoc) return false;

  return bands.every(b => norm(b?.location_text || '') === firstLoc);
}

/**
 * Rebuild layers from cluster data (server-side aggregated)
 */
function rebuildLayersFromClusters(clusters) {
  markersById.clear();
  coordIndex.clear();
  clusterLayer.clearLayers();

  for (const cluster of clusters) {
    const { lat, lon, count, sample_bands, location_text } = cluster;

    if (!Number.isFinite(lat) || !Number.isFinite(lon)) continue;

    // Parse sample bands
    const bands = (sample_bands || []).map(b => {
      if (typeof b === 'string') {
        try { return JSON.parse(b); } catch { return null; }
      }
      return b;
    }).filter(Boolean);

    const bandCount = count || bands.length;

    // Check if all bands share the same location
    const allSameLocation = bandsShareLocation(bands);
    const locationName = allSameLocation ? (location_text || bands[0]?.location_text || null) : null;

    // Use location marker (popup) if all bands share same location
    // Use cluster marker (zoom) if bands come from different locations
    const useLocationMarker = allSameLocation;

    const marker = useLocationMarker
      ? createLocationMarker(lat, lon, bandCount, bands, locationName)
      : createClusterMarker(lat, lon, bandCount, bands);

    clusterLayer.addLayer(marker);

    // Store reference for each band in cluster
    for (const b of bands) {
      if (b.ma_id) markersById.set(b.ma_id, marker);
    }
  }

  if (currentMode === "clusters") {
    if (!map.hasLayer(clusterLayer)) map.addLayer(clusterLayer);
  }

  $("unique").textContent = String(clusters.length);

  // Render list with visible bands
  const visibleBands = clusters.flatMap(c => c.sample_bands || [])
    .map(b => typeof b === 'string' ? JSON.parse(b) : b)
    .filter(Boolean);
  renderList(visibleBands.slice(0, 200));
}

/**
 * Rebuild layers from individual band data
 */
function rebuildLayersFromBands(bands) {
  markersById.clear();
  coordIndex.clear();
  clusterLayer.clearLayers();

  // Index by coords
  for (const b of bands) {
    if (!Number.isFinite(b.lat) || !Number.isFinite(b.lon)) continue;
    const k = keyFromLatLon(b.lat, b.lon);
    if (!coordIndex.has(k)) coordIndex.set(k, []);
    coordIndex.get(k).push(b);
  }

  for (const [k, coordBands] of coordIndex.entries()) {
    const [la, lo] = k.split(",").map(Number);

    if (!Number.isFinite(la) || !Number.isFinite(lo)) continue;

    const locationName = coordBands[0]?.location_text || null;

    // Always use location marker for individual bands (final level)
    const marker = createLocationMarker(la, lo, coordBands.length, coordBands, locationName);

    for (const b of coordBands) markersById.set(b.ma_id, marker);

    clusterLayer.addLayer(marker);
  }

  if (currentMode === "clusters") {
    if (!map.hasLayer(clusterLayer)) map.addLayer(clusterLayer);
  }

  $("count").textContent = String(bands.length);
  $("unique").textContent = String(coordIndex.size);
  renderList(bands);
}

/**
 * Rebuild layers from locally filtered data (legacy mode)
 */
function rebuildLayers() {
  markersById.clear();
  coordIndex.clear();

  clusterLayer.clearLayers();

  if (currentMode === "clusters") {
    if (!map.hasLayer(clusterLayer)) map.addLayer(clusterLayer);
  } else if (map.hasLayer(clusterLayer)) {
    map.removeLayer(clusterLayer);
  }

  // index by coords
  for (const b of filtered) {
    const k = keyFromLatLon(b.lat, b.lon);
    if (!coordIndex.has(k)) coordIndex.set(k, []);
    coordIndex.get(k).push(b);
  }

  console.log(`Rebuilding layers: ${filtered.length} bands, ${coordIndex.size} unique coords`);

  let markerCount = 0;
  const heatPoints = [];
  let heatMax = 1;
  for (const [k, bands] of coordIndex.entries()) {
    const [la, lo] = k.split(",").map(Number);

    // Validate coordinates
    if (!Number.isFinite(la) || !Number.isFinite(lo)) {
      console.warn(`Invalid coords for key ${k}:`, { la, lo });
      continue;
    }

    const intensity = bands.length;
    heatPoints.push([la, lo, intensity]);
    if (intensity > heatMax) heatMax = intensity;

    const locationName = bands[0]?.location_text || null;

    // Use location marker (final level) for all in legacy mode
    const marker = createLocationMarker(la, lo, bands.length, bands, locationName);
    markerCount++;

    for (const b of bands) markersById.set(b.ma_id, marker);

    clusterLayer.addLayer(marker);
  }

  console.log(`Added ${markerCount} markers to clusterLayer`);

  // heatmap (use all points if loaded, otherwise use filtered)
  if (heatPointsLoaded) {
    buildHeatLayer(allHeatPoints);
  } else {
    buildHeatLayer(heatPoints, heatMax);
  }

  if (currentMode === "heat") {
    if (heatLayer && !map.hasLayer(heatLayer)) map.addLayer(heatLayer);
  } else if (heatLayer && map.hasLayer(heatLayer)) {
    map.removeLayer(heatLayer);
  }

  // counters
  $("count").textContent = String(filtered.length);
  $("unique").textContent = String(coordIndex.size);
}

// --- Map init ---
const map = L.map("map", {
  preferCanvas: true,
  tap: true, // Enable tap for mobile
  touchZoom: true,
  dragging: true,
  zoomControl: false // Disable default, we'll add our own on the right
}).setView([50.2, 10.2], 4);

// Add zoom control on the right side
L.control.zoom({
  position: 'topright'
}).addTo(map);

L.tileLayer(DARK_TILES, { maxZoom: 19, attribution: DARK_ATTRIB }).addTo(map);

// Force map to render properly on mobile
setTimeout(() => {
  if (map && map.invalidateSize) {
    map.invalidateSize(true);
  }
}, 100);

// Also invalidate on window resize
window.addEventListener('resize', () => {
  if (map && map.invalidateSize) {
    map.invalidateSize();
  }
});

// Invalidate when page becomes visible (mobile browsers)
document.addEventListener('visibilitychange', () => {
  if (!document.hidden && map && map.invalidateSize) {
    setTimeout(() => map.invalidateSize(true), 100);
  }
});

// Layer for custom cluster markers
clusterLayer = L.layerGroup();
map.addLayer(clusterLayer);

// Debounced viewport change handler
let viewportDebounceTimer = null;
function onViewportChange() {
  if (!USE_VIEWPORT_LOADING) return;

  clearTimeout(viewportDebounceTimer);
  viewportDebounceTimer = setTimeout(() => {
    loadViewportBands();
  }, VIEWPORT_DEBOUNCE_MS);
}

// Listen for map movements
map.on("moveend", onViewportChange);
map.on("zoomend", onViewportChange);

function buildHeatLayer(points, maxIntensity) {
  if (!points || !points.length) {
    if (heatLayer) map.removeLayer(heatLayer);
    return;
  }

  // Calcul des statistiques pour calibrer l'échelle
  const intensities = points.map(p => p[2]).sort((a, b) => b - a);
  const actualMax = intensities[0] || 1;
  const p95 = intensities[Math.floor(intensities.length * 0.05)] || 1; // 95e percentile
  const p90 = intensities[Math.floor(intensities.length * 0.10)] || 1; // 90e percentile
  const p75 = intensities[Math.floor(intensities.length * 0.25)] || 1; // 75e percentile
  const p50 = intensities[Math.floor(intensities.length * 0.5)] || 1;  // Médiane

  // Utilise le 75e percentile pour une heatmap moins saturée
  // Multiplié par 2 pour étaler davantage les couleurs
  const max = Math.max(15, (maxIntensity || p75) * 2);

  console.log(`Heatmap: ${points.length} points, actualMax=${actualMax}, p95=${p95}, p90=${p90}, p75=${p75}, p50=${p50}, using max=${max}`);

  if (heatLayer) map.removeLayer(heatLayer);
  heatLayer = L.heatLayer(points, {
    radius: 20,           // Réduit de 28 à 20 pour moins de chevauchement
    blur: 18,             // Réduit de 22 à 18
    maxZoom: 10,          // Augmenté de 8 à 10 pour garder l'effet plus longtemps
    minOpacity: 0.15,     // Réduit de 0.25 à 0.15 pour moins de saturation
    max: max,
    gradient: {
      0.0: '#000033',     // Bleu très foncé (presque invisible)
      0.15: '#000066',    // Bleu foncé
      0.3: '#0066cc',     // Bleu moyen
      0.45: '#00ccff',    // Cyan
      0.58: '#44ff44',    // Vert
      0.72: '#ffff00',    // Jaune
      0.86: '#ff8800',    // Orange
      1.0: '#ff0000'      // Rouge
    }
  });

  if (currentMode === "heat") {
    map.addLayer(heatLayer);
  }
}

function setMode(mode) {
  // mode: "clusters" | "heat"
  currentMode = mode;
  const cOn = mode === "clusters";
  const hOn = mode === "heat";

  if (cOn) {
    if (!map.hasLayer(clusterLayer)) map.addLayer(clusterLayer);
    if (heatLayer && map.hasLayer(heatLayer)) map.removeLayer(heatLayer);
  } else {
    if (map.hasLayer(clusterLayer)) map.removeLayer(clusterLayer);
    if (!heatPointsLoaded) {
      loadAllHeatPoints();
    } else {
      buildHeatLayer(allHeatPoints);
      if (heatLayer && !map.hasLayer(heatLayer)) map.addLayer(heatLayer);
    }
  }
}

// --- UI widgets (multiselect) ---
function buildMultiSelect(containerId, items, enabledMap, labelFn, options = {}) {
  const container = $(containerId);
  if (!container) return;

  const summaryEl = options.summaryEl || null;
  const emptyLabel = options.emptyLabel || "Aucun élément";

  container.innerHTML = `
    <div class="ms-head">
      <input class="ms-search" type="search" placeholder="Chercher…" />
      <div class="ms-actions">
        <button class="btn-mini" type="button" data-act="all">Tout</button>
        <button class="btn-mini" type="button" data-act="none">Rien</button>
      </div>
    </div>
    <div class="ms-list"></div>
  `;

  const searchEl = container.querySelector(".ms-search");
  const listEl = container.querySelector(".ms-list");

  function updateSummary() {
    if (!summaryEl) return;
    const total = items.length;
    if (!total) {
      summaryEl.textContent = "Aucun";
      return;
    }
    const selected = items.filter(it => enabledMap.get(it.key) !== false).length;
    if (selected === total) summaryEl.textContent = "Tous";
    else if (selected === 0) summaryEl.textContent = "Aucun";
    else summaryEl.textContent = `${selected}/${total}`;
  }

  function render() {
    const q = norm(searchEl.value).toLowerCase();
    listEl.innerHTML = "";

    if (!items.length) {
      listEl.innerHTML = `<div class="ms-empty">${escapeHtml(emptyLabel)}</div>`;
      updateSummary();
      return;
    }

    const filteredItems = items.filter(it => labelFn(it).toLowerCase().includes(q));
    if (!filteredItems.length) {
      listEl.innerHTML = `<div class="ms-empty">Aucun résultat</div>`;
      updateSummary();
      return;
    }

    for (const it of filteredItems) {
      const key = it.key;
      const on = enabledMap.get(key) === true;
      const div = document.createElement("div");
      div.className = `ms-item ${on ? "on" : ""}`;
      div.dataset.key = key;
      div.innerHTML = `
        <div>${escapeHtml(labelFn(it))}</div>
        <div class="count-badge">${it.count}</div>
      `;
      div.addEventListener("click", () => {
        enabledMap.set(key, !(enabledMap.get(key) === true));
        render();
        applyFilters();
      });
      listEl.appendChild(div);
    }

    updateSummary();
  }

  container.querySelector('[data-act="all"]').addEventListener("click", () => {
    for (const it of items) enabledMap.set(it.key, true);
    render();
    applyFilters();
  });
  container.querySelector('[data-act="none"]').addEventListener("click", () => {
    for (const it of items) enabledMap.set(it.key, false);
    render();
    applyFilters();
  });
  searchEl.addEventListener("input", render);

  render();
}

function buildStatusToggles(statusItems) {
  const grid = $("statusGrid");
  grid.innerHTML = "";
  for (const it of statusItems) {
    statusEnabled.set(it.key, true);
    const btn = document.createElement("div");
    btn.className = "toggle on";
    btn.dataset.status = it.key;
    btn.innerHTML = `<span class="box"></span><span>${escapeHtml(it.key)}</span><span class="count-badge">${it.count}</span>`;
    btn.addEventListener("click", () => {
      const cur = statusEnabled.get(it.key) === true;
      statusEnabled.set(it.key, !cur);
      btn.classList.toggle("on", !cur);
      applyFilters();
    });
    grid.appendChild(btn);
  }
}

function buildMacroGenreButtons(genreFacets = []) {
  const container = $("macroGenres");
  if (!container) return;
  container.innerHTML = "";
  macroActiveCount = 0;

  const counts = new Map();
  for (const m of MACRO_GENRES) counts.set(m.key, 0);

  // Calculate macro genre counts from genre facets
  for (const facet of genreFacets) {
    const g = (facet.key || "").toLowerCase();
    if (!g) continue;
    for (const m of MACRO_GENRES) {
      if (m.terms.some(t => g.includes(t))) {
        counts.set(m.key, (counts.get(m.key) || 0) + facet.count);
      }
    }
  }

  for (const m of MACRO_GENRES) {
    macroGenreEnabled.set(m.key, false);
    const btn = document.createElement("button");
    btn.type = "button";
    btn.className = "macro-btn";
    btn.dataset.key = m.key;
    btn.innerHTML = `<span>${escapeHtml(m.key)}</span><span class="count-badge">${counts.get(m.key) || 0}</span>`;
    btn.addEventListener("click", () => {
      const cur = macroGenreEnabled.get(m.key) === true;
      macroGenreEnabled.set(m.key, !cur);
      btn.classList.toggle("on", !cur);
      macroActiveCount += cur ? -1 : 1;
      applyFilters();
    });
    container.appendChild(btn);
  }
}

function setupDropdown(triggerId, panelId) {
  const trigger = $(triggerId);
  const panel = $(panelId);
  if (!trigger || !panel) return;
  trigger.addEventListener("click", () => {
    const nowCollapsed = panel.classList.toggle("is-collapsed");
    trigger.setAttribute("aria-expanded", String(!nowCollapsed));
  });
}

// --- List + details + highlight ---
function sortBands(arr) {
  const mode = $("sortSelect")?.value || "az";
  const a = [...arr];

  if (mode === "az") {
    a.sort((x,y) => (x.name || "").localeCompare(y.name || "", "fr", { sensitivity: "base" }));
  } else if (mode === "status") {
    a.sort((x,y) => (x.status || "").localeCompare(y.status || "", "fr", { sensitivity: "base" }) || (x.name || "").localeCompare(y.name || "", "fr", { sensitivity: "base" }));
  } else if (mode === "genre") {
    a.sort((x,y) => (x.genre || "").localeCompare(y.genre || "", "fr", { sensitivity: "base" }) || (x.name || "").localeCompare(y.name || "", "fr", { sensitivity: "base" }));
  } else if (mode === "country") {
    a.sort((x,y) => (x.country || "").localeCompare(y.country || "", "fr", { sensitivity: "base" }) || (x.name || "").localeCompare(y.name || "", "fr", { sensitivity: "base" }));
  } else if (mode === "year") {
    a.sort((x,y) => (Number.isFinite(y.formed_year) ? y.formed_year : -1) - (Number.isFinite(x.formed_year) ? x.formed_year : -1));
  }

  return a;
}

function setActive(ma_id) {
  // list highlight
  for (const [id, el] of listElById.entries()) el.classList.toggle("active", id === ma_id);

  // marker highlight
  const m = markersById.get(ma_id);
  if (m && m.setStyle) {
    m.setStyle({ radius: 8, fillOpacity: 0.85, weight: 2 });
    // reset others
    for (const [id, mm] of markersById.entries()) {
      if (id !== ma_id && mm.setStyle) mm.setStyle({ radius: 6, fillOpacity: 0.55, weight: 1 });
    }
  }
}

function renderList(items, hintOverride) {
  const list = $("list");
  list.innerHTML = "";
  listElById.clear();

  const sorted = sortBands(items);
  $("selectionHint").textContent = hintOverride || `${sorted.length} ${t("results_label")}`;

  const q = currentQuery();

  for (const b of sorted.slice(0, 4000)) {
    const div = document.createElement("div");
    div.className = "band";
    div.dataset.id = String(b.ma_id);

    const name = escapeHtml(b.name || "Unknown");
    const genre = escapeHtml(b.genre || "—");
    const loc = escapeHtml(b.location_text || "—");
    const st = escapeHtml(b.status || "—");
    const yr = Number.isFinite(b.formed_year) ? String(b.formed_year) : "—";

    div.innerHTML = `
      <div class="name"><span class="sigil"></span><div>${name}</div></div>
      <div class="meta">
        <div><b>${escapeHtml(b.country || "—")}</b> • <b>${st}</b> • ${yr}</div>
        <div>${genre}</div>
        <div>${loc}</div>
      </div>
    `;

    div.addEventListener("mouseenter", () => {
      setActive(b.ma_id);
      const m = markersById.get(b.ma_id);
      if (m) m.openPopup?.();
    });

    div.addEventListener("click", () => {
      setActive(b.ma_id);
      const m = markersById.get(b.ma_id);
      if (m && m.getLatLng) {
        map.setView(m.getLatLng(), Math.max(map.getZoom(), 8), { animate: true });
        m.openPopup?.();
      } else if (Number.isFinite(b.lat) && Number.isFinite(b.lon)) {
        map.setView([b.lat, b.lon], Math.max(map.getZoom(), 10), { animate: true });
      }
    });

    list.appendChild(div);
    listElById.set(b.ma_id, div);

    // highlight visuel dans liste
    if (q && bandMatchesQuery(b, q)) {
      div.style.boxShadow = "0 0 0 1px rgba(198,26,26,0.15) inset";
    }
  }
}

function applyFilters() {
  const q = currentQuery();

  console.log("Applying filters...", {
    query: q,
    useViewportLoading: USE_VIEWPORT_LOADING
  });

  if (USE_VIEWPORT_LOADING) {
    // With viewport loading, trigger a reload from server
    loadViewportBands();
  } else {
    // Legacy mode: filter locally loaded data
    filtered = geocodedBands
      .filter(b => bandMatchesQuery(b, q))
      .filter(b => bandMatchesCountry(b))
      .filter(b => bandMatchesStatus(b))
      .filter(b => bandMatchesGenre(b))
      .filter(b => bandMatchesTheme(b))
      .filter(b => bandMatchesYear(b));

    console.log(`Filtered: ${filtered.length} bands match filters`);

    rebuildLayers();
    renderList(filtered);
  }
}

// --- Modal for no coords ---
const modalBackdrop = $("modalBackdrop");
const modalBody = $("modalBody");
const modalClose = $("modalClose");
const modalTitle = $("modalTitle");

function openModal(title, bands) {
  modalTitle.textContent = title;
  modalBody.innerHTML = "";

  const q = currentQuery();
  const items = bands
    .filter(b => bandMatchesQuery(b, q))
    .filter(b => bandMatchesCountry(b))
    .filter(b => bandMatchesStatus(b))
    .filter(b => bandMatchesGenre(b))
    .filter(b => bandMatchesTheme(b))
    .filter(b => bandMatchesYear(b));

  if (!items.length) {
    modalBody.innerHTML = `<div style="padding:10px;color:rgba(162,176,194,0.85);">Aucun résultat</div>`;
  } else {
    const sorted = sortBands(items);
    for (const b of sorted.slice(0, 8000)) {
      const div = document.createElement("div");
      div.className = "band";
      const maUrl = b.url || (b.ma_id ? `https://www.metal-archives.com/bands/x/${b.ma_id}` : "#");
      div.innerHTML = `
        <div class="name">
          <span class="sigil"></span>
          <a href="${escapeHtml(maUrl)}" target="_blank" rel="noopener noreferrer">${escapeHtml(b.name)}</a>
        </div>
        <div class="meta">
          <div><b>${escapeHtml(b.country||"—")}</b> • <b>${escapeHtml(b.status||"—")}</b></div>
          <div><b>Genre:</b> ${escapeHtml(b.genre||"—")}</div>
          <div><b>Lieu:</b> ${escapeHtml(b.location_text||"—")}</div>
        </div>
      `;
      modalBody.appendChild(div);
    }
  }

  modalBackdrop.classList.add("on");
  modalBackdrop.setAttribute("aria-hidden", "false");
}

async function loadNoLocationBands() {
  try {
    modalBody.innerHTML = "<div style='padding:20px;color:rgba(162,176,194,0.85);'>Chargement...</div>";

    const j = await apiGet("/api/bands?limit=10000&geocoded=0");
    const items = j.items || [];

    modalBody.innerHTML = "";

    if (!items.length) {
      modalBody.innerHTML = `<div style="padding:10px;color:rgba(162,176,194,0.85);">Aucun groupe sans coordonnées</div>`;
      return;
    }

    for (const x of items.slice(0, 500)) {
      const div = document.createElement("div");
      div.className = "band";
      const maUrl = x.ma_id ? `https://www.metal-archives.com/bands/x/${x.ma_id}` : "#";
      div.innerHTML = `
        <div class="name">
          <span class="sigil"></span>
          <a href="${escapeHtml(maUrl)}" target="_blank" rel="noopener noreferrer">${escapeHtml(x.name || "Unknown")}</a>
        </div>
        <div class="meta">
          <div><b>${escapeHtml(x.country || "—")}</b> • <b>${escapeHtml(x.status || "—")}</b></div>
          <div><b>Genre:</b> ${escapeHtml(x.genre || "—")}</div>
          <div><b>Lieu:</b> ${escapeHtml(x.location_text || "—")}</div>
        </div>
      `;
      modalBody.appendChild(div);
    }

    if (items.length > 500) {
      const more = document.createElement("div");
      more.style.cssText = "padding:10px;color:rgba(162,176,194,0.85);text-align:center;";
      more.textContent = `... et ${items.length - 500} autres`;
      modalBody.appendChild(more);
    }
  } catch (e) {
    modalBody.innerHTML = `<div style="padding:10px;color:rgba(162,176,194,0.85);">Erreur: ${escapeHtml(e.message)}</div>`;
  }
}

function closeModal() {
  modalBackdrop.classList.remove("on");
  modalBackdrop.setAttribute("aria-hidden", "true");
}

modalClose?.addEventListener("click", closeModal);
modalBackdrop?.addEventListener("click", (e) => {
  if (e.target === modalBackdrop) closeModal();
});

// Info modal (legal/FAQ)
const infoBackdrop = $("infoBackdrop");
const infoBody = $("infoBody");
const infoClose = $("infoClose");
const infoTitle = $("infoTitle");

// LEGAL_HTML and FAQ_HTML are now generated per-language in locales.js via t('legal_html') / t('faq_html')

function openInfoModal(title, html) {
  if (!infoBackdrop || !infoBody || !infoTitle) return;
  infoTitle.textContent = title;
  infoBody.innerHTML = html;
  infoBackdrop.classList.add("on");
  infoBackdrop.setAttribute("aria-hidden", "false");
}

function closeInfoModal() {
  if (!infoBackdrop) return;
  infoBackdrop.classList.remove("on");
  infoBackdrop.setAttribute("aria-hidden", "true");
}

if (infoClose) infoClose.addEventListener("click", closeInfoModal);
if (infoBackdrop) {
  infoBackdrop.addEventListener("click", (e) => {
    if (e.target === infoBackdrop) closeInfoModal();
  });
}

document.addEventListener("keydown", (e) => {
  if (e.key === "Escape") {
    closeModal();
    closeInfoModal();
  }
});

// Sidebar toggle
function setSidebarCollapsed(collapsed) {
  if (!appEl) return;
  appEl.classList.toggle("sidebar-collapsed", collapsed);
  const toggle = $("sidebarToggle");
  if (toggle) {
    toggle.setAttribute("aria-expanded", String(!collapsed));
    // Update button text
    const label = toggle.querySelector(".label");
    if (label) {
      label.textContent = collapsed ? t("options_btn") : t("close_btn");
    }
  }
  // Refresh map size after sidebar animation
  setTimeout(() => map?.invalidateSize?.(), 220);
}

const sidebarToggle = $("sidebarToggle");
if (sidebarToggle) {
  sidebarToggle.addEventListener("click", (e) => {
    e.preventDefault();
    e.stopPropagation();
    const collapsed = appEl.classList.contains("sidebar-collapsed");
    setSidebarCollapsed(!collapsed);
  });
}

// Note: Sidebar closing is now controlled only via the toggle button
// We removed the overlay auto-close behavior since users want to interact
// with the map while the sidebar is open

// Collapse sidebar by default on smaller screens
if (window.matchMedia("(max-width: 1200px)").matches) {
  setTimeout(() => {
    setSidebarCollapsed(true);
  }, 100);
}

// Language dropdown (flag-icons custom widget)
const LANG_FLAGS = { en:"gb", fr:"fr", de:"de", es:"es", it:"it", pl:"pl", nl:"nl", ro:"ro", pt:"pt", cs:"cz", sv:"se", hu:"hu", da:"dk", fi:"fi", sk:"sk", hr:"hr", sl:"si", lt:"lt", lv:"lv", et:"ee", nb:"no" };
const LANG_NAMES = { en:"English", fr:"Français", de:"Deutsch", es:"Español", it:"Italiano", pl:"Polski", nl:"Nederlands", ro:"Română", pt:"Português", cs:"Čeština", sv:"Svenska", hu:"Magyar", da:"Dansk", fi:"Suomi", sk:"Slovenčina", hr:"Hrvatski", sl:"Slovenščina", lt:"Lietuvių", lv:"Latviešu", et:"Eesti", nb:"Norsk bokmål" };

function syncLangTrigger(lang) {
  const trigger = document.getElementById("langTrigger");
  if (!trigger) return;
  const flagEl = trigger.querySelector(".lang-flag");
  if (flagEl) flagEl.className = `fi fi-${LANG_FLAGS[lang] || "un"} lang-flag`;
  const codeEl = trigger.querySelector(".lang-code");
  if (codeEl) codeEl.textContent = lang.toUpperCase();
  document.querySelectorAll(".lang-opt").forEach(b =>
    b.classList.toggle("current", b.dataset.lang === lang)
  );
}

const langTrigger = document.getElementById("langTrigger");
const langOptions = document.getElementById("langOptions");
if (langTrigger && langOptions) {
  langTrigger.addEventListener("click", (e) => {
    e.stopPropagation();
    const open = langOptions.classList.toggle("hidden") === false;
    langTrigger.setAttribute("aria-expanded", String(open));
  });
  document.addEventListener("click", () => {
    langOptions.classList.add("hidden");
    langTrigger.setAttribute("aria-expanded", "false");
  });
  langOptions.querySelectorAll(".lang-opt").forEach(btn => {
    btn.addEventListener("click", (e) => {
      e.stopPropagation();
      currentLang = btn.dataset.lang;
      localStorage.setItem("lang", currentLang);
      langOptions.classList.add("hidden");
      langTrigger.setAttribute("aria-expanded", "false");
      syncLangTrigger(currentLang);
      applyI18n();
    });
  });
  syncLangTrigger(currentLang);
}
applyI18n();

// Legal/FAQ links
const openLegal = $("openLegal");
if (openLegal) openLegal.addEventListener("click", () => openInfoModal(t("legal_title"), t("legal_html")));

const openFaq = $("openFaq");
if (openFaq) openFaq.addEventListener("click", () => openInfoModal(t("faq_title"), t("faq_html")));

// Setup dropdowns
setupDropdown("countryToggle", "countrySelect");
setupDropdown("genreToggle", "genreSelect");
setupDropdown("themeToggle", "themeSelect");

// --- Buttons / controls ---
$("btnNoLocation")?.addEventListener("click", () => {
  modalTitle.textContent = t("without_coords_modal");
  modalBackdrop.classList.add("on");
  modalBackdrop.setAttribute("aria-hidden", "false");
  loadNoLocationBands();
});

// Search with suggestions
const searchInput = $("q");
const suggestionsEl = $("searchSuggestions");

function showSearchSuggestions(query) {
  if (!query || query.length < 2 || !allBands.length) {
    suggestionsEl?.classList.remove("visible");
    return;
  }

  const matches = allBands
    .filter(b => bandMatchesQuery(b, query))
    .slice(0, 8); // Show max 8 suggestions

  if (!matches.length) {
    suggestionsEl?.classList.remove("visible");
    return;
  }

  if (suggestionsEl) {
    suggestionsEl.innerHTML = matches.map(b => {
      const statusText = b.status || "Unknown";
      const yearText = Number.isFinite(b.formed_year) ? b.formed_year : "—";
      return `
        <div class="suggestion-item" data-id="${b.ma_id}">
          <div class="suggestion-name">${escapeHtml(b.name)}</div>
          <div class="suggestion-meta">
            ${escapeHtml(b.country || "—")} • ${escapeHtml(statusText)} • ${yearText} • ${escapeHtml(b.genre || "—")}
          </div>
        </div>
      `;
    }).join("");

    // Add click handlers
    suggestionsEl.querySelectorAll(".suggestion-item").forEach(el => {
      el.addEventListener("click", () => {
        const bandId = Number(el.dataset.id);
        const band = allBands.find(b => b.ma_id === bandId);
        if (band && Number.isFinite(band.lat) && Number.isFinite(band.lon)) {
          const marker = markersById.get(bandId);
          if (marker && marker.getLatLng) {
            map.setView(marker.getLatLng(), 10, { animate: true });
            marker.openPopup();
            setActive(bandId);
          } else {
            map.setView([band.lat, band.lon], 12, { animate: true });
          }
        }
        suggestionsEl.classList.remove("visible");
        if (searchInput) searchInput.value = band?.name || "";
      });
    });

    suggestionsEl.classList.add("visible");
  }
}

const debouncedSearch = debounce(() => {
  applyFilters();
}, 400); // 400ms delay

const debouncedSuggestions = debounce((query) => {
  showSearchSuggestions(query);
}, 200); // 200ms delay for suggestions

searchInput?.addEventListener("input", (e) => {
  const query = e.target.value.toLowerCase().trim();
  debouncedSuggestions(query);
  debouncedSearch();
});

searchInput?.addEventListener("focus", () => {
  const query = searchInput.value.toLowerCase().trim();
  if (query.length >= 2) {
    showSearchSuggestions(query);
  }
});

// Close suggestions when clicking outside
document.addEventListener("click", (e) => {
  if (!searchInput?.contains(e.target) && !suggestionsEl?.contains(e.target)) {
    suggestionsEl?.classList.remove("visible");
  }
});

$("sortSelect")?.addEventListener("change", () => {
  // Re-render list with current data
  const visibleBands = currentViewportData.flatMap(c => c.sample_bands || [c])
    .map(b => typeof b === 'string' ? JSON.parse(b) : b)
    .filter(Boolean);
  renderList(visibleBands.slice(0, 200));
});

const heatToggle = $("toggleHeat");
if (heatToggle) {
  heatToggle.addEventListener("change", () => setMode(heatToggle.checked ? "heat" : "clusters"));
}

$("btnReset")?.addEventListener("click", () => {
  if (filtered.length) {
    const pts = filtered.map(b => L.latLng(b.lat, b.lon));
    const bounds = L.latLngBounds(pts);
    if (bounds.isValid()) map.fitBounds(bounds.pad(0.12));
  } else if (geocodedBands.length) {
    const pts = geocodedBands.map(b => L.latLng(b.lat, b.lon));
    const bounds = L.latLngBounds(pts);
    if (bounds.isValid()) map.fitBounds(bounds.pad(0.12));
  } else {
    map.setView([50.2, 10.2], 4, { animate: true });
  }
});

map.on("click", () => {
  const visibleBands = currentViewportData.flatMap(c => c.sample_bands || [c])
    .map(b => typeof b === 'string' ? JSON.parse(b) : b)
    .filter(Boolean);
  renderList(visibleBands.slice(0, 200));
});

// --- Boot ---
function computeFacetCounts(arr, getKey) {
  const m = new Map();
  for (const b of arr) {
    const val = getKey(b);
    const k = val ? norm(val) : "Unknown";
    m.set(k, (m.get(k) || 0) + 1);
  }
  return [...m.entries()]
    .map(([key, count]) => ({ key, count }))
    .sort((a,b) => b.count - a.count);
}

function computeTokenFacetCounts(arr, getTokens) {
  const m = new Map();
  for (const b of arr) {
    const tokens = getTokens(b) || [];
    if (!tokens.length) {
      // Count bands without tokens as "Unknown"
      m.set("Unknown", (m.get("Unknown") || 0) + 1);
    } else {
      for (const t of tokens) {
        const k = norm(t) || "Unknown";
        m.set(k, (m.get(k) || 0) + 1);
      }
    }
  }
  return [...m.entries()]
    .map(([key, count]) => ({ key, count }))
    .sort((a,b) => b.count - a.count);
}

/**
 * Build timeline slider from year range (from facets or computed)
 */
function buildTimelineFromRange() {
  const slider = $("yearSlider");
  slider.innerHTML = "";

  if (!yearRange.min || !yearRange.max) {
    $("yearMin").textContent = "—";
    $("yearMax").textContent = "—";
    $("yearHint").textContent = t("year_data_missing");
    slider.innerHTML = `<div class="timeline-empty">${t("year_data_unavailable")}</div>`;
    return;
  }

  const { min, max } = yearRange;

  console.log(`Timeline: ${min} - ${max}`);

  $("yearMin").textContent = String(min);
  $("yearMax").textContent = String(max);
  $("yearHint").textContent = t("all_years_label");

  noUiSlider.create(slider, {
    start: [min, max],
    connect: true,
    step: 1,
    range: { min, max },
    behaviour: "tap-drag",
    tooltips: false,
  });

  slider.noUiSlider.on("update", (vals) => {
    const a = Math.round(Number(vals[0]));
    const b = Math.round(Number(vals[1]));
    yearFilter = { min: a, max: b };
    $("yearHint").textContent = (a === min && b === max) ? t("all_years_label") : `${a} → ${b}`;
  });

  slider.noUiSlider.on("change", () => {
    console.log(`Year filter changed: ${yearFilter.min} - ${yearFilter.max}`);
    applyFilters();
  });
}

async function boot() {
  try {
    console.log("Boot: Loading data...");
    console.log("Viewport loading mode:", USE_VIEWPORT_LOADING ? "ENABLED" : "DISABLED");

    // Wait for fonts to load (can affect layout)
    if (document.fonts && document.fonts.ready) {
      await document.fonts.ready;
    }

    // Load facets and stats first (fast)
    await Promise.all([
      loadFacets(),
      loadStats()
    ]);
    loadGoatCounterFooterStats(); // pas besoin d'await
    // Build filters from facets if available, otherwise load bands first
    let statuses, genres, countries, themes;

    if (facetData) {
      console.log("Using server-side facets for filters");
      statuses = facetData.statuses.map(s => ({ key: s.value, count: s.count }));
      countries = facetData.countries.map(c => ({ key: c.value, count: c.count }));
      genres = facetData.genres.map(g => ({ key: g.value, count: g.count }));
      themes = []; // Themes need to be computed from bands

      // Set year range from facets
      if (facetData.year_range) {
        yearRange = {
          min: facetData.year_range.min_year,
          max: facetData.year_range.max_year
        };
        yearFilter = { ...yearRange };
      }
    } else {
      // Fallback: load bands and compute facets locally
      console.log("Loading bands for local facet computation...");
      await loadBands();
      statuses = computeFacetCounts(allBands, b => b.status);
      genres = computeFacetCounts(allBands, b => b.genre);
      countries = computeFacetCounts(allBands, b => b.country);
      themes = computeTokenFacetCounts(allBands, b => b.themes);
      
    }

    console.log(`Boot: Facets ready - ${statuses.length} statuses, ${countries.length} countries, ${genres.length} genres`);

    // Init enabled maps = true
    for (const it of genres) genreEnabled.set(it.key, true);
    for (const it of countries) countryEnabled.set(it.key, true);
    for (const it of themes) themeEnabled.set(it.key, true);

    buildStatusToggles(statuses);
    buildMacroGenreButtons(genres);
    buildMultiSelect("genreSelect", genres, genreEnabled, (it) => it.key, { summaryEl: $("genreSummary") });
    buildMultiSelect("countrySelect", countries, countryEnabled, (it) => it.key, { summaryEl: $("countrySummary") });

    if (themes.length) {
      $("themeBox").style.display = "";
      buildMultiSelect("themeSelect", themes, themeEnabled, (it) => it.key, { summaryEl: $("themeSummary"), emptyLabel: "Aucun thème" });
    } else {
      $("themeBox").style.display = "none";
    }

    // Build timeline from facet data or computed range
    buildTimelineFromRange();

    // Set mode BEFORE applying filters
    setMode(heatToggle && heatToggle.checked ? "heat" : "clusters");

    // Force map size calculation
    console.log("Forcing map size recalculation...");
    if (map && map.invalidateSize) {
      map.invalidateSize(true);
    }

    // Initial data load
    console.log("Boot: Loading initial viewport data...");

    if (USE_VIEWPORT_LOADING) {
      // Load viewport data
      await loadViewportBands();
    } else {
      // Legacy: use all loaded bands
      if (!allBands.length) await loadBands();
      applyFilters();
    }

    console.log("Boot: Initial load complete");

    // Fit to Europe by default
    setTimeout(() => {
      if (map && map.invalidateSize) {
        map.invalidateSize(true);
      }
      // Default view: Europe
      map.setView([50.2, 10.2], 4);
    }, 100);

    // Load all bands in background for heatmap and search
    setTimeout(() => loadAllHeatPoints(), 1000);

  } catch (err) {
    console.error("Boot error:", err);
    $("list").innerHTML = `<div style="padding:20px;color:rgba(162,176,194,0.85);">Erreur de chargement: ${escapeHtml(err.message || String(err))}</div>`;
  }
}

// Make sure DOM is ready before booting
if (document.readyState === 'loading') {
  document.addEventListener('DOMContentLoaded', boot);
} else {
  boot();
}
