/**
 * VisitorLens — Replay Page JS
 * Full visual session replay engine.
 * Ported from reference replay.php; adapted to use VSR REST API helpers.
 */
(function () {
  'use strict';

  if (typeof vlensAdmin === 'undefined') return;

  const shell = document.getElementById('vsr-replay-shell');
  if (!shell) return;

  // ── THEME ──────────────────────────────────────────────────────
  const saved = localStorage.getItem('vlens_replay_theme') || 'light';
  setTheme(saved);

  function setTheme(t) {
    shell.setAttribute('data-theme', t);
    localStorage.setItem('vlens_replay_theme', t);
    const btn = document.getElementById('vsrReplayThemeToggle');
    if (btn) btn.textContent = t === 'dark' ? '☀ Light' : '☾ Dark';
  }

  const themeBtn = document.getElementById('vsrReplayThemeToggle');
  if (themeBtn) {
    themeBtn.addEventListener('click', () => {
      setTheme(shell.getAttribute('data-theme') === 'dark' ? 'light' : 'dark');
    });
  }

  // ── SESSION LIST ───────────────────────────────────────────────
  let currentPage = 1;
  let filterTimer;

  async function loadSessions(page = 1) {
    currentPage = page;
    const list = document.getElementById('sessionList');
    if (list) list.innerHTML = '<div style="padding:32px;text-align:center;color:var(--text3)">Loading sessions\u2026</div>';

    const uri = (document.getElementById('uriFilter') || {}).value || '';
    const rangeVal = (document.getElementById('filterRange') || {}).value || '';
    const deviceVal = (document.getElementById('filterDevice') || {}).value || '';

    let path = 'replay/sessions?page=' + page;
    if (uri.trim()) path += '&uri=' + encodeURIComponent(uri.trim());
    if (deviceVal) path += '&device=' + encodeURIComponent(deviceVal);

    // Calculate date range
    if (rangeVal) {
      const now = new Date();
      let from;
      if (rangeVal === 'today') {
        from = new Date(now.getFullYear(), now.getMonth(), now.getDate());
      } else {
        const days = parseInt(rangeVal) || 7;
        from = new Date(now.getTime() - days * 86400000);
      }
      const fmt = d => d.getFullYear() + '-' + String(d.getMonth() + 1).padStart(2, '0') + '-' + String(d.getDate()).padStart(2, '0');
      path += '&date_from=' + fmt(from) + '&date_to=' + fmt(now);
    }
    try {
      const d = await VSR.get(path);
      renderSessions(d.rows || [], d.total || 0, d.page || 1, d.limit || 30, d.total_ips || 0, d.ip_order || [], d.online_ips || []);
    } catch (e) {
      console.error('VSR Replay: loadSessions error', e);
      if (list) list.innerHTML = '<div style="padding:32px;text-align:center;color:var(--text3)">\u26a0 Failed to load sessions — check browser console for details.</div>';
      const lc = document.getElementById('listCount');
      if (lc) lc.textContent = 'Error';
    }
  }

  function renderSessions(rows, total, page, limit, totalIps, ipOrder, onlineIps) {
    document.getElementById('listCount').textContent = total.toLocaleString() + ' sessions';
    const list = document.getElementById('sessionList');

    if (!rows.length) {
      list.innerHTML = '<div style="padding:32px;text-align:center;color:var(--text3)">No sessions found</div>';
      updatePagination(0, 1, 1);
      return;
    }

    // Group by IP
    const groups = {};
    rows.forEach(s => {
      const ip = s.ip_raw || 'Unknown';
      if (!groups[ip]) groups[ip] = [];
      groups[ip].push(s);
    });

    window._vsrSessionGroups = groups;

    const orderedIps = ipOrder.length ? ipOrder : Object.keys(groups);
    let html = '';

    orderedIps.forEach(ip => {
      const sessions = groups[ip];
      if (!sessions || !sessions.length) return;

      const cnt = sessions.length;
      const totalMs = sessions.reduce((sum, s) => sum + (parseInt(s.duration_ms) || 0), 0);
      const isOnline = onlineIps.includes(ip);
      const onlineDot = isOnline ? '<span class="online-dot-ip" title="Online now"></span>' : '';
      const lastRec = getIpLastRec(sessions);

      html += '<div class="ip-group" data-ip="' + esc(ip) + '">';
      html += '<div class="ip-group-header" onclick="vsrReplay.toggleIpGroup(this)">';
      html += '<span class="ip-group-arrow">&#9654;</span>';
      const userSession = sessions.find(s => s.wp_user_id && +s.wp_user_id > 0);
      html += '<div class="ip-group-info">';
      html += '<div class="ip-group-info-top">';
      html += '<span class="ip-group-ip">' + esc(maskIp(ip)) + '</span>';
      if (userSession) {
        const editUrl = vlensAdmin.adminUrl + 'user-edit.php?user_id=' + userSession.wp_user_id;
        html += '<a class="ip-group-user" href="' + esc(editUrl) + '" target="_blank" data-user-id="' + esc(userSession.wp_user_id) + '" onclick="event.stopPropagation()">' + esc(userSession.wp_display_name || 'User #' + userSession.wp_user_id) + '</a>';
      }
      html += '</div>';
      html += '<div class="ip-group-last-rec">Last: ' + lastRec + '</div>';
      html += '</div>';
      html += '<span class="ip-group-time">' + VSR.fmtDur(totalMs) + '</span>';
      html += '<span class="ip-group-count">' + cnt + ' session' + (cnt !== 1 ? 's' : '') + '</span>';
      html += onlineDot;
      html += '<button class="ip-group-play" title="Play all sessions for this IP" onclick="event.stopPropagation();vsrReplay.playIpGroup(\'' + esc(ip) + '\',this)">';
      html += '<svg width="10" height="10" fill="currentColor" viewBox="0 0 24 24"><polygon points="5 3 19 12 5 21 5 3"/></svg>';
      html += '</button>';
      html += '<button class="ip-group-del" title="Delete all sessions for this IP" onclick="event.stopPropagation();vsrReplay.deleteIpGroup(\'' + esc(ip) + '\',this)">';
      html += '<svg width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/></svg>';
      html += '</button>';
      html += '</div>';
      html += '<div class="ip-group-body">';

      sessions.forEach(s => {
        const dur = s.duration_ms ? VSR.fmtDur(s.duration_ms) : '—';
        const rage = s.rage_clicks > 0 ? '<span class="sr-badge badge-rage">' + s.rage_clicks + ' rage</span>' : '';
        const title = s.page_title || '';
        const faviconUrl = vlensAdmin.siteUrl + '/favicon.ico';
        html += '<div class="session-row" data-id="' + s.id + '" onclick="vsrReplay.selectSession(' + s.id + ',this)">';
        html += '<div class="sr-top">';
        html += '<img class="sr-favicon" src="' + esc(faviconUrl) + '" onerror="this.style.display=\'none\'" alt="">';
        html += '<div class="sr-uri" title="' + esc(s.page_uri) + '">' + esc(s.page_uri) + '</div>';
        html += '<div class="sr-time">' + VSR.fmtTime(s.started_at) + '</div>';
        html += '</div>';
        if (title) {
          html += '<div class="sr-page-title" title="' + esc(title) + '">' + esc(title) + '</div>';
        }
        html += '<div class="sr-meta">';
        html += '<span class="sr-badge badge-dur">' + dur + '</span>';
        html += '<span class="sr-badge badge-click">' + (s.click_count || 0) + ' clicks</span>';
        html += '<span class="sr-badge badge-scroll">' + (s.scroll_count || 0) + ' scrolls</span>';
        html += rage;
        html += '<span class="sr-badge badge-vp">' + deviceIcon(s.viewport_w) + ' ' + (s.viewport_w || '?') + '×' + (s.viewport_h || '?') + '</span>';
        html += '</div>';
        html += '<div class="sr-scroll-bar"><div class="sr-scroll-fill" style="width:' + (s.max_scroll_pct || 0) + '%"></div></div>';
        html += '</div>';
      });

      html += '</div></div>';
    });

    list.innerHTML = html;
    updatePagination(totalIps, page, limit);
  }

  function updatePagination(totalIps, page, limit) {
    const totalPages = Math.max(1, Math.ceil(totalIps / limit));
    const prev = document.getElementById('pgPrev');
    const next = document.getElementById('pgNext');
    const info = document.getElementById('pgInfo');
    if (prev) prev.disabled = page <= 1;
    if (next) next.disabled = page >= totalPages;
    if (info) info.textContent = 'Page ' + page + ' of ' + totalPages;
  }

  function getIpLastRec(sessions) {
    try {
      let latestMs = 0;
      sessions.forEach(s => {
        let endMs = s.ended_at ? new Date(s.ended_at).getTime() : 0;
        if (!endMs && s.started_at) {
          endMs = new Date(s.started_at).getTime() + (parseInt(s.duration_ms) || 0);
        }
        if (endMs > latestMs) latestMs = endMs;
      });
      if (!latestMs) return '—';
      return new Date(latestMs).toLocaleString('en-GB', {
        day: 'numeric', month: 'short',
        hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: true
      });
    } catch (e) { return '—'; }
  }

  document.getElementById('pgPrev').addEventListener('click', () => loadSessions(currentPage - 1));
  document.getElementById('pgNext').addEventListener('click', () => loadSessions(currentPage + 1));
  document.getElementById('uriFilter').addEventListener('input', () => {
    clearTimeout(filterTimer);
    filterTimer = setTimeout(() => loadSessions(1), 400);
  });
  document.getElementById('filterRange').addEventListener('change', () => loadSessions(1));
  document.getElementById('filterDevice').addEventListener('change', () => loadSessions(1));

  // ── IP GROUP ACTIONS ───────────────────────────────────────────
  function toggleIpGroup(header) {
    const group = header.closest('.ip-group') || header.parentElement;
    const body  = group.querySelector('.ip-group-body');
    const arrow = group.querySelector('.ip-group-arrow');
    const isOpen = group.classList.contains('expanded');
    body.style.display = isOpen ? 'none' : 'block';
    arrow.style.transform = isOpen ? 'rotate(0deg)' : 'rotate(90deg)';
    group.classList.toggle('expanded', !isOpen);
  }

  async function deleteIpGroup(ip, btn) {
    if (!confirm('Delete all sessions for IP ' + maskIp(ip) + '?')) return;
    btn.disabled = true;
    try {
      const d = await VSR.del('replay/delete-ip?ip=' + encodeURIComponent(ip));
      if (d.ok) loadSessions(currentPage);
    } catch (e) {
      alert('Delete failed');
      btn.disabled = false;
    }
  }

  // ── IP PLAYLIST ────────────────────────────────────────────────
  let ipPlaylist    = [];
  let ipPlaylistIdx = -1;
  let ipPlayBtn     = null;

  async function playIpGroup(ip, btn) {
    if (ipPlayBtn === btn && ipPlaylist.length) { stopIpPlaylist(); return; }
    stopIpPlaylist();
    ipPlayBtn = btn;
    btn.classList.add('playing');
    btn.title = 'Stop playlist';
    btn.innerHTML = '<svg width="10" height="10" fill="currentColor" viewBox="0 0 24 24"><rect x="6" y="4" width="4" height="16"/><rect x="14" y="4" width="4" height="16"/></svg>';

    const group = btn.closest('.ip-group');
    let sessionsForIp = (window._vsrSessionGroups && window._vsrSessionGroups[ip]) || [];

    if (!sessionsForIp.length) {
      const rows = Array.from(group.querySelectorAll('.session-row[data-id]'));
      ipPlaylist = rows.map(r => parseInt(r.dataset.id)).sort((a, b) => a - b);
    } else {
      const sorted = [...sessionsForIp].sort((a, b) => new Date(b.started_at) - new Date(a.started_at));
      const mostRecent = sorted[0];
      const mostRecentDate = mostRecent.started_at ? new Date(mostRecent.started_at).toDateString() : null;
      const dayGroup = mostRecentDate
        ? sorted.filter(s => s.started_at && new Date(s.started_at).toDateString() === mostRecentDate)
        : sorted;
      dayGroup.sort((a, b) => new Date(a.started_at) - new Date(b.started_at));
      ipPlaylist = dayGroup.map(s => parseInt(s.id));
    }

    if (!ipPlaylist.length) { stopIpPlaylist(); return; }

    const header = group.querySelector('.ip-group-header');
    if (!group.classList.contains('expanded')) toggleIpGroup(header);

    ipPlaylistIdx = 0;
    await playIpPlaylistSession();
  }

  async function playIpPlaylistSession() {
    if (ipPlaylistIdx >= ipPlaylist.length) { stopIpPlaylist(); return; }
    const sid = ipPlaylist[ipPlaylistIdx];
    const row = document.querySelector('.session-row[data-id="' + sid + '"]');
    await selectSession(sid, row);
    play();
    waitForSessionEnd();
  }

  function waitForSessionEnd() {
    const check = setInterval(() => {
      if (!ipPlaylist.length) { clearInterval(check); return; }
      if (!playing && currentT >= getMaxT() - 50) {
        clearInterval(check);
        if (ipPlaylistIdx + 1 < ipPlaylist.length) {
          const nextIdx = ipPlaylistIdx + 1;
          const trans = document.getElementById('sessionTransition');
          document.getElementById('stLabel').textContent = 'Session ' + (nextIdx + 1) + ' of ' + ipPlaylist.length;
          document.getElementById('stSub').textContent = 'Loading next session…';
          trans.classList.add('show');
          setTimeout(async () => {
            trans.classList.remove('show');
            ipPlaylistIdx = nextIdx;
            await playIpPlaylistSession();
          }, 1800);
        } else {
          stopIpPlaylist();
        }
      }
    }, 300);
    window._vsrPlaylistCheck = check;
  }

  function stopIpPlaylist() {
    ipPlaylist = [];
    ipPlaylistIdx = -1;
    if (window._vsrPlaylistCheck) { clearInterval(window._vsrPlaylistCheck); window._vsrPlaylistCheck = null; }
    if (ipPlayBtn) {
      ipPlayBtn.classList.remove('playing');
      ipPlayBtn.title = 'Play all sessions for this IP';
      ipPlayBtn.innerHTML = '<svg width="10" height="10" fill="currentColor" viewBox="0 0 24 24"><polygon points="5 3 19 12 5 21 5 3"/></svg>';
      ipPlayBtn = null;
    }
    pause();
  }

  // ── REPLAY ENGINE ──────────────────────────────────────────────
  let events           = [];
  let session          = null;
  let playing          = false;
  let speed            = 2;
  let currentT         = 0;
  let lastRealTime     = 0;
  let rafId            = null;
  let logCollapsed     = false;
  let lastClickEventIdx = -1;
  let lastAppliedIdx   = -1;
  const cursor     = document.getElementById('cursor');
  const vpFrame    = document.getElementById('vpFrame');
  const vpScaler   = document.getElementById('vpScaler');
  const replayView = document.getElementById('replayView');
  const scrollInd  = document.getElementById('scrollIndicator');

  async function selectSession(id, el) {
    shell.querySelectorAll('.session-row').forEach(r => r.classList.remove('active'));
    if (el) el.classList.add('active');
    stopPlayback();

    try {
      const d = await VSR.get('replay/events/' + id);
      session = d.session;
      events  = d.events || [];

      document.getElementById('replayEmpty').style.display = 'none';
      const ui = document.getElementById('replayUI');
      ui.style.display = 'flex';

      setupViewport();
      buildEventMarkers();
      renderLogAll();
      updateMeta();
      currentT = 0;
      lastClickEventIdx = -1;
      lastAppliedIdx    = -1;
      updateProgress();
      applyCursorToT(0);
    } catch (e) {
      console.error('VSR Replay: selectSession error', e);
    }
  }

  function setupViewport() {
    const vw = parseInt(session.viewport_w) || 1280;
    const vh = parseInt(session.viewport_h) || 800;
    const ph = parseInt(session.page_h)     || vh;

    vpFrame.style.width  = vw + 'px';
    vpFrame.style.height = ph + 'px';

    // Remove previous iframe
    const old = document.getElementById('replayIframe');
    if (old) old.remove();

    const iframe = document.createElement('iframe');
    iframe.id = 'replayIframe';
    const uri = session.page_uri || '/';
    const sep = uri.includes('?') ? '&' : '?';
    iframe.src = uri + sep + 'vs_replay=1';
    iframe.setAttribute('scrolling', 'no');
    iframe.style.cssText = 'position:absolute;top:0;left:0;width:' + vw + 'px;height:' + ph + 'px;border:none;pointer-events:none;z-index:0;background:#fff';

    iframe.addEventListener('error', () => {
      iframe.style.display = 'none';
      const fb = document.createElement('div');
      fb.style.cssText = 'position:absolute;inset:0;display:flex;align-items:center;justify-content:center;color:#888;font-size:13px;z-index:0;background:#f5f5f7';
      fb.textContent = 'Page cannot be embedded — cursor replay only';
      vpFrame.insertBefore(fb, vpFrame.firstChild);
    });

    vpFrame.insertBefore(iframe, vpFrame.firstChild);

    const panelW = replayView.offsetWidth  || 900;
    const panelH = (replayView.offsetHeight || 600) - 160 - 32;
    const scale  = Math.min(panelW / vw, panelH / vh);
    vpScaler.style.transform = 'scale(' + scale + ')';
    vpScaler.style.width  = vw + 'px';
    vpScaler.style.height = ph + 'px';
    replayView.style.overflow = 'hidden';

    const scaledW = vw * scale;
    vpScaler.style.left = Math.max(0, (panelW - scaledW) / 2) + 'px';
    vpScaler.style.top  = '0px';

    cursor.style.display = 'block';
    scrollInd.style.height = Math.max(20, Math.round(vh / ph * 100)) + '%';
    scrollInd.style.top    = '0px';
  }

  function buildEventMarkers() {
    const maxT = getMaxT();
    const wrap = document.getElementById('eventMarkers');
    wrap.innerHTML = '';
    events.forEach((e, i) => {
      if (!['click', 'rage_click', 'scroll', 'input', 'dead_click', 'u_turn', 'iframe_click', 'touch', 'broken_resource', 'mutation'].includes(e.type)) return;
      const pct = maxT > 0 ? (e.t / maxT * 100) : 0;
      const m   = document.createElement('div');
      m.className = 'ev-marker ' + e.type;
      m.style.left = pct + '%';
      if (['click', 'rage_click', 'dead_click', 'u_turn', 'iframe_click', 'touch', 'broken_resource', 'mutation'].includes(e.type)) {
        m.title = e.type.replace('_', ' ') + ' @ ' + fmtMs(e.t);
        m.addEventListener('click', ev => { ev.stopPropagation(); seekToEvent(i); });
      }
      wrap.appendChild(m);
    });
  }

  // Jump to next click event from current time
  function jumpToNextClick() {
    if (!events.length) return;
    for (let i = 0; i < events.length; i++) {
      if ((events[i].type === 'click' || events[i].type === 'rage_click') && events[i].t > currentT + 50) {
        seekToEvent(i);
        return;
      }
    }
    // Wrap around to first click
    for (let i = 0; i < events.length; i++) {
      if (events[i].type === 'click' || events[i].type === 'rage_click') {
        seekToEvent(i);
        return;
      }
    }
  }

  function netStatusClass(e) {
    if (e.type !== 'network') return '';
    try {
      var m = JSON.parse(e.meta || '{}');
      var st = m.status || 0;
      return st >= 200 && st < 300 ? ' net-ok' : (st >= 400 ? ' net-err' : ' net-other');
    } catch (_) { return ''; }
  }

  function renderLogAll() {
    const log = document.getElementById('logBody');
    log.innerHTML = events.map((e, i) => {
      return '<div class="log-row' + netStatusClass(e) + '" id="lr' + i + '" data-i="' + i + '">' +
        '<span class="log-t">' + fmtMs(e.t) + '</span>' +
        '<span class="log-type ' + esc(e.type) + '">' + esc(e.type) + '</span>' +
        '<span class="log-detail">' + esc(eventDetail(e)) + '</span>' +
        '</div>';
    }).join('');
    log.querySelectorAll('.log-row').forEach(row => {
      row.addEventListener('click', () => seekToEvent(+row.dataset.i));
    });
  }

  function eventDetail(e) {
    switch (e.type) {
      case 'click': case 'rage_click':
        return [(e.tag || ''), (e.el_text || ''), e.href ? '→' + e.href : '', '(' + (e.x || 0) + ',' + (e.y || 0) + ')'].join(' ').trim();
      case 'dead_click':
        return '⊘ ' + [(e.tag || ''), (e.el_text || ''), '(' + (e.x || 0) + ',' + (e.y || 0) + ')'].join(' ').trim();
      case 'u_turn':
        try { var m = JSON.parse(e.meta || '{}'); return '↩ ' + (m.from_url || '') + ' → ' + (m.to_url || '') + ' (' + (m.time_on_page_ms || 0) + 'ms)'; } catch (_) { return '↩ u-turn'; }
      case 'scroll':    return 'scrollY: ' + (e.scroll_y || 0) + 'px';
      case 'input':     return (e.tag || '') + ' name="' + (e.input_name || '') + '" type="' + (e.input_type || '') + '"';
      case 'focus': case 'blur': return (e.tag || '') + ' #' + (e.el_id || '') + ' .' + (e.el_class || '');
      case 'navigate':  try { return JSON.parse(e.meta || '{}').url || ''; } catch (_) { return ''; }
      case 'error':     try { return JSON.parse(e.meta || '{}').message || ''; } catch (_) { return ''; }
      case 'resize':    return (e.vp_w || '') + '×' + (e.vp_h || '');
      case 'visibility': try { return JSON.parse(e.meta || '{}').hidden ? 'hidden' : 'visible'; } catch (_) { return ''; }
      case 'network':
        try {
          var m = JSON.parse(e.meta || '{}');
          var st = m.status || 0;
          var tag = (st >= 200 && st < 300) ? '[OK]' : (st >= 400 ? '[ERR]' : '[' + st + ']');
          return tag + ' ' + (m.method || 'GET') + ' ' + (m.url || '') + ' (' + (m.duration_ms || 0) + 'ms)';
        } catch (_) { return 'network request'; }
      case 'custom_tagged':
        try {
          var m = JSON.parse(e.meta || '{}');
          var label = m.event_name || 'custom';
          var d = m.data ? Object.keys(m.data).map(function(k) { return k + '=' + m.data[k]; }).join(', ') : '';
          return esc(label) + (d ? ' {' + esc(d) + '}' : '');
        } catch (_) { return 'custom event'; }
      case 'iframe_click':
        try { var m = JSON.parse(e.meta || '{}'); return '⧈ iframe ' + (m.iframe_selector || '') + ' → ' + (m.tag || '') + ' ' + (m.el_text || ''); } catch (_) { return '⧈ iframe click'; }
      case 'iframe_scroll':
        try { var m = JSON.parse(e.meta || '{}'); return '⧈ iframe ' + (m.iframe_selector || '') + ' scrollY:' + (e.scroll_y || 0); } catch (_) { return '⧈ iframe scroll'; }
      case 'iframe_mousemove':
        return '⧈ iframe cursor';
      case 'iframe_input':
        try { var m = JSON.parse(e.meta || '{}'); return '⧈ iframe ' + (m.iframe_selector || '') + ' ' + (m.tag || '') + ' ' + (m.input_name || ''); } catch (_) { return '⧈ iframe input'; }
      case 'mutation':
        try { var m = JSON.parse(e.meta || '{}'); return '△ DOM ' + (m.count || 0) + ' mutation(s)'; } catch (_) { return '△ DOM mutation'; }
      case 'touch':
        try {
          var m = JSON.parse(e.meta || '{}');
          var g = m.gesture || 'touch';
          var dir = m.direction ? ' ' + m.direction : '';
          var fingers = m.fingers && m.fingers > 1 ? ' (' + m.fingers + ' fingers)' : '';
          return '👆 ' + g + dir + fingers;
        } catch (_) { return '👆 touch'; }
      case 'broken_resource':
        try {
          var m = JSON.parse(e.meta || '{}');
          var rt = m.resource_type || 'resource';
          return '⚠ ' + rt + ': ' + (m.url || '').slice(0, 80);
        } catch (_) { return '⚠ broken resource'; }
      case 'form_submit':
        try {
          var m = JSON.parse(e.meta || '{}');
          var src = m.source === 'server' ? ' [server]' : '';
          var fid = e.el_id ? ' #' + e.el_id : '';
          return '⊕ form' + fid + ' ' + (m.method || 'GET') + src +
                 (m.form_name ? ' "' + m.form_name + '"' : '') +
                 (e.href ? ' → ' + (e.href || '').slice(0, 60) : '');
        } catch (_) { return '⊕ form submit'; }
      default: return '';
    }
  }

  // ── PLAYBACK ──────────────────────────────────────────────────
  function play() {
    playing = true;
    lastRealTime = performance.now() - currentT / speed;
    document.getElementById('playIcon').innerHTML = '<rect x="6" y="4" width="4" height="16"/><rect x="14" y="4" width="4" height="16"/>';
    rafId = requestAnimationFrame(tick);
  }

  function pause() {
    playing = false;
    if (rafId) cancelAnimationFrame(rafId);
    const pi = document.getElementById('playIcon');
    if (pi) pi.innerHTML = '<polygon points="5 3 19 12 5 21 5 3"/>';
  }

  function stopPlayback() {
    pause();
    currentT = 0;
    lastClickEventIdx = -1;
    lastAppliedIdx    = -1;
    if (window._vsrPlaylistCheck) { clearInterval(window._vsrPlaylistCheck); window._vsrPlaylistCheck = null; }
    updateProgress();
  }

  function tick(realNow) {
    if (!playing) return;
    currentT = (realNow - lastRealTime) * speed;
    const maxT = getMaxT();
    if (currentT >= maxT) { currentT = maxT; pause(); }
    applyCursorToT(currentT);
    updateProgress();
    if (playing) rafId = requestAnimationFrame(tick);
  }

  // ── CLICK EFFECT ──────────────────────────────────────────────
  function showClickEffect(x, y, type) {
    const wrap = document.createElement('div');
    wrap.style.cssText = 'position:absolute;left:' + x + 'px;top:' + y + 'px;pointer-events:none;z-index:10000;transform:translate(-50%,-50%)';

    if (type === 'dead_click') {
      // Grey dashed ring for dead clicks
      const ring = document.createElement('div');
      ring.className = 'dead-click-ring';
      ring.style.cssText = 'position:absolute;width:10px;height:10px;transform:translate(-50%,-50%)';
      wrap.appendChild(ring);
      const dot = document.createElement('div');
      dot.style.cssText = 'position:absolute;width:6px;height:6px;border-radius:50%;background:var(--text3);transform:translate(-50%,-50%);opacity:.6';
      wrap.appendChild(dot);
    } else if (type === 'touch') {
      // Touch: finger dot with ripple
      const dot = document.createElement('div');
      dot.className = 'touch-dot';
      dot.style.cssText = 'position:absolute;width:14px;height:14px;border-radius:50%;background:var(--purple);transform:translate(-50%,-50%);opacity:.7;animation:vsrRpClickDotFade 0.6s ease-out forwards';
      wrap.appendChild(dot);
      const ring = document.createElement('div');
      ring.style.cssText = 'position:absolute;width:10px;height:10px;border-radius:50%;border:2px solid var(--purple);transform:translate(-50%,-50%);animation:vsrRpClickRingExpand 0.7s ease-out forwards';
      wrap.appendChild(ring);
    } else if (type === 'iframe_click') {
      // iFrame click: blue ring
      const ring = document.createElement('div');
      ring.style.cssText = 'position:absolute;width:10px;height:10px;border-radius:50%;border:2px solid var(--accent);transform:translate(-50%,-50%);animation:vsrRpClickRingExpand 0.65s ease-out forwards';
      wrap.appendChild(ring);
      const dot = document.createElement('div');
      dot.style.cssText = 'position:absolute;width:8px;height:8px;border-radius:50%;background:var(--accent);transform:translate(-50%,-50%);animation:vsrRpClickDotFade 0.5s ease-out forwards';
      wrap.appendChild(dot);
    } else {
      const isRage = type === 'rage_click';
      const ringCount = isRage ? 3 : 1;
      const color = isRage ? 'var(--red)' : 'var(--click-ring)';
      for (let r = 0; r < ringCount; r++) {
        const ring = document.createElement('div');
        ring.style.cssText = 'position:absolute;width:10px;height:10px;border-radius:50%;border:2px solid ' + color + ';transform:translate(-50%,-50%);animation:vsrRpClickRingExpand 0.65s ease-out ' + (r * 0.13) + 's forwards';
        wrap.appendChild(ring);
      }
      const dot = document.createElement('div');
      dot.style.cssText = 'position:absolute;width:8px;height:8px;border-radius:50%;background:' + color + ';transform:translate(-50%,-50%);animation:vsrRpClickDotFade 0.5s ease-out forwards';
      wrap.appendChild(dot);
    }

    vpFrame.appendChild(wrap);
    setTimeout(() => wrap.remove(), 900);
  }

  function applyCursorToT(t, forceFullScan) {
    const needFullScan = forceFullScan || t < currentT || lastAppliedIdx < 0;
    let cx = null, cy = null, sy = 0, logIdx = -1;

    if (needFullScan) {
      lastAppliedIdx = -1;
      for (let i = 0; i < events.length; i++) {
        const e = events[i];
        if (e.t > t) break;
        logIdx = i; lastAppliedIdx = i;
        if (e.type === 'mousemove' || e.type === 'click' || e.type === 'rage_click' || e.type === 'dead_click' || e.type === 'iframe_click' || e.type === 'iframe_mousemove' || (e.type === 'touch' && e.x != null)) {
          if (e.x != null) cx = +e.x;
          if (e.y != null) cy = +e.y;
        }
        if (e.type === 'scroll' && e.scroll_y != null) sy = +e.scroll_y;
        if ((e.type === 'click' || e.type === 'rage_click' || e.type === 'dead_click' || e.type === 'iframe_click' || e.type === 'touch') && i > lastClickEventIdx && e.x != null && e.y != null) {
          showClickEffect(+e.x, +e.y, e.type);
          lastClickEventIdx = i;
        }
      }
    } else {
      for (let i = 0; i <= lastAppliedIdx && i < events.length; i++) {
        const e = events[i];
        if (e.type === 'mousemove' || e.type === 'click' || e.type === 'rage_click') {
          if (e.x != null) cx = +e.x;
          if (e.y != null) cy = +e.y;
        }
        if (e.type === 'scroll' && e.scroll_y != null) sy = +e.scroll_y;
      }
      logIdx = lastAppliedIdx;
      for (let i = lastAppliedIdx + 1; i < events.length; i++) {
        const e = events[i];
        if (e.t > t) break;
        logIdx = i; lastAppliedIdx = i;
        if (e.type === 'mousemove' || e.type === 'click' || e.type === 'rage_click' || e.type === 'dead_click' || e.type === 'iframe_click' || e.type === 'iframe_mousemove' || (e.type === 'touch' && e.x != null)) {
          if (e.x != null) cx = +e.x;
          if (e.y != null) cy = +e.y;
        }
        if (e.type === 'scroll' && e.scroll_y != null) sy = +e.scroll_y;
        if ((e.type === 'click' || e.type === 'rage_click' || e.type === 'dead_click' || e.type === 'iframe_click' || e.type === 'touch') && i > lastClickEventIdx && e.x != null && e.y != null) {
          showClickEffect(+e.x, +e.y, e.type);
          lastClickEventIdx = i;
        }
      }
    }

    if (cx !== null && cy !== null) { cursor.style.left = cx + 'px'; cursor.style.top = cy + 'px'; }

    const vw     = parseInt(session.viewport_w) || 1280;
    const vh     = parseInt(session.viewport_h) || 800;
    const ph     = parseInt(session.page_h)     || vh;
    const panelW = replayView.offsetWidth  || 900;
    const panelH = (replayView.offsetHeight || 600) - 160 - 32;
    const scale  = Math.min(panelW / vw, panelH / vh);
    vpScaler.style.top = (-sy * scale) + 'px';
    scrollInd.style.top = Math.round(sy / ph * 100) + '%';

    if (logIdx >= 0) {
      shell.querySelectorAll('.log-row').forEach(r => r.classList.remove('current'));
      const row = document.getElementById('lr' + logIdx);
      if (row) { row.classList.add('current'); row.scrollIntoView({ block: 'nearest', behavior: 'smooth' }); }
    }
  }

  function seekToEvent(i) {
    const newT = events[i] ? events[i].t : 0;
    if (newT < currentT) { lastClickEventIdx = -1; lastAppliedIdx = -1; }
    currentT = newT;
    lastAppliedIdx = -1;
    if (playing) lastRealTime = performance.now() - currentT / speed;
    applyCursorToT(currentT, true);
    updateProgress();
  }

  function getMaxT() {
    if (!events.length) return 1;
    return events[events.length - 1].t || (session && session.duration_ms ? parseInt(session.duration_ms) : 1) || 1;
  }

  function updateProgress() {
    const maxT = getMaxT();
    const pct  = maxT > 0 ? Math.min(100, currentT / maxT * 100) : 0;
    const pf = document.getElementById('progressFill');
    const pt = document.getElementById('progressThumb');
    const td = document.getElementById('timeDisplay');
    if (pf) pf.style.width = pct + '%';
    if (pt) pt.style.left  = pct + '%';
    if (td) td.textContent = fmtMsAsTime(currentT) + ' / ' + fmtMs(maxT);
  }

  function updateMeta() {
    const el = document.getElementById('replayMeta');
    if (el && session) el.textContent = (session.viewport_w || '?') + '×' + (session.viewport_h || '?') + ' · ' + (session.click_count || 0) + ' clicks · ' + (session.scroll_count || 0) + ' scrolls';
  }

  // Progress bar seek
  document.getElementById('progressWrap').addEventListener('click', e => {
    const rect = e.currentTarget.getBoundingClientRect();
    const pct  = (e.clientX - rect.left) / rect.width;
    const newT = pct * getMaxT();
    const backward = newT < currentT;
    if (backward) { lastClickEventIdx = -1; lastAppliedIdx = -1; }
    currentT = newT;
    if (playing) lastRealTime = performance.now() - currentT / speed;
    applyCursorToT(currentT, backward);
    updateProgress();
  });

  document.getElementById('playBtn').addEventListener('click', () => {
    if (playing) pause();
    else { if (currentT >= getMaxT()) currentT = 0; play(); }
  });

  document.getElementById('speedSelect').addEventListener('change', e => {
    speed = +e.target.value;
    if (playing) lastRealTime = performance.now() - currentT / speed;
  });

  document.getElementById('nextClickBtn').addEventListener('click', jumpToNextClick);

  document.getElementById('logToggle').addEventListener('click', () => {
    logCollapsed = !logCollapsed;
    document.getElementById('eventLog').classList.toggle('collapsed', logCollapsed);
    document.getElementById('logToggleIcon').textContent = logCollapsed ? '▲' : '▼';
  });

  // Keyboard shortcuts
  document.addEventListener('keydown', e => {
    if (!shell) return;
    if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') return;
    if (e.key === ' ') {
      e.preventDefault();
      document.getElementById('playBtn').click();
    }
    if (e.key === 'ArrowRight') {
      currentT = Math.min(getMaxT(), currentT + 2000);
      if (playing) lastRealTime = performance.now() - currentT / speed;
      applyCursorToT(currentT); updateProgress();
    }
    if (e.key === 'ArrowLeft') {
      currentT = Math.max(0, currentT - 2000);
      lastClickEventIdx = -1; lastAppliedIdx = -1;
      if (playing) lastRealTime = performance.now() - currentT / speed;
      applyCursorToT(currentT, true); updateProgress();
    }
    if (e.key === 'n' && !e.ctrlKey && !e.metaKey) {
      e.preventDefault();
      jumpToNextClick();
    }
    if (e.key === 'f' && !e.ctrlKey && !e.metaKey) {
      document.getElementById('fullscreenBtn').click();
    }
  });

  // ── FULLSCREEN ────────────────────────────────────────────────
  const expandSvg   = '<path d="M8 3H5a2 2 0 0 0-2 2v3m18 0V5a2 2 0 0 0-2-2h-3m0 18h3a2 2 0 0 0 2-2v-3M3 16v3a2 2 0 0 0 2 2h3"/>';
  const collapseSvg = '<path d="M4 14h6v6m10-10h-6V4m0 16h6M4 4h6"/>';

  document.getElementById('fullscreenBtn').addEventListener('click', () => {
    const panel = document.getElementById('replayPanel');
    if (!document.fullscreenElement) {
      panel.requestFullscreen().catch(err => console.warn('Fullscreen failed:', err));
    } else {
      document.exitFullscreen();
    }
  });

  document.addEventListener('fullscreenchange', () => {
    const isFs = !!document.fullscreenElement;
    const fi   = document.getElementById('fullscreenIcon');
    if (fi) fi.innerHTML = isFs ? collapseSvg : expandSvg;
    if (session) setTimeout(setupViewport, 100);
  });

  // ── 19a: EXPORT ────────────────────────────────────────────────
  // Build query string from the current list-panel filters (same params as loadSessions).
  function buildFilterParams() {
    const uri       = (document.getElementById('uriFilter')    || {}).value || '';
    const rangeVal  = (document.getElementById('filterRange')  || {}).value || '';
    const deviceVal = (document.getElementById('filterDevice') || {}).value || '';

    const p = new URLSearchParams();
    if (uri.trim())  p.set('uri', uri.trim());
    if (deviceVal)   p.set('device', deviceVal);

    if (rangeVal) {
      const now = new Date();
      let from;
      if (rangeVal === 'today') {
        from = new Date(now.getFullYear(), now.getMonth(), now.getDate());
      } else {
        const days = parseInt(rangeVal) || 7;
        from = new Date(now.getTime() - days * 86400000);
      }
      const fmt = d => d.getFullYear() + '-' + String(d.getMonth() + 1).padStart(2, '0') + '-' + String(d.getDate()).padStart(2, '0');
      p.set('date_from', fmt(from));
      p.set('date_to',   fmt(now));
    }
    return p;
  }

  function triggerExport(format) {
    const p = buildFilterParams();
    p.set('format', format);
    p.set('_wpnonce', vlensAdmin.nonce);
    // Construct the full REST URL directly (restBase already ends with '/')
    const url = vlensAdmin.restBase + 'replay/export?' + p.toString();
    const a = document.createElement('a');
    a.href = url;
    a.download = '';
    document.body.appendChild(a);
    a.click();
    a.remove();
  }

  // Export dropdown toggle
  const exportWrap = document.getElementById('exportWrap');
  const exportMenu = document.getElementById('exportMenu');
  if (exportWrap && exportMenu) {
    document.getElementById('exportBtn').addEventListener('click', (e) => {
      e.stopPropagation();
      exportMenu.classList.toggle('open');
    });
    document.getElementById('exportCsvBtn').addEventListener('click', () => {
      exportMenu.classList.remove('open');
      triggerExport('csv');
    });
    document.getElementById('exportJsonBtn').addEventListener('click', () => {
      exportMenu.classList.remove('open');
      triggerExport('json');
    });
    // Close menu when clicking outside
    document.addEventListener('click', () => exportMenu.classList.remove('open'));
  }

  // ── 19c: BULK DELETE FILTERED ──────────────────────────────────
  const bulkDelBtn = document.getElementById('bulkDelBtn');
  if (bulkDelBtn) {
    bulkDelBtn.addEventListener('click', async () => {
      const p = buildFilterParams();
      if (!p.toString()) {
        alert('No filters are active. Apply at least one filter (URI, date range, or device) before bulk deleting.');
        return;
      }
      const filterDesc = Array.from(p.entries())
        .map(([k, v]) => k + '=' + v).join(', ');
      if (!confirm('Delete ALL sessions matching the current filters?\n\n' + filterDesc + '\n\nThis cannot be undone.')) return;
      bulkDelBtn.disabled = true;
      bulkDelBtn.textContent = 'Deleting…';
      try {
        const d = await VSR.del('replay/bulk-delete?' + p.toString());
        if (d.ok) {
          alert('Deleted ' + (d.deleted || 0) + ' session(s).');
          loadSessions(1);
        }
      } catch (e) {
        alert('Bulk delete failed. Check browser console for details.');
        console.error(e);
      } finally {
        bulkDelBtn.disabled = false;
        bulkDelBtn.textContent = '🗑 Delete Filtered';
      }
    });
  }

  // ── CLEAR ALL ──────────────────────────────────────────────────
  document.getElementById('clearBtn').addEventListener('click', async () => {
    if (!confirm('Delete ALL recorded sessions? This cannot be undone.')) return;
    const btn = document.getElementById('clearBtn');
    btn.textContent = 'Clearing…'; btn.disabled = true;
    try {
      const d = await VSR.del('replay/clear');
      if (d.ok) {
        document.getElementById('sessionList').innerHTML = '<div style="padding:32px;text-align:center;color:var(--text3)">No sessions found</div>';
        document.getElementById('listCount').textContent = '0 sessions';
        document.getElementById('replayEmpty').style.display = '';
        document.getElementById('replayUI').style.display = 'none';
      }
    } catch (e) {
      alert('Clear failed');
    }
    btn.textContent = '🗑 Clear All'; btn.disabled = false;
  });

  // ── UTILS ──────────────────────────────────────────────────────
  function fmtMs(ms) {
    const s = Math.floor(ms / 1000), m = Math.floor(s / 60);
    return m + ':' + (s % 60).toString().padStart(2, '0');
  }

  function fmtMsAsTime(ms) {
    if (session && session.started_at) {
      const wallMs = new Date(session.started_at).getTime() + ms;
      const d = new Date(wallMs);
      return d.getHours().toString().padStart(2, '0') + ':' +
             d.getMinutes().toString().padStart(2, '0') + ':' +
             d.getSeconds().toString().padStart(2, '0');
    }
    return fmtMs(ms);
  }

  function esc(s) {
    const d = document.createElement('div'); d.textContent = s || ''; return d.innerHTML;
  }

  function deviceType(w) {
    w = parseInt(w) || 0;
    if (w > 0 && w < 768) return 'mobile';
    if (w >= 768 && w < 1024) return 'tablet';
    return 'desktop';
  }

  function deviceIcon(w) {
    const t = deviceType(w);
    if (t === 'mobile') return '<svg class="sr-device-icon" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="7" y="2" width="10" height="20" rx="2"/><line x1="12" y1="18" x2="12" y2="18.01"/></svg>';
    if (t === 'tablet') return '<svg class="sr-device-icon" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="4" y="2" width="16" height="20" rx="2"/><line x1="12" y1="18" x2="12" y2="18.01"/></svg>';
    return '<svg class="sr-device-icon" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="2" y="3" width="20" height="14" rx="2"/><line x1="8" y1="21" x2="16" y2="21"/><line x1="12" y1="17" x2="12" y2="21"/></svg>';
  }

  function maskIp(ip) {
    if (!ip || ip === 'Unknown') return 'Unknown';
    if (ip.includes(':')) { const p = ip.split(':'); return '***:' + p.slice(-4).join(':'); }
    return ip;
  }

  // ── EXPOSE FOR INLINE ONCLICK ──────────────────────────────────
  window.vsrReplay = { selectSession, toggleIpGroup, deleteIpGroup, playIpGroup };

  // ── INIT ───────────────────────────────────────────────────────
  (async function init() {
    await loadSessions();

    // Refresh online dots every 30s
    setInterval(async () => {
      try {
        const d = await VSR.get('replay/sessions?page=' + currentPage);
        const onlineIps = d.online_ips || [];
        shell.querySelectorAll('.ip-group').forEach(group => {
          const ip = group.dataset.ip;
          const header = group.querySelector('.ip-group-header');
          const existingDot = header.querySelector('.online-dot-ip');
          const isOnline = onlineIps.includes(ip);
          if (isOnline && !existingDot) {
            const dot = document.createElement('span');
            dot.className = 'online-dot-ip';
            dot.title = 'Online now';
            const playBtn2 = header.querySelector('.ip-group-play');
            if (playBtn2) header.insertBefore(dot, playBtn2);
            else header.appendChild(dot);
          } else if (!isOnline && existingDot) {
            existingDot.remove();
          }
        });
      } catch (e) {}
    }, 30000);

    // Auto-select session from URL ?sid=
    const autoSid = new URLSearchParams(window.location.search).get('sid');
    if (!autoSid) return;
    const sid = parseInt(autoSid, 10);
    if (sid <= 0) return;

    const row = document.querySelector('.session-row[data-id="' + sid + '"]');
    if (!row) { selectSession(sid, null); return; }

    const ipGroup = row.closest('.ip-group');
    if (ipGroup) {
      const body  = ipGroup.querySelector('.ip-group-body');
      const arrow = ipGroup.querySelector('.ip-group-arrow');
      body.style.display = 'block';
      arrow.style.transform = 'rotate(90deg)';
      ipGroup.classList.add('expanded', 'highlight');
    }

    selectSession(sid, row);
    setTimeout(() => row.scrollIntoView({ block: 'center', behavior: 'smooth' }), 200);
  })();

})();
