/* One person: their televisions, their recommendation setup and the settings that follow
   them to every set. The identity is in the URL rather than in a query string so the page
   can be linked, bookmarked and returned to after a sign-in. */

const { fmt, ui, $ } = Admin;
const userId = decodeURIComponent(location.pathname.split('/').filter(Boolean).pop() || '');
const base = '/admin/api/accounts/' + encodeURIComponent(userId);

// The accounts endpoint answers for the whole household in one read and carries the
// preference catalogue with it, so this page asks for that rather than adding a per-user
// endpoint that would return a slice of the same query.
let catalogue = [];

// The selectable themes, likewise carried by that endpoint rather than written out here.
let themeCatalogue = [];

// The same dirty rule the settings form follows, kept separate so saving one does not
// discard an unsaved edit to the other.
let themesDirty = false;

// True while the operator has edited the settings form without saving. The page polls every
// thirty seconds and a redraw would take a half-finished change away mid-sentence, so a
// dirty form keeps the DOM it already has until it is saved, discarded or reloaded.
let dirty = false;

/* ---- settings controls ------------------------------------------------- */

function settingControl(definition, value) {
  const key = fmt.escape(definition.key);
  const name = fmt.escape(definition.name);
  const description = fmt.escape(definition.description);

  if (definition.kind === 'toggle') {
    return '<label class="check"><input type="checkbox" data-pref-key="' + key +
      '" data-pref-kind="toggle"' + (value ? ' checked' : '') + '><span>' + name +
      '<em>' + description + '</em></span></label>';
  }

  if (definition.kind === 'choice' || definition.kind === 'number') {
    // A number's unit comes from the catalogue. Assuming minutes was safe while the only
    // number was a time budget, and wrong the moment a second one counted anything else.
    const unit = definition.unit || '';
    const options = definition.kind === 'number'
      ? (definition.numbers || []).map((amount) =>
        [String(amount), amount === 0 ? 'No limit' : (unit ? amount + ' ' + unit : String(amount))])
      : (definition.options || []).map((option) => [option.value, option.label]);
    return '<label class="field"><span>' + name + '</span><em>' + description + '</em>' +
      '<select data-pref-key="' + key + '" data-pref-kind="' + fmt.escape(definition.kind) + '">' +
      options.map(([optionValue, optionLabel]) => '<option value="' + fmt.escape(optionValue) + '"' +
        (String(value) === optionValue ? ' selected' : '') + '>' +
        fmt.escape(String(optionLabel)) + '</option>').join('') + '</select></label>';
  }

  if (definition.kind === 'multi') {
    // Rendered in the viewer's own order, then whatever they have not selected. The order
    // of these rows is the order the launcher draws them in, so preserving it matters as
    // much as which are ticked.
    const selected = Array.isArray(value) ? value : [];
    const ordered = selected.concat((definition.options || [])
      .map((option) => option.value).filter((option) => !selected.includes(option)));
    return '<div class="field"><span>' + name + '</span><em>' + description + '</em>' +
      '<div class="checks" data-pref-key="' + key + '" data-pref-kind="multi">' +
      ordered.map((option) => {
        const match = (definition.options || []).find((entry) => entry.value === option);
        if (!match) return '';
        return '<label class="check"><input type="checkbox" data-pref-option="' +
          fmt.escape(option) + '"' + (selected.includes(option) ? ' checked' : '') + '><span>' +
          fmt.escape(match.label) + '</span></label>';
      }).join('') + '</div></div>';
  }

  // A free-form list of server row ids: one per line, which is also how the television
  // stores them. There is no vocabulary to offer, because these ids ship from the gateway
  // without an app release.
  const entries = Array.isArray(value) ? value : [];
  return '<label class="field"><span>' + name + '</span><em>' + description + '</em>' +
    '<textarea data-pref-key="' + key + '" data-pref-kind="list" spellcheck="false" ' +
    'placeholder="One row id per line">' + fmt.escape(entries.join('\n')) + '</textarea></label>';
}

