/* global L */
// ─── The basemap, in one place ───────────────────────────────────────────────
//
// There were two copies of a CARTO tile URL — one in admin-course.jsx, one in
// admin-network.jsx — and both broke on the same day for the same reason.
//
// CARTO retired its keyless basemaps. The tiles still return 200 OK; they just
// come back with "API KEY REQUIRED / carto.com/basemaps/apikey" stamped across
// the image. That is why the maps looked wrong rather than empty, and why
// nothing errored anywhere: as far as Leaflet is concerned those tiles loaded
// fine. No amount of retrying or cache-busting was ever going to fix it.
//
// So the default is a provider that needs no key at all. CARTO is still first
// choice IF a key exists — set window.SBX_CARTO_KEY before the app scripts and
// the exact previous look comes back, unchanged.

const CARTO_KEY = String(window.SBX_CARTO_KEY || '').trim();

// Tried in order. Each entry is one raster basemap; `labels` is an optional
// second layer of place names drawn over it (Esri splits the two).
const BASEMAP_PROVIDERS = [
  CARTO_KEY ? {
    id: 'carto-dark',
    url: `https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png?api_key=${encodeURIComponent(CARTO_KEY)}`,
    attribution: '&copy; OpenStreetMap &copy; CARTO',
    maxNativeZoom: 20,
  } : null,
  {
    // Esri's Dark Gray Canvas: keyless, and the closest match to what CARTO
    // dark_all looked like. Its deepest tile is z16, which is not enough for
    // dropping a pin on a particular green — hence maxNativeZoom below, which
    // lets Leaflet upscale past 16 instead of going blank.
    id: 'esri-dark',
    url: 'https://server.arcgisonline.com/ArcGIS/rest/services/Canvas/World_Dark_Gray_Base/MapServer/tile/{z}/{y}/{x}',
    labels: 'https://server.arcgisonline.com/ArcGIS/rest/services/Canvas/World_Dark_Gray_Reference/MapServer/tile/{z}/{y}/{x}',
    attribution: 'Tiles &copy; Esri &mdash; Esri, DeLorme, NAVTEQ',
    maxNativeZoom: 16,
  },
  {
    // Last resort. Light tiles under a dark UI, so they get knocked back with
    // a CSS filter — a legible map that is the wrong colour beats a legible
    // watermark saying the map is broken.
    id: 'osm',
    url: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png',
    attribution: '&copy; OpenStreetMap contributors',
    maxNativeZoom: 19,
    darken: true,
  },
].filter(Boolean);

// How many tiles have to fail before a provider is written off. One failure is
// a flaky tile; a provider that is genuinely unreachable fails every tile in
// the viewport at once, and there are more than three of those.
const TILE_FAILURES_BEFORE_FALLBACK = 4;

// addBasemap(map) — attach the basemap, and step to the next provider if the
// current one cannot be reached at all.
//
// Returns { destroy() }, which callers should invoke when they tear the map
// down; otherwise a pending fallback fires against a removed map.
function addBasemap(map, opts) {
  const maxZoom = (opts && opts.maxZoom) || 19;
  let layers = [];
  let dead = false;
  let index = 0;

  function clear() {
    layers.forEach(l => { try { map.removeLayer(l); } catch (e) { /* already gone */ } });
    layers = [];
  }

  function mount(i) {
    clear();
    const p = BASEMAP_PROVIDERS[i];
    if (!p) return;                       // nothing left to try; leave it blank
    let failures = 0;
    let anyLoaded = false;

    const common = { attribution: p.attribution, maxZoom, maxNativeZoom: p.maxNativeZoom };
    const base = L.tileLayer(p.url, common);
    if (p.darken) base.on('add', () => {
      const c = base.getContainer();
      if (c) c.style.filter = 'invert(1) hue-rotate(180deg) brightness(0.72) saturate(0.5)';
    });
    base.on('tileload', () => { anyLoaded = true; });
    base.on('tileerror', () => {
      // Only fall through if the provider has produced nothing at all. A
      // handful of missing tiles at the edge of coverage is normal and is not
      // a reason to throw away a working map.
      if (dead || anyLoaded) return;
      if (++failures < TILE_FAILURES_BEFORE_FALLBACK) return;
      if (i + 1 >= BASEMAP_PROVIDERS.length) return;
      index = i + 1;
      mount(index);
    });
    base.addTo(map);
    layers.push(base);

    if (p.labels) {
      const ref = L.tileLayer(p.labels, { ...common, attribution: '' });
      ref.addTo(map);
      layers.push(ref);
    }
  }

  mount(index);
  return {
    get provider() { return BASEMAP_PROVIDERS[index] ? BASEMAP_PROVIDERS[index].id : 'none'; },
    destroy() { dead = true; clear(); },
  };
}

Object.assign(window, { addBasemap, BASEMAP_PROVIDERS });
