{include file="_partials/head.tpl"}

<style>
  .czm-box{background:var(--surface);border:1px solid var(--border);border-radius:14px;padding:16px 18px;margin-bottom:16px}
  .czm-stat{display:flex;gap:26px;margin:0 0 14px;flex-wrap:wrap;align-items:center}
  .czm-stat .n{font-size:24px;font-weight:800;color:var(--text);line-height:1}
  .czm-stat .l{font-size:12px;color:var(--text-muted)}
  .czm-live{display:flex;align-items:center;gap:9px;background:rgba(16,185,129,.12);border:1px solid rgba(16,185,129,.32);color:var(--success);border-radius:999px;padding:7px 14px}
  .czm-live .n{font-size:22px;font-weight:800;color:var(--success);line-height:1}
  .czm-live .l{font-size:12px;color:var(--success);font-weight:600}
  .czm-live .dot{width:10px;height:10px;border-radius:50%;background:#22e07a;box-shadow:0 0 0 0 rgba(34,224,122,.7);animation:rtp 1.6s infinite;flex-shrink:0}
  @keyframes rtp{0%{box-shadow:0 0 0 0 rgba(34,224,122,.6)}70%{box-shadow:0 0 0 9px rgba(34,224,122,0)}100%{box-shadow:0 0 0 0 rgba(34,224,122,0)}}
  .czm-mapwrap{position:relative}
  .czm-host{position:relative}
  .czm-host svg{width:100%;height:auto;display:block;cursor:grab;touch-action:none}
  .czm-host svg:active{cursor:grabbing}
  .czm-zoom{position:absolute;top:10px;right:10px;display:flex;flex-direction:column;gap:6px;z-index:10}
  .czm-zoom button{width:34px;height:34px;border:1px solid var(--border);background:var(--surface);color:var(--text);border-radius:9px;font-size:19px;font-weight:700;line-height:1;cursor:pointer;box-shadow:0 2px 6px rgba(0,0,0,.14);display:flex;align-items:center;justify-content:center;padding:0}
  .czm-zoom button:hover{background:var(--surface-2);border-color:#1797F0;color:#1797F0}
  .czm-zoom button:active{transform:translateY(1px)}
  .czm-host svg path{fill:var(--border);stroke:var(--surface);stroke-width:.4;transition:fill .15s,stroke .15s}
  .czm-host svg path[data-c]:hover{stroke:var(--text);stroke-width:1;cursor:pointer}
  /* live-маркеры (рисуются поверх стран в том же SVG) */
  .cz-mk{cursor:pointer}
  .cz-mk-ring{fill:#22e07a;animation:czmk 1.8s ease-out infinite;pointer-events:none}
  .cz-mk-dot{fill:#16a34a;stroke:#fff;stroke-width:.7}
  .cz-mk-num{fill:#fff;font:700 6px system-ui,sans-serif;text-anchor:middle;dominant-baseline:central;pointer-events:none}
  @keyframes czmk{0%{r:2px;opacity:.55}100%{r:14px;opacity:0}}
  .czm-tip{position:fixed;z-index:50;background:var(--text);color:var(--surface);font-size:12.5px;padding:7px 11px;border-radius:8px;pointer-events:none;display:none;white-space:nowrap;box-shadow:0 6px 18px rgba(0,0,0,.3)}
  .czm-tip b{color:var(--surface)}
  .czm-tip img{width:18px;height:13px;border-radius:2px;vertical-align:-2px;margin-right:6px;box-shadow:0 0 0 1px rgba(127,127,127,.35)}
  .czm-tip.is-live{border-left:3px solid #22e07a;padding-left:9px}
  .czm-tip .lv{display:inline-block;width:7px;height:7px;border-radius:50%;background:#22e07a;margin-right:6px;vertical-align:0}
  .czm-legend{display:flex;align-items:center;gap:10px;margin-top:12px;font-size:12px;color:var(--text-muted);flex-wrap:wrap}
  .czm-legend .bar{flex:1;max-width:200px;height:9px;border-radius:5px;background:linear-gradient(90deg,hsl(204,85%,75%),hsl(204,85%,30%))}
  .czm-legend .live-key{display:inline-flex;align-items:center;gap:6px}
  .czm-legend .live-key i{width:10px;height:10px;border-radius:50%;background:#16a34a;box-shadow:0 0 0 3px rgba(34,224,122,.35);display:inline-block}
  .czm-cols{display:grid;grid-template-columns:1fr 1fr;gap:16px}
  @media(max-width:860px){.czm-cols{grid-template-columns:1fr}}
  .czm-list{display:flex;flex-direction:column}
  .czm-row{display:flex;align-items:center;gap:10px;padding:8px 0;border-bottom:1px solid var(--border-soft)}
  .czm-flag{width:24px;height:18px;border-radius:3px;object-fit:cover;box-shadow:0 0 0 1px rgba(0,0,0,.08);flex-shrink:0}
  .czm-row .nm{flex:1;color:var(--text);font-size:13.5px}
  .czm-row .ct{color:#16a34a;font-weight:700;font-size:13.5px}
  .czm-feed-row{display:flex;align-items:center;gap:10px;padding:8px 0;border-bottom:1px solid var(--border-soft);font-size:13px}
  .czm-feed-row .nm{flex:1;color:var(--text)}
  .czm-feed-row .src{background:var(--surface-2);color:var(--text-muted);border-radius:999px;padding:1px 8px;font-size:11px;white-space:nowrap}
  .czm-feed-row .tm{color:var(--text-muted);font-variant-numeric:tabular-nums;font-size:12px}
  .czm-empty{color:var(--text-muted);font-size:13px;padding:10px 2px}
  .czm-sec-h{font-size:15px;margin:0 0 8px;color:var(--text);display:flex;align-items:center;gap:8px}
  .an-acts{display:flex;gap:8px;align-items:center}
</style>

<div class="page-head">
  <div><h1>Карта мира — кто откуда</h1></div>
  <div class="an-acts">
    <a href="/admin/analytics/realtime" class="btn btn-secondary">{icon name="chart" size="16"} Realtime</a>
    <a href="/admin/analytics" class="btn btn-secondary">{icon name="chart" size="16"} Дашборд</a>
  </div>
</div>

<div class="tabs-bar">
  <a href="/admin/analytics/map?range=today" class="tab [if {is_today}="1"]is-active[/if]">Сегодня</a>
  <a href="/admin/analytics/map?range=7d" class="tab [if {is_7d}="1"]is-active[/if]">7 дней</a>
  <a href="/admin/analytics/map?range=30d" class="tab [if {is_30d}="1"]is-active[/if]">30 дней</a>
  <a href="/admin/analytics/map?range=all" class="tab [if {is_all}="1"]is-active[/if]">Всё время</a>
</div>

<div class="czm-box">
  <div class="czm-stat">
    <div class="czm-live"><span class="dot"></span><div><span class="n" id="czLiveOnline">0</span> <span class="l">онлайн сейчас</span></div></div>
    <div><div class="n">{total}</div><div class="l">визитов за период</div></div>
    <div><div class="n">{countries}</div><div class="l">стран за период</div></div>
  </div>
  <div class="czm-mapwrap">
    <div class="czm-host" id="czMapHost"><p class="muted" style="font-size:13px">Загрузка карты…</p></div>
    <div class="czm-zoom">
      <button type="button" id="czZin" title="Приблизить" aria-label="Приблизить">+</button>
      <button type="button" id="czZout" title="Отдалить" aria-label="Отдалить">&minus;</button>
      <button type="button" id="czZrst" title="Сбросить масштаб" aria-label="Сбросить масштаб">&#8635;</button>
    </div>
  </div>
  <div class="czm-tip" id="czMapTip"></div>
  <div class="czm-legend">
    <span>заливка по странам за период:</span><span>меньше</span><span class="bar"></span><span>больше</span>
    <span class="live-key"><i></i> зелёная точка — посетитель прямо сейчас (за 5 мин)</span>
  </div>
</div>

<div class="czm-cols">
  <div class="czm-box">
    <h3 class="czm-sec-h">{icon name="chart" size="16"} Сейчас онлайн — по странам</h3>
    <div class="czm-list" id="czLiveList"><p class="czm-empty">Пока никого. Точки появятся, как только кто-то зайдёт на сайт.</p></div>
  </div>
  <div class="czm-box">
    <h3 class="czm-sec-h">{icon name="chart" size="16"} Последние посетители</h3>
    <div class="czm-list" id="czLiveFeed"><p class="czm-empty">Лента активности за последние 5 минут.</p></div>
  </div>
</div>

[if {is_empty}="1"][else]
<div class="czm-box">
  <h3 class="czm-sec-h">Страны за период</h3>
  <div class="czm-list" style="display:grid;grid-template-columns:repeat(2,1fr);gap:8px 26px">
    [foreach var="list" as="c"]
      <div class="czm-row">
        [if {c.flag}]<img class="czm-flag" src="{c.flag}" alt="{c.code}" loading="lazy" onerror="this.style.display='none'">[/if]
        <span class="nm">{c.name}</span>
        <span class="ct" style="color:#1797F0">{c.count}</span>
      </div>
    [/foreach]
  </div>
  <p class="muted" style="font-size:11px;margin-top:14px">Данные о странах: <a href="https://db-ip.com" target="_blank" rel="noopener">DB-IP.com</a> (CC BY 4.0). IP определяется на сервере и не сохраняется.</p>
</div>
[/if]

<script type="application/json" id="czMapData">{"counts":{map_counts|raw},"names":{map_names|raw},"max":{map_max}}</script>
<script>
(function () {
  var SVGNS = 'http://www.w3.org/2000/svg';
  var host = document.getElementById('czMapHost');
  var tip = document.getElementById('czMapTip');
  var onlineEl = document.getElementById('czLiveOnline');
  var listEl = document.getElementById('czLiveList');
  var feedEl = document.getElementById('czLiveFeed');
  var D;
  try { D = JSON.parse(document.getElementById('czMapData').textContent); } catch (e) { D = { counts: {}, names: {}, max: 1 }; }

  var svgRoot = null;         // корневой <svg> после загрузки
  var pending = false;        // есть live-данные, ждём готовности SVG
  var liveCounts = {};        // cc_low => сессий (страны без координат)
  var liveCities = {};        // cc_low => "Дублин, Корк" (для тултипа маркера-страны)
  var livePoints = [];        // [{lat,lon,city,cc,name,n}] — точные точки городов

  // Проекция карты world.svg (откалибрована): X линейный, Y Меркатор.
  function project(lat, lon) {
    if (typeof lat !== 'number' || typeof lon !== 'number' || isNaN(lat) || isNaN(lon)) return null;
    if (lat > 85) lat = 85; else if (lat < -85) lat = -85;
    var x = 2.862 * lon + 473.947;
    var y = -167.564 * Math.log(Math.tan(Math.PI / 4 + lat * Math.PI / 360)) + 468.831;
    return [x, y];
  }

  // ── Зум/панорамирование через viewBox (векторно, без внешних либ) ──
  var MAXZOOM = 10;
  var base = null;            // исходный viewBox {x,y,w,h}
  var view = null;            // текущий viewBox {x,y,w}; h = w*aspect
  var aspect = 1;
  var dragging = false, dragMoved = false, lastX = 0, lastY = 0;

  function vh() { return view.w * aspect; }
  function setVB() { svgRoot.setAttribute('viewBox', view.x + ' ' + view.y + ' ' + view.w + ' ' + vh()); }
  function clampPan() {
    if (view.w > base.w) view.w = base.w;
    var h = vh();
    view.x = Math.max(base.x, Math.min(view.x, base.x + base.w - view.w));
    view.y = Math.max(base.y, Math.min(view.y, base.y + base.h - h));
  }
  function clientToSvg(cx, cy) {
    var r = svgRoot.getBoundingClientRect();
    return { x: view.x + (cx - r.left) / r.width * view.w, y: view.y + (cy - r.top) / r.height * vh() };
  }
  function zoomAt(cx, cy, factor) {
    var pt = clientToSvg(cx, cy);
    var rx = (pt.x - view.x) / view.w, ry = (pt.y - view.y) / vh();
    var minW = base.w / MAXZOOM;
    var newW = Math.max(minW, Math.min(view.w * factor, base.w));
    view.w = newW;
    view.x = pt.x - rx * newW;
    view.y = pt.y - ry * vh();
    clampPan(); setVB();
  }
  function zoomCenter(factor) { var r = svgRoot.getBoundingClientRect(); zoomAt(r.left + r.width / 2, r.top + r.height / 2, factor); }
  function resetView() { view = { x: base.x, y: base.y, w: base.w }; setVB(); }

  function initZoom() {
    var vb = (svgRoot.getAttribute('viewBox') || '0 0 1010 666').split(/[\s,]+/).map(Number);
    base = { x: vb[0] || 0, y: vb[1] || 0, w: vb[2] || 1010, h: vb[3] || 666 };
    aspect = base.h / base.w;
    view = { x: base.x, y: base.y, w: base.w };

    svgRoot.addEventListener('wheel', function (e) {
      e.preventDefault();
      zoomAt(e.clientX, e.clientY, e.deltaY < 0 ? 0.82 : 1.22);
    }, { passive: false });
    svgRoot.addEventListener('dblclick', function (e) { e.preventDefault(); zoomAt(e.clientX, e.clientY, 0.6); });
    svgRoot.addEventListener('pointerdown', function (e) {
      dragging = true; dragMoved = false; lastX = e.clientX; lastY = e.clientY;
      try { svgRoot.setPointerCapture(e.pointerId); } catch (err) {}
    });
    svgRoot.addEventListener('pointermove', function (e) {
      if (!dragging) return;
      var r = svgRoot.getBoundingClientRect();
      var dx = (e.clientX - lastX) / r.width * view.w;
      var dy = (e.clientY - lastY) / r.height * vh();
      if (Math.abs(e.clientX - lastX) + Math.abs(e.clientY - lastY) > 2) dragMoved = true;
      view.x -= dx; view.y -= dy; lastX = e.clientX; lastY = e.clientY;
      tip.style.display = 'none';
      clampPan(); setVB();
    });
    function endDrag() { dragging = false; }
    svgRoot.addEventListener('pointerup', endDrag);
    svgRoot.addEventListener('pointercancel', endDrag);

    var zi = document.getElementById('czZin'), zo = document.getElementById('czZout'), zr = document.getElementById('czZrst');
    if (zi) zi.addEventListener('click', function () { zoomCenter(0.7); });
    if (zo) zo.addEventListener('click', function () { zoomCenter(1.43); });
    if (zr) zr.addEventListener('click', resetView);
  }

  function esc(s) { var d = document.createElement('div'); d.textContent = (s == null ? '' : String(s)); return d.innerHTML; }

  // Хороплет-заливка по странам за период
  function paintChoropleth() {
    var paths = svgRoot.querySelectorAll('path[id]');
    Array.prototype.forEach.call(paths, function (p) {
      var c = D.counts[p.id] || 0;
      if (c > 0) {
        var t = c / (D.max || 1);
        p.style.fill = 'hsl(204,85%,' + (75 - 45 * t) + '%)';
        p.setAttribute('data-c', c);
      }
    });
  }

  // Один маркер: пульсирующее кольцо + точка (+ число, если >1).
  // Данные тултипа — в data-атрибутах, показ через кастомный .czm-tip (делегация на host).
  function makeMarker(cx, cy, n, place, cc, detail) {
    var grp = document.createElementNS(SVGNS, 'g');
    grp.setAttribute('class', 'cz-mk');
    grp.setAttribute('data-place', place || '');
    grp.setAttribute('data-n', n);
    if (cc) grp.setAttribute('data-cc', cc);
    if (detail) grp.setAttribute('data-detail', detail);
    var ring = document.createElementNS(SVGNS, 'circle');
    ring.setAttribute('class', 'cz-mk-ring');
    ring.setAttribute('cx', cx); ring.setAttribute('cy', cy); ring.setAttribute('r', '2');
    grp.appendChild(ring);
    var dot = document.createElementNS(SVGNS, 'circle');
    dot.setAttribute('class', 'cz-mk-dot');
    dot.setAttribute('cx', cx); dot.setAttribute('cy', cy);
    dot.setAttribute('r', n > 1 ? '4.5' : '3.2');
    grp.appendChild(dot);
    if (n > 1) {
      var num = document.createElementNS(SVGNS, 'text');
      num.setAttribute('class', 'cz-mk-num');
      num.setAttribute('x', cx); num.setAttribute('y', cy);
      num.textContent = n;
      grp.appendChild(num);
    }
    return grp;
  }

  // Перерисовать live-слой: точные точки городов + страны без координат (в центр страны)
  function renderMarkers() {
    if (!svgRoot) return;
    var old = svgRoot.querySelector('#czLiveLayer');
    if (old) old.parentNode.removeChild(old);
    var g = document.createElementNS(SVGNS, 'g');
    g.setAttribute('id', 'czLiveLayer');
    var covered = {}; // страны, уже показанные точной точкой

    // 1) точные точки по координатам города
    (livePoints || []).forEach(function (p) {
      var n = p.n || 0;
      if (n <= 0) return;
      var xy = project(p.lat, p.lon);
      if (!xy) return;
      if (p.cc) covered[p.cc.toLowerCase()] = true;
      var place = (p.city && p.name && p.name !== p.city) ? (p.city + ', ' + p.name) : (p.city || p.name || '');
      g.appendChild(makeMarker(xy[0], xy[1], n, place, p.cc || ''));
    });

    // 2) страны без координат (нет mmdb) — пульс в центр страны, как fallback
    Object.keys(liveCounts || {}).forEach(function (cc) {
      if (covered[cc]) return;
      var n = liveCounts[cc] || 0;
      if (n <= 0) return;
      var path = svgRoot.getElementById(cc);
      if (!path) return;
      var bb;
      try { bb = path.getBBox(); } catch (e) { return; }
      if (!bb || (bb.width === 0 && bb.height === 0)) return;
      g.appendChild(makeMarker(bb.x + bb.width / 2, bb.y + bb.height / 2, n,
        (D.names[cc] || cc.toUpperCase()), cc.toUpperCase(), liveCities[cc] || ''));
    });

    svgRoot.appendChild(g);
  }

  function renderPanels(d) {
    // Список стран онлайн
    if (d.list && d.list.length) {
      var lh = '';
      d.list.forEach(function (c) {
        lh += '<div class="czm-row">'
            + (c.flag ? '<img class="czm-flag" src="' + esc(c.flag) + '" alt="' + esc(c.code) + '" onerror="this.style.display=\'none\'">' : '')
            + '<span class="nm">' + esc(c.name) + '</span>'
            + '<span class="ct">' + esc(c.count) + '</span></div>';
      });
      listEl.innerHTML = lh;
    } else {
      listEl.innerHTML = '<p class="czm-empty">Пока никого. Точки появятся, как только кто-то зайдёт на сайт.</p>';
    }
    // Лента последних посетителей
    if (d.feed && d.feed.length) {
      var fh = '';
      d.feed.forEach(function (e) {
        var dev = e.device ? (' · ' + esc(e.device)) : '';
        var place = e.city && e.name ? (esc(e.city) + ', ' + esc(e.name)) : (esc(e.city || e.name) || '—');
        fh += '<div class="czm-feed-row">'
            + (e.flag ? '<img class="czm-flag" src="' + esc(e.flag) + '" alt="' + esc(e.cc) + '" onerror="this.style.display=\'none\'">' : '')
            + '<span class="nm">' + place + dev + '</span>'
            + '<span class="src">' + esc(e.source) + '</span>'
            + '<span class="tm">' + esc(e.time) + '</span></div>';
      });
      feedEl.innerHTML = fh;
    } else {
      feedEl.innerHTML = '<p class="czm-empty">За последние 5 минут активности нет.</p>';
    }
  }

  function applyLive(d) {
    if (!d || !d.ok) return;
    onlineEl.textContent = d.online || 0;
    liveCounts = d.counts || {};
    liveCities = d.cities || {};
    livePoints = d.points || [];
    renderPanels(d);
    if (svgRoot) { renderMarkers(); pending = false; }
    else { pending = true; }
  }

  function loadLive() {
    if (document.visibilityState === 'hidden') return;
    fetch('/admin/analytics/map/live', { headers: { 'Accept': 'application/json' } })
      .then(function (r) { return r.json(); })
      .then(applyLive)
      .catch(function () {});
  }

  // Загрузка SVG-карты, затем заливка + live-слой
  fetch('/public/assets/analytics/world.svg', { headers: { 'Accept': 'image/svg+xml' } })
    .then(function (r) { return r.text(); })
    .then(function (svg) {
      host.innerHTML = svg;
      svgRoot = host.querySelector('svg');
      if (svgRoot) {
        paintChoropleth();
        initZoom();
        if (pending) { renderMarkers(); pending = false; }
      }
    })
    .catch(function () { host.innerHTML = '<p class="muted" style="font-size:13px">Не удалось загрузить карту.</p>'; });

  // Кастомный тултип: live-маркер (город/страна, онлайн) приоритетнее хороплета (страна за период)
  function moveTip(e) {
    tip.style.left = (e.clientX + 14) + 'px';
    tip.style.top = (e.clientY + 14) + 'px';
  }
  host.addEventListener('mousemove', function (e) {
    if (dragging) { tip.style.display = 'none'; return; }
    var mk = e.target.closest ? e.target.closest('.cz-mk') : null;
    if (mk) {
      var place = mk.getAttribute('data-place') || '';
      var det = mk.getAttribute('data-detail');
      var n = mk.getAttribute('data-n') || '0';
      var cc = mk.getAttribute('data-cc');
      var flag = cc ? '<img src="/public/assets/analytics/flags/' + cc.toLowerCase() + '.svg" onerror="this.remove()">' : '';
      tip.className = 'czm-tip is-live';
      tip.innerHTML = '<span class="lv"></span>' + flag + esc(place) + (det ? ' · ' + esc(det) : '') + ' — <b>' + esc(n) + '</b> онлайн';
      tip.style.display = 'block';
      moveTip(e);
      return;
    }
    var p = e.target.closest ? e.target.closest('path[id]') : null;
    if (!p) { tip.style.display = 'none'; return; }
    var id = p.id;
    tip.className = 'czm-tip';
    tip.innerHTML = esc(D.names[id] || id.toUpperCase()) + ' — <b>' + (D.counts[id] || 0) + '</b> за период';
    tip.style.display = 'block';
    moveTip(e);
  });
  host.addEventListener('mouseleave', function () { tip.style.display = 'none'; });

  // Старт реалтайма
  loadLive();
  setInterval(loadLive, 5000);
  document.addEventListener('visibilitychange', function () { if (document.visibilityState === 'visible') loadLive(); });
})();
</script>

{include file="_partials/foot.tpl"}