// Reads the form back into the document shape the server normalises. Unknown keys and
// illegal values are the server's problem by design — this only has to be honest about what
// the operator selected.
function collectSettings() {
  const preferences = {};
  $('account-settings').querySelectorAll('[data-pref-key]').forEach((control) => {
    const key = control.dataset.prefKey;
    switch (control.dataset.prefKind) {
      case 'toggle': preferences[key] = control.checked; break;
      case 'number': preferences[key] = Number(control.value); break;
      case 'multi':
        preferences[key] = Array.from(control.querySelectorAll('input:checked'))
          .map((input) => input.dataset.prefOption);
        break;
      case 'list':
        preferences[key] = control.value.split('\n').map((entry) => entry.trim()).filter(Boolean);
        break;
      default: preferences[key] = control.value;
    }
  });
  return preferences;
}

function renderSettings(account) {
  const settings = account.settings || {};
  const values = settings.preferences || {};
  const areas = [];
  catalogue.forEach((definition) => {
    let area = areas.find((entry) => entry.name === definition.area);
    if (!area) areas.push(area = { name: definition.area, definitions: [] });
    area.definitions.push(definition);
  });

  $('account-settings-state').innerHTML = settings.saved
    ? ui.tag('r' + fmt.number(settings.revision || 0) + ' · ' + (settings.source || 'device') +
      ' · ' + fmt.when(settings.updatedAt), settings.source === 'admin' ? 'warn' : 'ok')
    : ui.tag('defaults · never synced', 'idle');

  $('account-settings').innerHTML = areas.map((area) =>
    '<div class="field"><p class="card-sub">' + fmt.escape(area.name) + '</p>' +
    '<div class="checks">' + area.definitions.map((definition) =>
      settingControl(definition, values[definition.key])).join('') + '</div></div>').join('');
}

/* ---- colour schemes ------------------------------------------------------ */

// The palette is written the way Android reads it, #AARRGGBB, and CSS reads #RRGGBBAA. The
// conversion lives here rather than on the wire because the television is the end that has
// to parse thousands of these and the console is the end that parses eight.
function cssColour(value) {
  const hex = String(value || '').replace('#', '');
  if (hex.length !== 8) return '#' + hex;
  return '#' + hex.slice(2) + hex.slice(0, 2);
}

function themeRow(theme, allowed) {
  const palette = theme.palette || {};
  // Only the palette itself is set inline; see the .swatch note in admin.css.
  const style = '--swatch-surface:' + cssColour(palette.surface) + ';' +
    '--swatch-accent:' + cssColour(palette.accent) + ';' +
    '--swatch-hairline:' + cssColour(palette.hairline);
  return '<label class="check"><input type="checkbox" data-theme-id="' + fmt.escape(theme.id) +
    '"' + (allowed ? ' checked' : '') + '>' +
    '<span class="swatch" style="' + fmt.escape(style) + '"><i></i></span>' +
    '<span>' + fmt.escape(theme.name) + '<em>' + fmt.escape(theme.description) +
    '</em></span></label>';
}

function renderThemes(account) {
  // An empty list from the server means unrestricted, so it draws as every box ticked.
  // Storing "all" and "never configured" identically is deliberate — they are the same
  // decision — and this is the one place an operator would notice if it were not.
  const allowed = account.themes || [];
  const unrestricted = allowed.length === 0;
  $('account-themes-state').innerHTML = unrestricted
    ? ui.tag('all schemes', 'idle')
    : ui.tag(fmt.number(allowed.length) + ' of ' + fmt.number(themeCatalogue.length), 'note');
  $('account-themes').innerHTML = themeCatalogue.map((theme) =>
    themeRow(theme, unrestricted || allowed.includes(theme.id))).join('');
}

function collectThemes() {
  return Array.from($('account-themes').querySelectorAll('input:checked'))
    .map((input) => input.dataset.themeId);
}

/* ---- the rest of the page ---------------------------------------------- */

// Every build this set has been seen running, newest first and the current one flagged.
// One television that has been through four releases is a different thing from four
// televisions, and the history is what says which of those an operator is looking at.
function versionHistory(device) {
  const versions = device.versions || [];
  if (!versions.length) return '';
  return '<span class="list-versions">' + versions.map((entry) =>
    ui.chip(entry.version + (entry.version === device.version ? ' · now' : ''),
      entry.version === device.version ? 'accent' : null)).join('') + '</span>';
}

