(function () {
  const cfg = window.WPSH_STATS_CONFIG || null;
  if (!cfg) return;
  const i18n = cfg.messages || {};

  const mapContainer = document.getElementById("wpsh-world-map");
  const rowsContainer = document.getElementById("wpsh-blocked-ip-rows");
  const paginationContainer = document.getElementById("wpsh-blocked-ip-pagination");
  const worldMapEnabled = cfg.worldMapEnabled !== false;
  const worldMapDataUrl = typeof cfg.worldMapDataUrl === "string" ? cfg.worldMapDataUrl : "";
  const worldMapCentroidsUrl = typeof cfg.worldMapCentroidsUrl === "string" ? cfg.worldMapCentroidsUrl : "";
  if (!rowsContainer) return;

  const PAGE_SIZE = 10;
  let currentPage = 1;
  let sortedRows = [];
  let worldTopologyPromise = null;
  let countryCentroidsPromise = null;

  if (!cfg.tracingEnabled) {
    if (mapContainer) {
      mapContainer.textContent = t("ip_tracking_disabled", "IP-Tracking ist deaktiviert.");
    }
    renderTableRows([]);
    return;
  }

  const blockedIps = Array.isArray(cfg.blockedIps) ? cfg.blockedIps : [];
  if (!blockedIps.length) {
    if (mapContainer) {
      mapContainer.textContent = t("no_blocked", "Keine verdaechtigen Besucher im gewaehlten Zeitraum.");
    }
    renderTableRows([]);
    return;
  }

  const allRows = blockedIps.map((item) => {
    let country = item.country || (isPrivateIp(item.ip) ? t("local_private", "Lokal/Privat") : t("unknown", "Unbekannt"));
    if (country === "Unbekannt") country = t("unknown", "Unbekannt");
    if (country === "Lokal/Privat") country = t("local_private", "Lokal/Privat");
    const lat = Number.isFinite(Number(item.lat)) ? Number(item.lat) : null;
    const lng = Number.isFinite(Number(item.lng)) ? Number(item.lng) : null;
    return {
      ip: String(item.ip || ""),
      ipDisplay: String(item.ip_display || item.ip || ""),
      blocked: Number(item.blocked || 0),
      strictHits: Number(item.strict_hits || 0),
      total: Number(item.total || 0),
      country,
      lat,
      lng,
    };
  });

  renderTableRows(allRows);

  if (worldMapEnabled && mapContainer) {
    drawMap(allRows);
  }

  function drawMap(rows) {
    if (!mapContainer) return;
    mapContainer.textContent = t("loading_geo", "Lade Karte...");
    renderVectorMap(rows).catch(() => {
      drawFallbackDotsMap(rows);
    });
  }

  async function renderVectorMap(rows) {
    if (!mapContainer) return;
    const [topology, countryCentroids] = await Promise.all([loadWorldTopology(), loadCountryCentroids()]);
    const w = Math.max(680, mapContainer.clientWidth || 1000);
    const h = clamp(Math.round(w * 0.52), 360, 620);
    const mapBox = {
      x: 24,
      y: 20,
      width: w - 48,
      height: h - 40,
    };
    const gridLines = buildGridLines(mapBox);
    const landPaths = buildLandPaths(topology, mapBox);
    const markerRows = buildCountryMarkers(rows, mapBox, countryCentroids);
    const maxIntensity = markerRows.reduce((max, row) => Math.max(max, Number(row.intensity || 0)), 1);

    const dots = markerRows
      .map((row) => {
        const style = markerStyle(Number(row.intensity || 0), maxIntensity);
        const label = row.label || row.country || t("unknown", "Unbekannt");
        return `
  <g>
    <circle cx="${row.x.toFixed(1)}" cy="${row.y.toFixed(1)}" r="${style.outerRadius.toFixed(1)}" fill="${style.glow}" fill-opacity="0.24" />
    <circle cx="${row.x.toFixed(1)}" cy="${row.y.toFixed(1)}" r="${style.radius.toFixed(1)}" fill="${style.fill}" fill-opacity="0.88" stroke="#ffffff" stroke-width="1.8">
      <title>${escapeHtml(label)} - Angriffe: ${escapeHtml(String(row.intensity || 0))} | Geblockt: ${escapeHtml(String(row.blocked || 0))} | IPs: ${escapeHtml(String(row.ipCount || 1))}</title>
    </circle>
  </g>`;
      })
      .join("");

    const svg = `
<svg viewBox="0 0 ${w} ${h}" width="100%" height="${h}" role="img" aria-label="Weltkarte">
  <defs>
    <linearGradient id="wpsh-map-bg" x1="0" y1="0" x2="0" y2="1">
      <stop offset="0%" stop-color="#fbfdff" />
      <stop offset="100%" stop-color="#eef4fb" />
    </linearGradient>
    <linearGradient id="wpsh-land-fill" x1="0" y1="0" x2="1" y2="1">
      <stop offset="0%" stop-color="#a7dfff" />
      <stop offset="55%" stop-color="#82d1fb" />
      <stop offset="100%" stop-color="#66c5f5" />
    </linearGradient>
    <filter id="shadow" x="-10%" y="-10%" width="120%" height="120%">
      <feDropShadow dx="0" dy="12" stdDeviation="16" flood-color="#b8cce3" flood-opacity="0.16" />
    </filter>
    <clipPath id="wpsh-map-clip">
      <rect x="0" y="0" width="${w}" height="${h}" rx="18" />
    </clipPath>
  </defs>
  <rect x="0" y="0" width="${w}" height="${h}" rx="18" fill="url(#wpsh-map-bg)" />
  <g clip-path="url(#wpsh-map-clip)">
    <rect x="0" y="0" width="${w}" height="${h}" fill="url(#wpsh-map-bg)" />
    <g stroke="#dbe7f3" stroke-opacity="0.7" stroke-width="1">${gridLines}</g>
    <g filter="url(#shadow)">${landPaths}</g>
  </g>
  ${dots}
</svg>`;

    mapContainer.innerHTML = svg;
  }

  function drawFallbackDotsMap(rows) {
    if (!mapContainer) return;
    const w = Math.max(680, mapContainer.clientWidth || 1000);
    const h = clamp(Math.round(w * 0.52), 360, 620);
    const mapBox = {
      x: 24,
      y: 20,
      width: w - 48,
      height: h - 40,
    };
    const gridLines = buildGridLines(mapBox);
    const markerRows = buildCountryMarkers(rows, mapBox, null);
    const maxIntensity = markerRows.reduce((max, row) => Math.max(max, Number(row.intensity || 0)), 1);
    const dots = markerRows
      .map((row) => {
        const style = markerStyle(Number(row.intensity || 0), maxIntensity);
        return `<circle cx="${row.x.toFixed(1)}" cy="${row.y.toFixed(1)}" r="${style.radius.toFixed(1)}" fill="${style.fill}" stroke="#ffffff" stroke-width="1.8" />`;
      })
      .join("");
    mapContainer.innerHTML = `
<svg viewBox="0 0 ${w} ${h}" width="100%" height="${h}" role="img" aria-label="Weltkarte">
  <rect x="0" y="0" width="${w}" height="${h}" rx="18" fill="#edf3fb" />
  <g stroke="#d8e5f2" stroke-width="1">${gridLines}</g>
  ${dots}
</svg>`;
  }

  function latLngToXY(lat, lng, box) {
    const clampedLng = clamp(Number(lng), -180, 180);
    const clampedLat = clamp(Number(lat), -85, 85);
    const x = box.x + ((clampedLng + 180) / 360) * box.width;
    const y = box.y + ((90 - clampedLat) / 180) * box.height;
    return { x, y };
  }

  async function loadWorldTopology() {
    if (worldTopologyPromise) {
      return worldTopologyPromise;
    }
    if (worldMapDataUrl === "") {
      throw new Error("Missing world map data URL.");
    }

    worldTopologyPromise = fetch(worldMapDataUrl, { credentials: "same-origin" }).then((response) => {
      if (!response.ok) {
        throw new Error("Unable to load world map data.");
      }
      return response.json();
    });

    return worldTopologyPromise;
  }

  async function loadCountryCentroids() {
    if (countryCentroidsPromise) {
      return countryCentroidsPromise;
    }
    if (worldMapCentroidsUrl === "") {
      return Promise.resolve(new Map());
    }

    countryCentroidsPromise = fetch(worldMapCentroidsUrl, { credentials: "same-origin" })
      .then((response) => {
        if (!response.ok) {
          throw new Error("Unable to load world map centroid data.");
        }
        return response.text();
      })
      .then((csv) => parseCountryCentroids(csv))
      .catch(() => new Map());

    return countryCentroidsPromise;
  }

  function buildLandPaths(topology, mapBox) {
    const polygons = topologyToPolygons(topology);
    return polygons
      .map((polygon) => {
        const rings = polygon
          .map((ring) => {
            return buildRingPath(ring, mapBox);
          })
          .filter((ringPath) => ringPath !== "")
          .join(" ");
        if (rings === "") {
          return "";
        }
        return `<path d="${rings}" fill="url(#wpsh-land-fill)" fill-opacity="0.98" stroke="#ffffff" stroke-opacity="0.55" stroke-width="0.75" fill-rule="evenodd" />`;
      })
      .join("");
  }

  function topologyToPolygons(topology) {
    const objects = topology && topology.objects ? topology.objects : {};
    const countries = objects.countries || null;
    if (!countries) {
      return [];
    }

    const decodedArcs = (topology.arcs || []).map((arc) => decodeArc(arc, topology.transform));
    const geometries = countries.type === "GeometryCollection" ? countries.geometries || [] : [countries];
    const polygons = [];

    geometries.forEach((geometry) => {
      if (geometry.type === "Polygon") {
        polygons.push(buildPolygon(geometry.arcs || [], decodedArcs));
      } else if (geometry.type === "MultiPolygon") {
        (geometry.arcs || []).forEach((polygonArcs) => {
          polygons.push(buildPolygon(polygonArcs, decodedArcs));
        });
      }
    });

    return polygons;
  }

  function buildRingPath(ring, mapBox) {
    if (!Array.isArray(ring) || ring.length < 2) {
      return "";
    }

    let path = "";
    let previousLon = null;
    ring.forEach((point, index) => {
      const lon = Number(point[0]);
      const lat = Number(point[1]);
      if (!Number.isFinite(lon) || !Number.isFinite(lat)) {
        return;
      }

      if (previousLon !== null && Math.abs(lon - previousLon) > 180) {
        const xy = latLngToXY(lat, lon, mapBox);
        path += ` M${xy.x.toFixed(1)} ${xy.y.toFixed(1)}`;
        previousLon = lon;
        return;
      }

      const xy = latLngToXY(lat, lon, mapBox);
      path += `${index === 0 || previousLon === null ? " M" : " L"}${xy.x.toFixed(1)} ${xy.y.toFixed(1)}`;
      previousLon = lon;
    });

    return path.trim() === "" ? "" : `${path} Z`;
  }

  function buildPolygon(ringsSpec, decodedArcs) {
    return ringsSpec.map((ringSpec) => stitchArcs(ringSpec, decodedArcs));
  }

  function stitchArcs(indices, decodedArcs) {
    const points = [];
    indices.forEach((arcIndex, arcOffset) => {
      const arc = resolveArc(decodedArcs, arcIndex);
      arc.forEach((point, pointIndex) => {
        if (arcOffset > 0 && pointIndex === 0) {
          return;
        }
        points.push(point);
      });
    });
    return points;
  }

  function resolveArc(decodedArcs, arcIndex) {
    if (arcIndex >= 0) {
      return decodedArcs[arcIndex] || [];
    }

    const reversed = decodedArcs[~arcIndex] || [];
    return [...reversed].reverse();
  }

  function decodeArc(arc, transform) {
    let x = 0;
    let y = 0;
    const scale = transform && Array.isArray(transform.scale) ? transform.scale : [1, 1];
    const translate = transform && Array.isArray(transform.translate) ? transform.translate : [0, 0];
    return (arc || []).map((delta) => {
      x += Number(delta[0] || 0);
      y += Number(delta[1] || 0);
      return [x * scale[0] + translate[0], y * scale[1] + translate[1]];
    });
  }

  function buildGridLines(box) {
    const latitudes = [-60, -30, 0, 30, 60];
    const longitudes = [-120, -60, 0, 60, 120];
    const horizontal = latitudes
      .map((lat) => {
        const y = latLngToXY(lat, 0, box).y.toFixed(1);
        return `<line x1="${box.x.toFixed(1)}" y1="${y}" x2="${(box.x + box.width).toFixed(1)}" y2="${y}" />`;
      })
      .join("");
    const vertical = longitudes
      .map((lng) => {
        const x = latLngToXY(0, lng, box).x.toFixed(1);
        return `<line x1="${x}" y1="${box.y.toFixed(1)}" x2="${x}" y2="${(box.y + box.height).toFixed(1)}" />`;
      })
      .join("");
    return horizontal + vertical;
  }

  function buildCountryMarkers(rows, mapBox, countryCentroids) {
    const markers = new Map();

    rows.forEach((row) => {
      const country = String(row.country || "");
      if (hideCountryOnMap(country)) {
        return;
      }

      const centroid = resolveCountryCentroid(country, countryCentroids);
      const lat = centroid ? centroid.lat : row.lat;
      const lng = centroid ? centroid.lng : row.lng;
      if (!Number.isFinite(lat) || !Number.isFinite(lng)) {
        return;
      }

      const xy = latLngToXY(lat, lng, mapBox);
      const key = centroid ? `country:${normalizeCountryName(country)}` : `coord:${lat}:${lng}`;
      const weight = Math.max(1, Number(row.total || 0), Number(row.blocked || 0), Number(row.strictHits || 0));
      const existing = markers.get(key);

      if (!existing) {
        markers.set(key, {
          x: xy.x,
          y: xy.y,
          blocked: Number(row.blocked || 0),
          total: Number(row.total || 0),
          strictHits: Number(row.strictHits || 0),
          intensity: weight,
          ipCount: 1,
          country,
          label: country,
        });
        return;
      }

      existing.blocked += Number(row.blocked || 0);
      existing.total += Number(row.total || 0);
      existing.strictHits += Number(row.strictHits || 0);
      existing.intensity = Math.max(existing.intensity, existing.total, existing.blocked, existing.strictHits, weight);
      existing.ipCount += 1;
    });

    return Array.from(markers.values()).sort((a, b) => Number(b.intensity || 0) - Number(a.intensity || 0));
  }

  function parseCountryCentroids(csv) {
    const lines = String(csv || "")
      .split(/\r?\n/)
      .map((line) => line.trim())
      .filter((line) => line !== "");
    if (lines.length <= 1) {
      return new Map();
    }

    const headers = parseCsvLine(lines[0]);
    const longitudeIndex = headers.indexOf("longitude");
    const latitudeIndex = headers.indexOf("latitude");
    const countryIndex = headers.indexOf("COUNTRY");
    if (longitudeIndex === -1 || latitudeIndex === -1 || countryIndex === -1) {
      return new Map();
    }

    const centroids = new Map();
    lines.slice(1).forEach((line) => {
      const parts = parseCsvLine(line);
      const country = String(parts[countryIndex] || "").trim();
      const longitude = Number(parts[longitudeIndex] || "");
      const latitude = Number(parts[latitudeIndex] || "");
      if (country === "" || !Number.isFinite(longitude) || !Number.isFinite(latitude)) {
        return;
      }

      centroids.set(normalizeCountryName(country), { lat: latitude, lng: longitude });
    });

    return centroids;
  }

  function parseCsvLine(line) {
    const values = [];
    let current = "";
    let inQuotes = false;

    for (let index = 0; index < line.length; index += 1) {
      const char = line[index];
      if (char === '"') {
        if (inQuotes && line[index + 1] === '"') {
          current += '"';
          index += 1;
        } else {
          inQuotes = !inQuotes;
        }
        continue;
      }

      if (char === "," && !inQuotes) {
        values.push(current);
        current = "";
        continue;
      }

      current += char;
    }

    values.push(current);
    return values;
  }

  function resolveCountryCentroid(country, countryCentroids) {
    if (!(countryCentroids instanceof Map) || countryCentroids.size === 0) {
      return null;
    }

    const normalized = normalizeCountryName(country);
    if (normalized === "") {
      return null;
    }

    return countryCentroids.get(normalized) || null;
  }

  function hideCountryOnMap(country) {
    const normalized = normalizeCountryName(country);
    return normalized === "" || normalized === "unknown" || normalized === "unknown approx" || normalized === "local private";
  }

  function isUnknownCountry(country) {
    const normalized = normalizeCountryName(country);
    return normalized === "" || normalized === "unknown" || normalized === "unknown approx";
  }

  function normalizeCountryName(country) {
    const base = String(country || "")
      .trim()
      .toLowerCase()
      .normalize("NFD")
      .replace(/[\u0300-\u036f]/g, "")
      .replace(/[()]/g, " ")
      .replace(/[^a-z0-9]+/g, " ")
      .replace(/\s+/g, " ")
      .trim();

    const aliases = {
      "america": "united states",
      "united states of america": "united states",
      "usa": "united states",
      "u s a": "united states",
      "uk": "united kingdom",
      "great britain": "united kingdom",
      "england": "united kingdom",
      "russian federation": "russia",
      "korea republic of": "south korea",
      "republic of korea": "south korea",
      "korea south": "south korea",
      "korea democratic people s republic of": "north korea",
      "viet nam": "vietnam",
      "lao people s democratic republic": "laos",
      "syrian arab republic": "syria",
      "iran islamic republic of": "iran",
      "moldova republic of": "moldova",
      "venezuela bolivarian republic of": "venezuela",
      "bolivia plurinational state of": "bolivia",
      "tanzania united republic of": "tanzania",
      "brasil": "brazil",
      "deutschland": "germany",
      "osterreich": "austria",
      "suisse": "switzerland",
      "svizzera": "switzerland",
      "czech republic": "czechia",
      "turkiye": "turkey",
      "unknown approx": "unknown approx",
      "unbekannt": "unknown",
      "unbekannt approx": "unknown approx",
      "lokal privat": "local private",
    };

    return aliases[base] || base;
  }

  function markerStyle(intensity, maxIntensity) {
    const normalized = maxIntensity > 0 ? intensity / maxIntensity : 0;
    const radius = 6 + Math.sqrt(Math.max(0, normalized)) * 10;
    if (normalized >= 0.78) {
      return { radius, outerRadius: radius + 5.5, fill: "#ff6b57", glow: "#ffb39f" };
    }
    if (normalized >= 0.55) {
      return { radius, outerRadius: radius + 4.8, fill: "#f2b31a", glow: "#ffe39a" };
    }
    if (normalized >= 0.3) {
      return { radius, outerRadius: radius + 4.2, fill: "#1db7ff", glow: "#9ce3ff" };
    }
    return { radius, outerRadius: radius + 3.8, fill: "#0d4dc1", glow: "#87b8ff" };
  }

  function clamp(value, min, max) {
    return Math.min(max, Math.max(min, value));
  }

  function renderTableRows(rows) {
    if (!rows.length) {
      rowsContainer.innerHTML = "<tr><td colspan=\"4\">" + escapeHtml(t("no_ip_entries", "Keine geblockten IP-Eintraege im gewaehlten Zeitraum.")) + "</td></tr>";
      if (paginationContainer) paginationContainer.innerHTML = "";
      return;
    }

    sortedRows = summarizeUnknownRows(rows).sort((a, b) => {
      const aUnknown = isUnknownCountry(a.country) ? 1 : 0;
      const bUnknown = isUnknownCountry(b.country) ? 1 : 0;
      if (aUnknown !== bUnknown) {
        return aUnknown - bUnknown;
      }
      return Number(b.total || 0) - Number(a.total || 0);
    });
    const totalPages = Math.max(1, Math.ceil(sortedRows.length / PAGE_SIZE));
    currentPage = Math.max(1, Math.min(currentPage, totalPages));

    const start = (currentPage - 1) * PAGE_SIZE;
    const visibleRows = sortedRows.slice(start, start + PAGE_SIZE);

    rowsContainer.innerHTML = visibleRows
      .map((row) => {
        const hasBlocked = Number(row.blocked || 0) > 0;
        const hasStrict = Number(row.strictHits || 0) > 0;
        const statusHtml = buildStatusPills(hasStrict, hasBlocked);
        return (
          "<tr>" +
          "<td><code>" + escapeHtml(String(row.ipDisplay || row.ip)) + "</code></td>" +
          "<td>" + escapeHtml(String(row.country || t("unknown", "Unbekannt"))) + "</td>" +
          "<td>" + statusHtml + "</td>" +
          "<td>" + escapeHtml(String(row.total || row.blocked || 0)) + "</td>" +
          "</tr>"
        );
      })
      .join("");

    renderPagination(totalPages, sortedRows.length);
  }

  function summarizeUnknownRows(rows) {
    const knownRows = [];
    const unknownRows = [];

    rows.forEach((row) => {
      if (row && row.isUnknownAggregate) {
        knownRows.push(row);
        return;
      }

      if (isUnknownCountry(row && row.country)) {
        unknownRows.push(row);
        return;
      }

      knownRows.push(row);
    });

    if (!unknownRows.length) {
      return [...knownRows];
    }

    const aggregate = unknownRows.reduce(
      (acc, row) => {
        acc.blocked += Number(row.blocked || 0);
        acc.strictHits += Number(row.strictHits || 0);
        acc.total += Number(row.total || 0);
        return acc;
      },
      {
        ip: "",
        ipDisplay: "",
        blocked: 0,
        strictHits: 0,
        total: 0,
        country: t("unknown", "Unbekannt"),
        lat: null,
        lng: null,
        isUnknownAggregate: true,
      }
    );

    const rangeCount = unknownRows.length;
    aggregate.ipDisplay = `${rangeCount} ${rangeCount === 1 ? "anonymized range" : "anonymized ranges"}`;

    return [...knownRows, aggregate];
  }

  function renderPagination(totalPages, totalItems) {
    if (!paginationContainer) return;
    if (totalPages <= 1) {
      paginationContainer.innerHTML = "";
      return;
    }

    const pageItems = pageList(totalPages, currentPage);
    const parts = [];

    parts.push(
      `<button type="button" class="wpsh-pagination__btn" data-page="${currentPage - 1}" ${currentPage === 1 ? "disabled" : ""} aria-label="${escapeHtml(t("prev", "Vorherige Seite"))}">&lsaquo;</button>`
    );

    pageItems.forEach((item) => {
      if (item === "...") {
        parts.push('<span class="wpsh-pagination__dots">...</span>');
        return;
      }

      const p = Number(item);
      parts.push(
        `<button type="button" class="wpsh-pagination__btn ${p === currentPage ? "is-active" : ""}" data-page="${p}" ${p === currentPage ? "disabled" : ""}>${p}</button>`
      );
    });

    parts.push(
      `<button type="button" class="wpsh-pagination__btn" data-page="${currentPage + 1}" ${currentPage === totalPages ? "disabled" : ""} aria-label="${escapeHtml(t("next", "Naechste Seite"))}">&rsaquo;</button>`
    );

    const from = (currentPage - 1) * PAGE_SIZE + 1;
    const to = Math.min(totalItems, currentPage * PAGE_SIZE);
    parts.push(`<span class="wpsh-pagination__meta">${from}-${to} / ${totalItems}</span>`);

    paginationContainer.innerHTML = parts.join("");
  }

  if (paginationContainer) {
    paginationContainer.addEventListener("click", (event) => {
      const target = event.target.closest("button[data-page]");
      if (!target) return;
      const nextPage = Number(target.getAttribute("data-page"));
      if (!Number.isFinite(nextPage)) return;
      const totalPages = Math.max(1, Math.ceil(sortedRows.length / PAGE_SIZE));
      if (nextPage < 1 || nextPage > totalPages || nextPage === currentPage) return;
      currentPage = nextPage;
      renderTableRows(sortedRows);
    });
  }

  function pageList(totalPages, current) {
    if (totalPages <= 10) {
      return Array.from({ length: totalPages }, (_, i) => i + 1);
    }

    const items = [1];
    const windowStart = Math.max(2, current - 1);
    const windowEnd = Math.min(totalPages - 1, current + 1);

    if (windowStart > 2) items.push("...");
    for (let p = windowStart; p <= windowEnd; p += 1) {
      items.push(p);
    }
    if (windowEnd < totalPages - 1) items.push("...");
    items.push(totalPages);
    return items;
  }

  function buildStatusPills(hasStrict, hasBlocked) {
    const pills = [];
    if (hasStrict) {
      pills.push('<span class="wpsh-status-pill wpsh-status-pill--strict">' + escapeHtml(t("strict_mode", "Strict difficulty")) + "</span>");
    }
    if (hasBlocked) {
      pills.push('<span class="wpsh-status-pill wpsh-status-pill--blocked">' + escapeHtml(t("blocked", "Blocked")) + "</span>");
    }
    if (!pills.length) {
      pills.push('<span class="wpsh-status-pill">' + escapeHtml(t("suspicious", "Suspicious")) + "</span>");
    }
    return pills.join(" ");
  }

  function t(key, fallback) {
    const value = i18n[key];
    return typeof value === "string" && value !== "" ? value : fallback;
  }

  function isPrivateIp(ip) {
    return (
      ip === "127.0.0.1" ||
      ip === "::1" ||
      ip.startsWith("10.") ||
      ip.startsWith("192.168.") ||
      /^172\.(1[6-9]|2[0-9]|3[0-1])\./.test(ip)
    );
  }

  function pseudoLatLngFromIp(ip) {
    let hash = 0;
    for (let i = 0; i < ip.length; i += 1) {
      hash = (hash * 31 + ip.charCodeAt(i)) >>> 0;
    }
    const lat = ((hash % 14000) / 100) - 70;
    const lng = (((hash / 14000) % 36000) / 100) - 180;
    return { lat, lng };
  }

  function escapeHtml(value) {
    return value
      .replaceAll("&", "&amp;")
      .replaceAll("<", "&lt;")
      .replaceAll(">", "&gt;")
      .replaceAll('"', "&quot;")
      .replaceAll("'", "&#39;");
  }

})();
