/**
 * event_log_panel.js — Saltybot Event Log Panel (Issue #576)
 *
 * Subscribes via rosbridge WebSocket to:
 *   /rosout               rcl_interfaces/msg/Log   — ROS2 node messages
 *   /saltybot/events      std_msgs/String (JSON)   — custom robot events
 *
 * Features:
 *   - 1000-entry ring buffer (oldest dropped when full)
 *   - Filter by severity (DEBUG/INFO/WARN/ERROR/FATAL + EVENTS)
 *   - Filter by node name (select from seen nodes)
 *   - Text search with highlight
 *   - Auto-scroll (pauses when user scrolls up, resumes at bottom)
 *   - Pause on hover (stops auto-scroll, doesn't drop messages)
 *   - CSV export of current filtered view
 *   - Clear all / clear filtered
 */

'use strict';

// ── Constants ─────────────────────────────────────────────────────────────────

const RING_CAPACITY = 1000;

// ROS2 /rosout level byte values
const ROS_LEVELS = {
  10: 'DEBUG',
  20: 'INFO',
  30: 'WARN',
  40: 'ERROR',
  50: 'FATAL',
};

const SEV_LABEL = {
  DEBUG: 'DEBUG',
  INFO:  'INFO ',
  WARN:  'WARN ',
  ERROR: 'ERROR',
  FATAL: 'FATAL',
  EVENT: 'EVENT',
};

// ── State ─────────────────────────────────────────────────────────────────────

let ros       = null;
let rosoutSub = null;
let eventsSub = null;

// Ring buffer — array of entry objects
const ring = [];
let   nextId    = 0;    // monotonic entry ID

// Seen node names for filter dropdown
const seenNodes = new Set();

// Filter state
const filters = {
  levels: new Set(['DEBUG','INFO','WARN','ERROR','FATAL','EVENT']),
  node:   '',      // '' = all
  search: '',
};

// Auto-scroll + hover-pause
let autoScroll   = true;
let hoverPaused  = false;
let userScrolled = false;   // user scrolled away manually

// Pending DOM rows to flush (batched for perf)
let pendingFlush = false;

// ── DOM refs ──────────────────────────────────────────────────────────────────

const feed       = document.getElementById('log-feed');
const emptyState = document.getElementById('empty-state');
const countBadge = document.getElementById('count-badge');
const pausedInd  = document.getElementById('paused-indicator');
const searchEl   = document.getElementById('search-input');
const nodeFilter = document.getElementById('node-filter');

// ── Utility ───────────────────────────────────────────────────────────────────

function tsNow() {
  return new Date().toLocaleTimeString('en-US', {
    hour12: false, hour: '2-digit', minute: '2-digit', second: '2-digit',
  }) + '.' + String(Date.now() % 1000).padStart(3,'0');
}

function tsFromRos(stamp) {
  if (!stamp) return tsNow();
  const ms = stamp.sec * 1000 + Math.floor((stamp.nanosec ?? 0) / 1e6);
  const d  = new Date(ms);
  return d.toLocaleTimeString('en-US', {
    hour12: false, hour: '2-digit', minute: '2-digit', second: '2-digit',
  }) + '.' + String(d.getMilliseconds()).padStart(3,'0');
}

function escapeHtml(s) {
  return s.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
}

function highlightSearch(text, term) {
  if (!term) return escapeHtml(text);
  const escaped = term.replace(/[.*+?^${}()|[\]\\]/g,'\\$&');
  const re = new RegExp(`(${escaped})`, 'gi');
  return escapeHtml(text).replace(re, '<mark>$1</mark>');
}

// ── Ring buffer ───────────────────────────────────────────────────────────────

function pushEntry(entry) {
  entry.id = nextId++;
  ring.push(entry);
  if (ring.length > RING_CAPACITY) ring.shift();

  if (!seenNodes.has(entry.node) && entry.node) {
    seenNodes.add(entry.node);
    rebuildNodeFilter();
  }
}

// ── ROS connection ────────────────────────────────────────────────────────────