function renderDevices(account) {
  const devices = account.devices || [];
  $('account-devices').innerHTML = devices.length ? devices.map((device) => {
    const seen = fmt.presence(device.lastSeen);
    return '<div class="list-row"><span class="list-main"><span>' +
      '<span class="list-title">' +
        '<span class="dot" data-tone="' + seen.tone + '" title="' + seen.label + '"></span>' +
        fmt.escape(device.name || 'Memby TV') + '</span>' +
      '<span class="list-meta">' +
        fmt.escape(device.version ? 'Memby ' + device.version : 'Legacy Memby client') +
        ' · ' + fmt.escape(seen.label) +
        ' · last seen ' + fmt.escape(fmt.when(device.lastSeen)) +
        ' · signed in ' + fmt.escape(fmt.when(device.signedInAt)) +
      '</span>' + versionHistory(device) + '</span></span>' +
      '<span class="list-actions">' +
        '<button class="small" data-account-action="rename-device" data-device-id="' +
          fmt.escape(device.id) + '" data-device-name="' + fmt.escape(device.name) + '"' +
          (device.id ? '' : ' disabled') + '>Rename</button>' +
        '<button class="small danger" data-account-action="remove-device" data-device-id="' +
          fmt.escape(device.id) + '"' + (device.id ? '' : ' disabled') + '>Sign out</button>' +
      '</span></div>';
  }).join('') : ui.empty('No devices are signed in to this user.');
}

function renderRecommendations(account) {
  const prompt = account.recommendations || {};
  const ratings = prompt.ratings || [];
  const dimensions = [
    ['Genres', prompt.genres], ['Studios', prompt.studios], ['Actors', prompt.actors],
    ['Actresses', prompt.actresses], ['Directors', prompt.directors],
    ['Types', prompt.contentTypes],
  ].filter((entry) => (entry[1] || []).length);

  const chips = ratings.map((rating) =>
    ui.chip(rating.title + ' · ' + fmt.number(rating.rating) + ' ★', 'warn')).join('') +
    dimensions.flatMap(([label, values]) => (values || [])
      .map((value) => ui.chip(label + ': ' + value))).join('');

  const state = prompt.completed
    ? ui.tag('completed', 'ok')
    : prompt.prompted ? ui.tag('prompt queued', 'warn') : ui.tag('not invited', 'idle');

  $('account-recommendations').innerHTML = '<div class="row tight">' + state + '</div>' +
    (chips ? '<div class="chips">' + chips + '</div>'
      : ui.empty('No recommendation selections have been saved.'));

  $('account-recommendation-actions').innerHTML = prompt.completed
    ? '<button data-account-action="reset-recommendations">Clear stored choices</button>'
    : prompt.prompted
      ? '<button data-account-action="cancel-recommendation-prompt">Cancel prompt</button>'
      : '<button class="primary" data-account-action="prompt-recommendations">Send setup prompt</button>';
}

function renderIdentity(account) {
  const devices = account.devices || [];
  const active = devices.filter((device) => fmt.recent(device.lastSeen)).length;
  $('page-title').textContent = account.username || 'Unnamed user';
  $('page-intro').textContent = 'Memby user · ' + fmt.number(devices.length) + ' device' +
    (devices.length === 1 ? '' : 's') + ' · last seen ' + fmt.when(account.lastSeen);
  document.title = (account.username || 'User') + ' · Memby admin';
  $('account-identity').innerHTML = '<div class="row">' +
    '<span class="avatar">' + fmt.escape(fmt.initials(account.username)) + '</span>' +
    '<span class="list-title">' + fmt.escape(account.username || 'Unnamed user') + '</span>' +
    (active ? ui.tag(active + ' active now', 'ok') : ui.tag('idle', 'idle')) +
    '<span class="chip mono">' + fmt.escape(account.id) + '</span></div>';
}

