/* One person's settings, over time. The identity is in the path — /admin/accounts/<id>/
   settings — so the page can be linked and returned to after a sign-in, like every other
   page here.

   The two questions this answers are different and both need the whole page. "What did I
   change and can I undo it" is the table; "why is that television still wrong" is the
   device list above it, because a revision the server wrote is not a revision a set has
   taken. */

const { fmt, ui, $ } = Admin;
const segments = location.pathname.split('/').filter(Boolean);
// .../accounts/<id>/settings — the id is the segment before the page name.
const userId = decodeURIComponent(segments[segments.length - 2] || '');
const base = '/admin/api/accounts/' + encodeURIComponent(userId);

// Which revisions have their full document open. Kept out of the DOM so the thirty-second
// poll can redraw the table without closing what the operator opened.
const expanded = new Set();
let catalogue = [];
let latest = null;

function message(text) { $('history-message').textContent = text || ''; }

/* ---- where each television has got to ---------------------------------- */

function renderDevices(payload) {
  const devices = payload.devices || [];
  const current = payload.currentRevision || 0;

  $('history-current').innerHTML = payload.saved
    ? ui.tag('now on r' + fmt.number(current) + ' · ' + (payload.currentSource || 'device'),
      payload.currentSource === 'admin' ? 'warn' : 'ok')
    : ui.tag('defaults · never synced', 'idle');

  $('history-devices').innerHTML = devices.length ? devices.map((device) => {
    const tone = device.never ? 'idle' : device.behind ? 'bad' : 'ok';
    const state = device.never
      ? ui.tag('never taken one', 'idle')
      : device.behind
        ? ui.tag(fmt.number(device.behind) + ' behind', 'bad')
        : ui.tag('up to date', 'ok');
    const held = device.never
      ? 'Has not fetched these settings yet'
      : 'Holding r' + fmt.number(device.revision) + ' · taken ' + fmt.when(device.ackedAt);
    return '<div class="list-row"><span class="list-main"><span>' +
      '<span class="list-title">' +
        '<span class="dot" data-tone="' + tone + '"></span>' +
        fmt.escape(device.name || 'Memby TV') +
        (device.signedOut ? ' ' + ui.tag('signed out', 'idle') : '') +
      '</span>' +
      '<span class="list-meta">' + fmt.escape(held) +
        (device.clientVersion ? ' · Memby ' + fmt.escape(device.clientVersion) : '') +
        (device.signedOut ? '' : ' · last seen ' + fmt.escape(fmt.when(device.lastSeen))) +
      '</span></span></span>' +
      '<span class="list-actions">' + state + '</span></div>';
  }).join('') : ui.empty('No television has been signed in to this account.');
}

/* ---- the history table -------------------------------------------------- */

function changeChips(revision) {
  if (revision.initial) return '<span class="muted">First recorded settings</span>';
  const changes = revision.changes || [];
  if (!changes.length) {
    // A write that changed nothing an operator can see: a television pushing the document
    // it already held, usually. Saying so is more useful than an empty cell.
    return '<span class="muted">No visible change</span>';
  }
  return '<div class="chips">' + changes.map((change) =>
    ui.chip(change.name + ': ' + change.before + ' → ' + change.after)).join('') + '</div>';
}

function takenBy(revision) {
  const acks = revision.acks || [];
  if (!acks.length) return '<span class="muted">—</span>';
  const names = acks.map((ack) => ack.deviceName || ack.deviceId).join(', ');
  return '<span title="' + fmt.escape(names) + '">' + fmt.number(acks.length) + '</span>';
}

// The whole document at one revision, in the catalogue's own order and wording. This is
// what makes a restore a decision rather than a guess.
function documentRow(revision) {
  const values = revision.preferences || {};
  const chips = catalogue.map((definition) =>
    ui.chip(definition.name + ': ' + describe(definition, values[definition.key])));
  return '<tr class="detail"><td colspan="6" class="muted"><div class="chips">' +
    chips.join('') + '</div></td></tr>';
}

// The same wording the server puts in a change line, so a document and a diff never
// describe one value two ways.
function describe(definition, value) {
  if (definition.kind === 'toggle') return value ? 'On' : 'Off';
  if (definition.kind === 'choice') {
    const match = (definition.options || []).find((option) => option.value === value);
    return match ? match.label : String(value ?? '');
  }
  if (definition.kind === 'number') {
    if (Number(value) === 0 && definition.unit) return 'No limit';
    return definition.unit ? value + ' ' + definition.unit : String(value ?? '');
  }
  const entries = Array.isArray(value) ? value : [];
  if (!entries.length) return 'None';
  return entries.map((entry) => {
    const match = (definition.options || []).find((option) => option.value === entry);
    return match ? match.label : entry;
  }).join(', ');
}

function renderHistory(payload) {
  const revisions = payload.revisions || [];
  $('history-rows').innerHTML = revisions.length ? revisions.map((revision) => {
    const open = expanded.has(revision.revision);
    const source = revision.source === 'admin' ? 'warn' : 'ok';
    const row = '<tr>' +
      '<td>' + fmt.escape(fmt.when(revision.createdAt)) + '</td>' +
      '<td class="num">r' + fmt.number(revision.revision) +
        (revision.current ? ' ' + ui.tag('current', 'ok') : '') + '</td>' +
      '<td>' + ui.tag(revision.author, source) +
        (revision.restoredFrom
          ? ' <span class="muted">restored r' + fmt.number(revision.restoredFrom) + '</span>'
          : '') + '</td>' +
      '<td class="muted">' + changeChips(revision) + '</td>' +
      '<td class="num">' + takenBy(revision) + '</td>' +
      '<td class="num"><span class="list-actions">' +
        '<button class="small" data-history-action="toggle" data-revision="' +
          revision.revision + '">' + (open ? 'Hide' : 'Show') + '</button>' +
        (revision.current ? ''
          : '<button class="small" data-history-action="restore" data-revision="' +
            revision.revision + '">Restore</button>') +
      '</span></td></tr>';
    return open ? row + documentRow(revision) : row;
  }).join('') : ui.emptyRow(6, 'Nothing has been changed on this account yet.');
}

/* ---- page --------------------------------------------------------------- */

Admin.onRefresh(async () => {
  const payload = await Admin.api(base + '/preferences/history');
  latest = payload;
  catalogue = payload.catalogue || catalogue;
  const name = payload.username || 'this account';
  $('page-title').textContent = 'Settings history';
  $('page-intro').textContent = 'Every change to ' + name + '’s synced settings, and which ' +
    'of their televisions has taken it.';
  document.title = name + ' · settings history · Memby admin';
  $('history-crumb').href = '/admin/accounts/' + encodeURIComponent(userId);
  $('history-crumb').textContent = '← ' + name;
  renderDevices(payload);
  renderHistory(payload);
});

document.addEventListener('click', (event) => {
  const button = event.target.closest('[data-history-action]');
  if (!button) return;
  const revision = Number(button.dataset.revision);

  if (button.dataset.historyAction === 'toggle') {
    if (expanded.has(revision)) expanded.delete(revision); else expanded.add(revision);
    if (latest) renderHistory(latest);
    return;
  }

  if (button.dataset.historyAction === 'restore') {
    if (!confirm('Restore revision ' + revision + '? It goes out as a new change, so every ' +
      'one of their televisions will pick it up, and the current version stays in this ' +
      'history to return to.')) return;
    message('restoring…');
    Admin.act(async () => {
      await Admin.api(base + '/preferences/revisions/' + revision + '/restore',
        { method: 'POST' });
      message('restored r' + revision);
    });
  }
});