function connect() {
  const url = document.getElementById('ws-input').value.trim();
  if (!url) return;
  if (ros) ros.close();

  ros = new ROSLIB.Ros({ url });

  ros.on('connection', () => {
    document.getElementById('conn-dot').className = 'connected';
    document.getElementById('conn-label').textContent = url;
    document.getElementById('btn-connect').textContent = 'RECONNECT';
    setupSubs();
    addSystemEntry('Connected to ' + url, 'INFO');
  });

  ros.on('error', (err) => {
    document.getElementById('conn-dot').className = 'error';
    document.getElementById('conn-label').textContent = 'ERROR: ' + (err?.message || err);
    teardown();
    addSystemEntry('Connection error: ' + (err?.message || err), 'ERROR');
  });

  ros.on('close', () => {
    document.getElementById('conn-dot').className = '';
    document.getElementById('conn-label').textContent = 'Disconnected';
    teardown();
    addSystemEntry('Disconnected', 'WARN');
  });
}

function setupSubs() {
  // /rosout — ROS2 log messages
  rosoutSub = new ROSLIB.Topic({
    ros, name: '/rosout',
    messageType: 'rcl_interfaces/msg/Log',
  });
  rosoutSub.subscribe((msg) => {
    const level = ROS_LEVELS[msg.level] ?? 'INFO';
    pushEntry({
      ts:     tsFromRos(msg.stamp),
      level,
      node:   (msg.name  || 'unknown').replace(/^\//, ''),
      msg:    msg.msg  || '',
      source: 'rosout',
    });
    scheduleRender();
  });

  // /saltybot/events — custom events (JSON string: {level, node, msg})
  eventsSub = new ROSLIB.Topic({
    ros, name: '/saltybot/events',
    messageType: 'std_msgs/String',
  });
  eventsSub.subscribe((msg) => {
    let level = 'EVENT', node = 'saltybot', text = msg.data;
    try {
      const parsed = JSON.parse(msg.data);
      level = (parsed.level || 'EVENT').toUpperCase();
      node  = parsed.node  || 'saltybot';
      text  = parsed.msg   || parsed.message || msg.data;
    } catch (_) { /* raw string */ }

    pushEntry({ ts: tsNow(), level, node, msg: text, source: 'events' });
    scheduleRender();
  });
}

function teardown() {
  if (rosoutSub) { rosoutSub.unsubscribe(); rosoutSub = null; }
  if (eventsSub) { eventsSub.unsubscribe(); eventsSub = null; }
}

function addSystemEntry(text, level = 'INFO') {
  pushEntry({ ts: tsNow(), level, node: '[system]', msg: text, source: 'system' });
  scheduleRender();
}

// ── Rendering ─────────────────────────────────────────────────────────────────

function scheduleRender() {
  if (pendingFlush) return;
  pendingFlush = true;
  requestAnimationFrame(renderAll);
}

function entryVisible(e) {
  if (!filters.levels.has(e.level)) return false;
  if (filters.node && e.node !== filters.node) return false;
  if (filters.search) {
    const q = filters.search.toLowerCase();
    if (!e.msg.toLowerCase().includes(q) &&
        !e.node.toLowerCase().includes(q)) return false;
  }
  return true;
}

function buildRow(e) {
  const cls  = 'sev-' + e.level.toLowerCase();
  const hl   = highlightSearch(e.msg, filters.search);
  return `<div class="log-entry ${cls}" data-id="${e.id}">` +
    `<span class="log-ts">${e.ts}</span>` +
    `<span class="log-sev">${SEV_LABEL[e.level] ?? e.level}</span>` +
    `<span class="log-node" title="${escapeHtml(e.node)}">${escapeHtml(e.node)}</span>` +
    `<span class="log-msg">${hl}</span>` +
    `</div>`;
}

function renderAll() {
  pendingFlush = false;

  const visible = ring.filter(entryVisible);

  if (visible.length === 0) {
    feed.innerHTML = '';
    emptyState.style.display = 'flex';
    countBadge.textContent = `0 / ${ring.length}`;
    return;
  }

  emptyState.style.display = 'none';
  feed.innerHTML = visible.map(buildRow).join('');
  countBadge.textContent = `${visible.length} / ${ring.length}`;

  maybeScrollBottom();
}

function maybeScrollBottom() {
  if ((autoScroll && !hoverPaused && !userScrolled)) {
    feed.scrollTop = feed.scrollHeight;
  }
}

// ── Auto-scroll + pause logic ─────────────────────────────────────────────────

feed.addEventListener('mouseenter', () => {
  hoverPaused = true;
  pausedInd.classList.add('visible');
});
feed.addEventListener('mouseleave', () => {
  hoverPaused = false;
  pausedInd.classList.remove('visible');
  if (autoScroll && !userScrolled) feed.scrollTop = feed.scrollHeight;
});

feed.addEventListener('scroll', () => {
  const atBottom = feed.scrollHeight - feed.scrollTop - feed.clientHeight < 40;
  if (atBottom) {
    userScrolled = false;
  } else if (!hoverPaused) {
    userScrolled = true;
  }
});

// Pause/resume button
document.getElementById('btn-pause').addEventListener('click', () => {
  autoScroll = !autoScroll;
  userScrolled = false;
  const btn = document.getElementById('btn-pause');
  if (autoScroll) {
    btn.textContent = '⏸ PAUSE';
    btn.classList.remove('pause');
    btn.classList.add('active');
    feed.scrollTop = feed.scrollHeight;
  } else {
    btn.textContent = '▶ RESUME';
    btn.classList.add('pause');
    btn.classList.remove('active');
  }
});

// ── Filter controls ───────────────────────────────────────────────────────────

document.querySelectorAll('.sev-btn').forEach((btn) => {
  btn.addEventListener('click', () => {
    const lv = btn.dataset.level;
    if (filters.levels.has(lv)) {
      filters.levels.delete(lv);
      btn.classList.add('off');
    } else {
      filters.levels.add(lv);
      btn.classList.remove('off');
    }
    scheduleRender();
  });
});

searchEl.addEventListener('input', () => {
  filters.search = searchEl.value.trim();
  scheduleRender();
});

nodeFilter.addEventListener('change', () => {
  filters.node = nodeFilter.value;
  scheduleRender();
});

function rebuildNodeFilter() {
  const current = nodeFilter.value;
  const nodes   = [...seenNodes].sort();
  nodeFilter.innerHTML = '<option value="">All nodes</option>' +
    nodes.map(n => `<option value="${escapeHtml(n)}"${n===current?' selected':''}>${escapeHtml(n)}</option>`).join('');
}

// ── Clear ─────────────────────────────────────────────────────────────────────

document.getElementById('btn-clear').addEventListener('click', () => {
  ring.length = 0;
  seenNodes.clear();
  rebuildNodeFilter();
  scheduleRender();
});

// ── CSV export ────────────────────────────────────────────────────────────────

document.getElementById('btn-export').addEventListener('click', () => {
  const visible = ring.filter(entryVisible);
  if (visible.length === 0) return;

  const header = 'timestamp,level,node,message\n';
  const rows = visible.map((e) =>
    [e.ts, e.level, e.node, '"' + e.msg.replace(/"/g,'""') + '"'].join(',')
  );
  const csv  = header + rows.join('\n');
  const blob = new Blob([csv], { type: 'text/csv' });
  const url  = URL.createObjectURL(blob);
  const a    = document.createElement('a');
  a.href     = url;
  a.download = `saltybot_log_${new Date().toISOString().slice(0,19).replace(/:/g,'-')}.csv`;
  a.click();
  URL.revokeObjectURL(url);
});

// ── Connect button ────────────────────────────────────────────────────────────

document.getElementById('btn-connect').addEventListener('click', connect);
document.getElementById('ws-input').addEventListener('keydown', (e) => {
  if (e.key === 'Enter') connect();
});

// ── Init ──────────────────────────────────────────────────────────────────────

const stored = localStorage.getItem('evlog_ws_url');
if (stored) document.getElementById('ws-input').value = stored;
document.getElementById('ws-input').addEventListener('change', (e) => {
  localStorage.setItem('evlog_ws_url', e.target.value);
});

// Keyboard shortcut: Ctrl+F focuses search
document.addEventListener('keydown', (e) => {
  if ((e.ctrlKey || e.metaKey) && e.key === 'f') {
    e.preventDefault();
    searchEl.focus();
    searchEl.select();
  }
  // Escape: clear search
  if (e.key === 'Escape' && document.activeElement === searchEl) {
    searchEl.value = '';
    filters.search = '';
    scheduleRender();
  }
});

// Initial empty state
scheduleRender();