Admin.onRefresh(async () => {
  $('account-settings-history').href = '/admin/accounts/' + encodeURIComponent(userId) + '/settings';
  const payload = await Admin.api('/admin/api/accounts');
  catalogue = payload.catalogue || catalogue;
  themeCatalogue = payload.themes || themeCatalogue;
  const account = (payload.accounts || []).find((entry) => entry.id === userId);
  if (!account) {
    $('account-identity').innerHTML =
      ui.empty('This user is no longer signed in to Memby.');
    return;
  }
  renderIdentity(account);
  renderDevices(account);
  renderRecommendations(account);
  if (!dirty) renderSettings(account);
  if (!themesDirty) renderThemes(account);
});

/* ---- actions ------------------------------------------------------------ */

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

function themeMessage(text) { $('account-themes-message').textContent = text || ''; }

document.addEventListener('input', (event) => {
  if ($('account-themes').contains(event.target)) {
    themesDirty = true;
    themeMessage('unsaved changes');
    return;
  }
  if (!$('account-settings').contains(event.target)) return;
  dirty = true;
  message('unsaved changes');
});

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

  if (action === 'rename-device') {
    const name = prompt('Name this Memby device', button.dataset.deviceName || 'Memby TV');
    if (name === null || !name.trim()) return;
    Admin.act(() => Admin.api(base + '/devices/' + encodeURIComponent(button.dataset.deviceId), {
      method: 'PUT', body: JSON.stringify({ deviceName: name.trim() }),
    }));
  }
  if (action === 'remove-device') {
    if (!confirm('Sign this device out of Memby? Its Emby account will not be changed.')) return;
    Admin.act(() => Admin.api(base + '/devices/' +
      encodeURIComponent(button.dataset.deviceId), { method: 'DELETE' }));
  }
  if (action === 'remove-account') {
    if (!confirm('Remove Memby access for ' + ($('page-title').textContent || 'this user') +
      '? Every Memby device will be signed out. Their Emby account will not be changed.')) return;
    Admin.act(async () => {
      await Admin.api(base + '/sessions', { method: 'DELETE' });
      location.href = '/admin/accounts';
    });
  }
  if (action === 'reset-recommendations') {
    if (!confirm('Clear this person’s stored recommendation choices? Viewing history remains intact.')) return;
    Admin.act(() => Admin.api(base + '/recommendations', { method: 'DELETE' }));
  }
  if (action === 'prompt-recommendations') {
    Admin.act(() => Admin.api(base + '/recommendations/prompt', { method: 'PUT' }));
  }
  if (action === 'cancel-recommendation-prompt') {
    if (!confirm('Cancel this person’s queued recommendation prompt?')) return;
    Admin.act(() => Admin.api(base + '/recommendations', { method: 'DELETE' }));
  }
  if (action === 'push-preferences') {
    const preferences = collectSettings();
    message('pushing…');
    Admin.act(async () => {
      await Admin.api(base + '/preferences', {
        method: 'PUT', body: JSON.stringify({ preferences }),
      });
      // Cleared before the refresh, so the form is redrawn from what the server actually
      // stored rather than from what was submitted.
      dirty = false;
      message('pushed');
    });
  }
  if (action === 'save-themes') {
    const themes = collectThemes();
    if (!themes.length &&
      !confirm('Allow this person no colour schemes? They will be left on Midnight with ' +
        'nothing to choose between.')) return;
    themeMessage('saving…');
    Admin.act(async () => {
      await Admin.api(base + '/themes', {
        method: 'PUT', body: JSON.stringify({ themes }),
      });
      // Cleared before the refresh so the boxes are redrawn from what was stored, which is
      // how "every box ticked" comes back as the unrestricted state rather than as a list.
      themesDirty = false;
      themeMessage('saved');
    });
  }
  if (action === 'all-themes') {
    $('account-themes').querySelectorAll('input[data-theme-id]')
      .forEach((input) => { input.checked = true; });
    themesDirty = true;
    themeMessage('unsaved changes');
  }
  if (action === 'reload-preferences') {
    dirty = false;
    message('');
    Admin.refresh();
  }
  if (action === 'reset-preferences') {
    if (!confirm('Restore the Memby defaults for this person? Their televisions will pick ' +
      'the change up the next time they check in.')) return;
    dirty = false;
    Admin.act(() => Admin.api(base + '/preferences', { method: 'DELETE' }));
  }
});
