const { fmt, $ } = Admin;

const state = { cursor: 0, records: [], dropped: 0, paused: false, fetching: false };
const ranks = { DEBUG: 10, INFO: 20, WARN: 30, ERROR: 40 };

// The same order the server's own console lines use — who and where first, the reason for
// the line last — so a log read here and a log read over SSH look alike. `version` is
// dropped: it is the same on every line and is reported once in the rail instead.
const fieldOrder = [
  'component', 'user', 'device', 'client', 'protocol',
  'method', 'path', 'status', 'duration',
];

function orderedFields(attributes) {
  const rank = (key) => {
    const at = fieldOrder.indexOf(key);
    if (at >= 0) return at;
    return key === 'error' ? 1000 : 100;
  };
  return Object.entries(attributes)
    .filter(([key]) => key !== 'version')
    .sort((a, b) => rank(a[0]) - rank(b[0]));
}

const text = (event) =>
  [event.message, ...Object.entries(event.attributes || {}).flat()].join(' ').toLowerCase();

function render() {
  const minimum = ranks[$('log-level').value] || 20;
  const search = $('log-search').value.trim().toLowerCase();
  const filtered = state.records.filter((event) =>
    (ranks[event.level] || 0) >= minimum && (!search || text(event).includes(search)));

  // Rendering every retained record at once can freeze a browser during an incident. Keep
  // all of them available for filtering and export, but draw only the visible tail.
  const visible = filtered.slice(-2500);
  const log = $('log');
  const pinned = log.scrollHeight - log.scrollTop - log.clientHeight < 50;
  log.innerHTML = visible.length ? visible.map((event) => {
    const attrs = orderedFields(event.attributes || {})
      .map(([key, value]) => fmt.escape(key) + '=' + fmt.escape(value)).join(' ');
    // The level is a class on the line as well as on its own column: scrolling a log is
    // looking for the one line that is not INFO, and a coloured word four columns in is
    // easy to scroll past.
    return '<div class="log-line ' + fmt.escape(event.level) + '">' +
      '<span class="log-time">' + fmt.escape(fmt.when(event.occurredAt)) + '</span>' +
      '<span class="log-level ' + fmt.escape(event.level) + '">' + fmt.escape(event.level) + '</span>' +
      '<span>' + fmt.escape(event.message) + '</span>' +
      '<span class="log-attrs">' + attrs + '</span></div>';
  }).join('') : '<div class="log-empty">No events match this filter.</div>';
  if (pinned) log.scrollTop = log.scrollHeight;

  $('log-stats').textContent =
    fmt.number(state.records.length) + ' retained · ' + fmt.number(filtered.length) + ' matching' +
    (visible.length < filtered.length ? ' · showing the latest ' + fmt.number(visible.length) : '') +
    (state.dropped ? ' · ' + fmt.number(state.dropped) + ' overwritten before delivery' : '');
}

// The ring buffer is drained in pages until it is caught up, so a console opened after an
// incident sees what happened rather than only what happens next.
Admin.onRefresh(async () => {
  if (state.paused || state.fetching) return;
  state.fetching = true;
  try {
    let pages = 0;
    let page;
    do {
      page = await Admin.api('/admin/api/events?after=' + state.cursor + '&limit=1000');
      state.cursor = page.next || state.cursor;
      state.dropped += page.dropped || 0;
      if ((page.events || []).length) {
        state.records.push(...page.events);
        if (state.records.length > 20000) {
          state.records.splice(0, state.records.length - 20000);
        }
      }
      pages += 1;
    } while (page.hasMore && pages < 20 && !state.paused);
    render();
  } finally {
    state.fetching = false;
  }
});

Admin.ready(() => {
  $('log-pause').addEventListener('click', () => {
    state.paused = !state.paused;
    $('log-pause').textContent = state.paused ? 'Resume' : 'Pause';
    if (!state.paused) Admin.refresh();
  });
  $('log-clear').addEventListener('click', () => {
    state.records = [];
    state.dropped = 0;
    render();
  });
  $('log-export').addEventListener('click', () => {
    const blob = new Blob([JSON.stringify(state.records, null, 2)], { type: 'application/json' });
    const link = document.createElement('a');
    link.href = URL.createObjectURL(blob);
    link.download = 'memby-events-' + new Date().toISOString().replace(/[:.]/g, '-') + '.json';
    link.click();
    setTimeout(() => URL.revokeObjectURL(link.href), 1000);
  });
  $('log-level').addEventListener('change', render);
  $('log-search').addEventListener('input', render);
});
