
import { prettyBinarySize } from '@cloudron/pankow/utils';
import { RELAY_PROVIDERS, ISTATES, EVENTS } from './constants.js';
import { Marked } from 'marked';

function safeMarked() {
  const marked = new Marked({
    renderer: {
      link({ href, title, text }) {
        if (href && href.startsWith('mailto:')) return text; // mailto is rendered as text
        const titleAttr = title ? ` title="${title}"` : '';
        const isAbsolute = href && (href.startsWith('http://') || href.startsWith('https://') || href.startsWith('//'));
        const targetAttr = isAbsolute ? ' target="_blank"' : '';
        return `<a href="${href}"${targetAttr}${titleAttr}>${text}</a>`;
      }
    }
  });
  return marked;
}

function renderSafeMarkdown(text) {
  return safeMarked().parse(text);
}

function prettyRelayProviderName(provider) {
  if (provider === 'noop') return 'Disabled (no email will be sent)';
  const tmp = RELAY_PROVIDERS.find(p => p.provider === provider);
  return tmp ? tmp.name : provider;
}

// https://stackoverflow.com/questions/3665115/how-to-create-a-file-in-memory-for-user-to-download-but-not-through-server#18197341
function download(filename, text) {
  var element = document.createElement('a');
  element.setAttribute('href', 'data:text/plain;charset=utf-8,' + encodeURIComponent(text));
  element.setAttribute('download', filename);

  element.style.display = 'none';
  document.body.appendChild(element);

  element.click();

  document.body.removeChild(element);
}

function mountlike(provider) {
  return provider === 'sshfs' || provider === 'cifs' || provider === 'nfs' || provider === 'mountpoint' || provider === 'ext4' || provider === 'xfs' || provider === 'disk';
}

function s3like(provider) {
  return provider === 's3' || provider === 'minio' || provider === 's3-v4-compat'
    || provider === 'exoscale-sos' || provider === 'digitalocean-spaces' || provider === 'hetzner-objectstorage'
    || provider === 'scaleway-objectstorage' || provider === 'wasabi' || provider === 'backblaze-b2' || provider === 'cloudflare-r2'
    || provider === 'linode-objectstorage' || provider === 'ovh-objectstorage' || provider === 'ionos-objectstorage'
    || provider === 'vultr-objectstorage' || provider === 'upcloud-objectstorage' || provider === 'idrive-e2'
    || provider === 'contabo-objectstorage' || provider === 'synology-c2-objectstorage';
}

function eventlogDetails(eventLog, app = null, appIdContext = '') {
  const data = eventLog.data;
  const errorMessage = data.errorMessage;
  let details;

  function appName(pre, app, defaultValue) {
    if (appIdContext) return defaultValue || '';

    pre = pre ? (pre + ' ') : '';

    return pre + (app.label || app.fqdn || app.subdomain) + ' (' + app.manifest.title + ') ';
  }

  switch (eventLog.action) {
    case EVENTS.ACTIVATE:
      return 'Cloudron was activated';

    case EVENTS.PROVISION:
      return 'Cloudron was setup';

    case EVENTS.RESTORE:
      return 'Cloudron was restored using backup at ' + data.remotePath;

    case EVENTS.APP_CONFIGURE: {
      if (!data.app) return '';
      app = data.app;

      if ('accessRestriction' in data) { // since it can be null
        return 'Access restriction ' + appName('of', app) + ' was changed';
      } else if ('operators' in data) {
        return 'Operators ' + appName('of', app) + ' was changed';
      } else if (data.label) {
        return `Label ${appName('of', app)} was set to ${data.label}`;
      } else if (data.tags) {
        return `Tags ${appName('of', app)} was set to ${data.tags.join(', ')}`;
      } else if (data.icon) {
        return 'Icon ' + appName('of', app) + ' was changed';
      } else if (data.memoryLimit) {
        return 'Memory limit ' + appName('of', app) + ' was set to ' + prettyBinarySize(data.memoryLimit);
      } else if (data.cpuShares) { // replaced by cpuQuota in 8.0
        return 'CPU shares ' + appName('of', app) + ' was set to ' + Math.round((data.cpuShares * 100)/1024) + '%';
      } else if (data.cpuQuota) {
        return 'CPU quota ' + appName('of', app) + ' was set to ' + data.cpuQuota + '%';
      } else if (data.env) {
        return 'Env vars ' + appName('of', app) + ' was changed';
      } else if ('debugMode' in data) { // since it can be null
        if (data.debugMode) return appName('', app, 'App') + ' was placed in repair mode';
        else return appName('', app, 'App') + ' was taken out of repair mode';
      } else if ('enableBackup' in data) {
        return 'Automatic backups ' + appName('of', app) + ' was ' + (data.enableBackup ? 'enabled' : 'disabled');
      } else if ('enableAutomaticUpdate' in data) {
        return 'Automatic updates ' + appName('of', app) + ' was ' + (data.enableAutomaticUpdate ? 'enabled' : 'disabled');
      } else if ('reverseProxyConfig' in data) {
        return 'Reverse proxy configuration ' + appName('of', app) + ' was updated';
      } else if ('upstreamUri' in data) {
        return 'Upstream URI ' + appName('of', app) + ' was updated';
      } else if ('cert' in data) {
        if (data.cert) return 'Custom certificate was set ' + appName('for', app);
        else return 'Certificate ' + appName('of', app) + ' was reset';
      } else if (data.subdomain) {
        if (data.fqdn !== data.app.fqdn) {
          return 'Location ' + appName('of', app) + ' was changed to ' + data.fqdn;
        } else if (data.redirectDomains !== data.app.redirectDomains) {
          const altFqdns = data.redirectDomains.map(a => a.fqdn);
          return 'Alternate domains ' + appName('of', app) + ' was ' + (altFqdns.length ? 'set to ' + altFqdns.join(', ') : 'reset');
        } else if (data.aliasDomains !== data.app.aliasDomains) {
          const aliasDomains = data.aliasDomains.map(a => a.fqdn);
          return 'Alias domains ' + appName('of', app) + ' was ' + (aliasDomains.length ? 'set to ' + aliasDomains.join(', ') : 'reset');
        } else if (data.portBindings !== data.app.portBindings) {
          return 'Port bindings ' + appName('of', app) + ' was changed';
        }
      } else if ('dataDir' in data) {
        if (data.dataDir) return 'Data directory ' + appName('of', app) + ' was set ' + data.dataDir;
        else return 'Data directory ' + appName('of', app) + ' was reset';
      } else if ('icon' in data) {
        if (data.icon) return 'Icon ' + appName('of', app) + ' was set';
        else return 'Icon ' + appName('of', app) + ' was reset';
      } else if ('mailboxName' in data) {
        if (data.mailboxName) return `Mailbox ${appName('of', app)} was set to ${data.mailboxDisplayName || '' } ${data.mailboxName}@${data.mailboxDomain}`;
        else return 'Mailbox ' + appName('of', app) + ' was disabled';
      }

      return appName('', app, 'App ') + 'was re-configured';
    }

    case EVENTS.APP_INSTALL:
      if (!data.app) return '';
      details = data.app.versionsUrl ? 'Community app ' : '';
      details += data.app.manifest.title + ' (package v' + data.app.manifest.version + ')';
      details += data.sourceBuild ? ' was built and installed' : ' was installed';
      details += appIdContext ? '' : ` at ${data.app.fqdn}`;
      return details;

    case EVENTS.APP_RESTORE:
      if (!data.app) return '';
      details = appName('', data.app, 'App') + ' was restored';
      // older versions  (<3.5) did not have these fields
      if (data.fromManifest) details += ' from version ' + data.fromManifest.version;
      if (data.toManifest) details += ' to version ' + data.toManifest.version;
      if (data.remotePath) details += ' using backup at ' + data.remotePath;
      return details;

    case EVENTS.APP_IMPORT:
      if (!data.app) return '';
      details = appName('', data.app, 'App') + ' was imported';
      if (data.toManifest) details += ' to version ' + data.toManifest.version;
      if (data.remotePath) details += ' using backup at ' + data.remotePath;
      return details;

    case EVENTS.APP_UNINSTALL:
      if (!data.app) return '';
      return appName('', data.app, 'App') + ' (package v' + data.app.manifest.version + ') was uninstalled';

    case EVENTS.APP_UPDATE:
      if (!data.app) return '';
      return 'Update ' + appName('of', data.app) + ' started from v' + data.fromManifest.version + ' to v' + data.toManifest.version;

    case EVENTS.APP_UPDATE_FINISH:
      if (!data.app) return '';
      return appName('', data.app, 'App') + ' was updated to v' + data.app.manifest.version;

    case EVENTS.APP_BACKUP:
      if (!data.app) return '';
      return 'Backup ' + appName('of', data.app) + ' started';

    case EVENTS.APP_BACKUP_FINISH:
      if (!data.app) return '';
      if (errorMessage) return 'Backup ' + appName('of', data.app) + ' failed: ' + errorMessage;
      else return 'Backup ' + appName('of', data.app) + ' succeeded';

    case EVENTS.APP_CLONE:
      if (appIdContext === data.oldAppId) return 'App was cloned to ' + data.newApp.fqdn + ' using backup at ' + data.remotePath;
      else if (appIdContext === data.appId) return 'App was cloned from ' + data.oldApp.fqdn + ' using backup at ' + data.remotePath;
      else return appName('', data.newApp, 'App') + ' was cloned ' + appName('from', data.oldApp) + ' using backup at ' + data.remotePath;

    case EVENTS.APP_REPAIR:
      return appName('', data.app, 'App') + ' was re-configured'; // re-configure of email apps is more common?

    case EVENTS.APP_LOGIN: {
      if (!app) return '';
      return appName('', app, 'App') + ' logged in';
    }

    case EVENTS.APP_OOM:
      if (!data.app) return '';
      return appName('', data.app, 'App') + ' ran out of memory';

    case EVENTS.APP_DOWN:
      if (!data.app) return '';
      return appName('', data.app, 'App') + ' is down';

    case EVENTS.APP_UP:
      if (!data.app) return '';
      return appName('', data.app, 'App') + ' is back online';

    case EVENTS.APP_START:
      if (!data.app) return '';
      return appName('', data.app, 'App') + ' was started';

    case EVENTS.APP_STOP:
      if (!data.app) return '';
      return appName('', data.app, 'App') + ' was stopped';

    case EVENTS.APP_RESTART:
      if (!data.app) return '';
      return appName('', data.app, 'App') + ' was restarted';

    case EVENTS.ARCHIVES_ADD:
      return 'Backup ' + data.backupId + ' added to archive';

    case EVENTS.ARCHIVES_DEL:
      return 'Backup ' + data.backupId + ' deleted from archive';

    case EVENTS.BACKUP_START:
      return `Backup started at site ${data.siteName}`;

    case EVENTS.BACKUP_FINISH:
      if (!errorMessage) return `Cloudron backup created at site ${data.siteName}`;
      else return `Cloudron backup at site ${data.siteName} errored with error: ${errorMessage}`;

    case EVENTS.BACKUP_CLEANUP_START:
      return 'Backup cleaner started';

    case EVENTS.BACKUP_CLEANUP_FINISH:
      return errorMessage ? 'Backup cleaner errored: ' + errorMessage : 'Backup cleaner removed ' + (data.removedBoxBackupPaths ? data.removedBoxBackupPaths.length : '0') + ' backup(s)';

    case EVENTS.BACKUP_SITE_ADD:
      return `New backup site ${data.name} added with provider ${data.provider} and format ${data.format}`;

    case EVENTS.BACKUP_SITE_UPDATE:
      if (data.schedule) {
        return `Backup site ${data.name} schedule was updated to ${data.schedule}`;
      } else if (data.limits) {
        return `Backup site ${data.name} limits were updated`;
      } else if (data.retention) {
        return `Backup site ${data.name} retention was updated`;
      } else if (data.contents) {
        return `Backup site ${data.name} contents were updated`;
      } else if (data.encryption) {
        return `Backup site ${data.name} encryption was ${data.encryption ? 'enabled' : 'disabled'}`;
      } else if (data.config) {
        return `Backup site ${data.name} credentials were updated`;
      } else if (data.oldName) {
        return `Backup site was renamed from ${data.oldName} to ${data.name}`;
      } else {
        return `Backup site ${data.name} was updated`;
      }

    case EVENTS.BACKUP_SITE_REMOVE:
      return `Backup site ${data.name} removed`;

    case EVENTS.BACKUP_INTEGRITY_START:
      return 'Backup integrity check started'; // for ${data.backupId}

    case EVENTS.BACKUP_INTEGRITY_FINISH:
      if (!errorMessage) return `Backup integrity check ${data.status}`; // passed or failed
      else return `Backup integrity check errored: ${errorMessage}`;

    case EVENTS.BRANDING_AVATAR:
      return 'Cloudron Avatar Changed';

    case EVENTS.BRANDING_NAME:
      return 'Cloudron Name set to ' + data.name;

    case EVENTS.BRANDING_FOOTER:
      return 'Cloudron Footer set to ' + data.footer;

    case EVENTS.CERTIFICATE_NEW:
      details = 'Certificate installation for ' + data.domain + (errorMessage ? ' failed' : ' succeeded');
      if (data.renewalInfo) details += `. Recommended renewal time is between ${data.renewalInfo.start} and ${data.renewalInfo.end}`;
      return details;

    case EVENTS.CERTIFICATE_RENEWAL:
      return 'Certificate renewal for ' + data.domain + (errorMessage ? ' failed' : ' succeeded');

    case EVENTS.CERTIFICATE_CLEANUP:
      return 'Certificate(s) of ' + data.domains.join(',') + ' was cleaned up since they expired 6 months ago';

    case EVENTS.DASHBOARD_DOMAIN_UPDATE:
      return 'Dashboard domain set to ' + data.fqdn || (data.subdomain + '.' + data.domain);

    case EVENTS.DIRECTORY_SERVER_CONFIGURE:
      if (data.fromEnabled !== data.toEnabled) return 'Directory server was ' + (data.toEnabled ? 'enabled' : 'disabled');
      else return 'Directory server configuration was changed';

    case EVENTS.DOMAIN_ADD:
      return 'Domain ' + data.domain + ' with ' + data.provider + ' provider was added';

    case EVENTS.DOMAIN_UPDATE:
      return 'Domain ' + data.domain + ' with ' + data.provider + ' provider was updated';

    case EVENTS.DOMAIN_REMOVE:
      return 'Domain ' + data.domain + ' was removed';

    case EVENTS.EXTERNAL_LDAP_CONFIGURE:
      if (data.config.provider === 'noop') return 'External Directory disabled';
      else return 'External Directory set to ' + data.config.url + ' (' + data.config.provider + ')';

    case EVENTS.GROUP_ADD:
      return 'Group ' + data.name + ' was added';

    case EVENTS.GROUP_UPDATE:
      return 'Group name changed from ' + data.oldName + ' to ' + data.group.name;

    case EVENTS.GROUP_REMOVE:
      return 'Group ' + data.group.name + ' was removed';

    case EVENTS.GROUP_MEMBERSHIP:
      return 'Group membership of ' + data.group.name + ' changed. Now was ' + data.userIds.length + ' member(s).';

    case EVENTS.INSTALL_FINISH:
      return 'Cloudron version ' + data.version + ' installed';

    case EVENTS.MAIL_LOCATION:
      return 'Mail server location was changed to ' + data.subdomain + (data.subdomain ? '.' : '') + data.domain;

    case EVENTS.MAIL_ENABLED:
      return 'Mail was enabled for domain ' + data.domain;

    case EVENTS.MAIL_DISABLED:
      return 'Mail was disabled for domain ' + data.domain;

    case EVENTS.MAIL_MAILBOX_ADD:
      return 'Mailbox ' + data.name + '@' + data.domain + ' was added';

    case EVENTS.MAIL_MAILBOX_UPDATE:
      if (data.aliases) return 'Mailbox aliases of ' + data.name + '@' + data.domain + ' was updated';
      else return 'Mailbox ' + data.name + '@' + data.domain + ' was updated';

    case EVENTS.MAIL_MAILBOX_REMOVE:
      return 'Mailbox ' + data.name + '@' + data.domain + ' was removed';

    case EVENTS.MAIL_LIST_ADD:
      return 'Mail list ' + data.name + '@' + data.domain + 'was added';

    case EVENTS.MAIL_LIST_UPDATE:
      return 'Mail list ' + data.name + '@' + data.domain + ' was updated';

    case EVENTS.MAIL_LIST_REMOVE:
      return 'Mail list ' + data.name + '@' + data.domain + ' was removed';

    case EVENTS.REGISTRY_ADD:
      return 'Docker registry ' + data.registry.provider + '@' + data.registry.serverAddress + ' was added';

    case EVENTS.REGISTRY_UPDATE:
      return 'Docker registry updated to ' + data.newRegistry.provider + '@' + data.newRegistry.serverAddress;

    case EVENTS.REGISTRY_DEL:
      return 'Docker registry ' + data.registry.provider + '@' + data.registry.serverAddress + ' was removed';

    case EVENTS.START:
      return 'Cloudron started with version ' + data.version;

    case EVENTS.SERVICE_CONFIGURE:
      return 'Service ' + data.id + ' was configured';

    case EVENTS.SERVICE_REBUILD:
      return 'Service ' + data.id + ' was rebuilt';

    case EVENTS.SERVICE_RESTART:
      return 'Service ' + data.id + ' was restarted';

    case EVENTS.UPDATE:
      return 'Cloudron update to version ' + data.boxUpdateInfo.version + ' was started';

    case EVENTS.UPDATE_FINISH:
      if (errorMessage) return 'Cloudron update errored. Error: ' + errorMessage;
      else return 'Cloudron updated to version ' + data.newVersion;

    case EVENTS.USER_ADD:
      return 'User ' + data.email + (data.user.username ? ' (' + data.user.username + ')' : '') + ' was added';

    case EVENTS.USER_UPDATE:
      return 'User ' + (data.user ? (data.user.email + (data.user.username ? ' (' + data.user.username + ')' : '')) : data.userId) + ' was updated';

    case EVENTS.USER_REMOVE:
      return 'User ' + (data.user ? (data.user.email + (data.user.username ? ' (' + data.user.username + ')' : '')) : data.userId) + ' was removed';

    case EVENTS.USER_TRANSFER:
      return 'Apps of ' + data.oldOwnerId + ' was transferred to ' + data.newOwnerId;

    case EVENTS.USER_LOGIN:
      if (data.mailboxId) {
        return 'User ' + (data.user ? data.user.username : data.userId) + ' logged in to mailbox ' + data.mailboxId;
      } else if (data.appId) {
        return 'User ' + (data.user ? data.user.username : data.userId) + ' logged in' + (appIdContext ? '' : ' to ' + (app ? app.fqdn : data.appId));
      } else { // can happen with directoryserver
        return 'User ' + (data.user ? data.user.username : data.userId) + ' authenticated';
      }

    case EVENTS.USER_LOGIN_GHOST:
      return 'User ' + (data.user ? data.user.username : data.userId) + ' was impersonated';

    case EVENTS.USER_LOGOUT:
      return 'User ' + (data.user ? data.user.username : data.userId) + ' logged out';

    case EVENTS.USER_DIRECTORY_PROFILE_CONFIG_UPDATE:
      return 'User directory profile config updated. Mandatory 2FA: ' + (data.config.mandatory2FA) + ' Lock profiles: ' + (data.config.lockUserProfiles);

    case EVENTS.DYNDNS_UPDATE: {
      details = errorMessage ? 'Error updating DNS. ' : 'Updated DNS. ';
      if (data.fromIpv4 !== data.toIpv4) details += 'From IPv4 ' + data.fromIpv4 + ' to ' + data.toIpv4 + '. ';
      if (data.fromIpv6 !== data.toIpv6) details += 'From IPv6 ' + data.fromIpv6 + ' to ' + data.toIpv6 + '.';
      if (errorMessage) details += ' ' + errorMessage;
      return details;
    }

    case EVENTS.SUPPORT_SSH:
      return 'Remote Support was ' + (data.enable ? 'enabled' : 'disabled');

    case EVENTS.SUPPORT_TICKET:
      return 'Support ticket was created';

    case EVENTS.VOLUME_ADD:
      return 'Volume "' + (data.volume || data).name + '" was added';

    case EVENTS.VOLUME_UPDATE:
      return 'Volme "' + (data.volume || data).name + '" was updated';

    case EVENTS.VOLUME_REMOUNT:
      return 'Volume "' + (data.volume || data).name + '" was remounted';

    case EVENTS.VOLUME_REMOVE:
      return 'Volume "' + (data.volume || data).name + '" was removed';

    default:
      return eventLog.action;
  }
}

function eventlogSource(eventlog, app = null) {
  const source = eventlog.source;

  let line = source.username || source.userId || source.mailboxId || source.authType || 'system';
  if (source.appId) {
    // const app = null; //getApp(source.appId);
    line += ' - ' + (app ? app.fqdn : source.appId);
  }

  return line;
}

function taskNameFromInstallationState(installationState) {
  switch (installationState) {
    case ISTATES.PENDING_INSTALL: return 'install';
    case ISTATES.PENDING_CLONE: return 'clone';
    case ISTATES.PENDING_LOCATION_CHANGE: return 'location change';
    case ISTATES.PENDING_CONFIGURE: return 'configure';
    case ISTATES.PENDING_RECREATE_CONTAINER: return 'create container';
    case ISTATES.PENDING_DEBUG: return 'debug';
    case ISTATES.PENDING_RESIZE: return 'resize';
    case ISTATES.PENDING_DATA_DIR_MIGRATION: return 'data migration';
    case ISTATES.PENDING_SERVICES_CHANGE: return 'service change';
    case ISTATES.PENDING_UNINSTALL: return 'uninstall';
    case ISTATES.PENDING_RESTORE: return 'restore';
    case ISTATES.PENDING_IMPORT: return 'import';
    case ISTATES.PENDING_UPDATE: return 'update';
    case ISTATES.PENDING_BACKUP: return 'backup';
    case ISTATES.PENDING_START: return 'start app';
    case ISTATES.PENDING_STOP: return 'stop app';
    case ISTATES.PENDING_RESTART: return 'restart app';
    default: return installationState || '';
  }
}

// checks provision status and redirects to correct view
// {
//    setup: { active, message, errorMessage }
//    restore { active, message, errorMessage }
//    activated
//    adminFqn
// }
// returns true if redirected . currentView is one of 'dashboard', 'restore', 'setup', 'activation'
function redirectIfNeeded(status, currentView) {
  var search = decodeURIComponent(window.location.search).slice(1).split('&').map(function (item) { return item.split('='); }).reduce(function (o, k) { o[k[0]] = k[1]; return o; }, {});

  if ('develop' in search  || localStorage.getItem('develop')) {
    console.warn('Cloudron develop mode on. To disable run localStorage.removeItem(\'develop\')');
    localStorage.setItem('develop', true);
    return false;
  }

  if (status.activated) {
    if (currentView === 'dashboard') {
      // support local development with localhost check
      if (window.location.hostname !== status.adminFqdn && window.location.hostname !== 'localhost' && !window.location.hostname.startsWith('192.')) {
        // user is accessing by IP or by the old admin location (pre-migration)
        window.location.href = '/setup.html' + window.location.search;
        return true;
      }

      return false;
    }
    window.location.href = 'https://' + status.adminFqdn + '/';
    return true;
  }

  if (status.setup.active) {
    console.log('Setup is active');
    if (currentView === 'setup') return false;
    window.location.href = '/setup.html' + window.location.search;
    return true;
  }

  if (status.restore.active) {
    console.log('Restore is active');
    if (currentView === 'restore') return;
    window.location.href = '/restore.html' + window.location.search;
    return true;
  }

  if (status.adminFqdn) {
    console.log('adminFqdn is set');
    // if we are here from https://ip/activation.html ,go to https://admin/activation.html
    if (status.adminFqdn !== window.location.hostname) {
      window.location.href = 'https://' + status.adminFqdn + '/activation.html' + (window.location.search);
      return true;
    }
    if (currentView === 'activation') return false;
    window.location.href = 'https://' + status.adminFqdn + '/activation.html' + (window.location.search);
    return true;
  }

  if (currentView !== 'setup' && currentView !== 'restore') {
    window.location.href = '/setup.html' + window.location.search;
    return true;
  }

  // if we are here, proceed with current view
  return false;
}

// strip potential sso tags in postinstall notes
function stripSsoInfo(notes, sso) {
  if (sso) return notes.replace(/<nosso>.*?<\/nosso>/gs, '').replace(/<sso>/gs, '').replace(/<\/sso>/gs, '');
  return notes.replace(/<sso>.*?<\/sso>/gs, '').replace(/<nosso>/gs, '').replace(/<\/nosso>/gs, '');
}

function getDataURLFromFile(file) {
  return new Promise((resolve, reject) => {
    const reader = new FileReader();

    reader.onload = function(event) {
      resolve(event.target.result);
    };

    reader.onerror = function(event) {
      reject(event.target.error);
    };

    reader.readAsDataURL(file);
  });
}

function getTextFromFile(file) {
  return new Promise((resolve, reject) => {
    const reader = new FileReader();
    reader.onload = (e) => resolve(e.target.result);
    reader.onerror = (e) => reject(e);
    reader.readAsText(file);
  });
}

// values correspond to cron days
const cronDays = [
  { id: 0, name: 'Sunday' },
  { id: 1, name: 'Monday' },
  { id: 2, name: 'Tuesday' },
  { id: 3, name: 'Wednesday' },
  { id: 4, name: 'Thursday' },
  { id: 5, name: 'Friday' },
  { id: 6, name: 'Saturday' },
];

// generates 24h time sets (instead of american 12h) to avoid having to translate everything to locales eg. 12:00
const cronHours = Array.from({ length: 24 }).map(function (v, i) { return { id: i, name: (i < 10 ? '0' : '') + i + ':00' }; });

function prettySchedule(pattern) {
  if (!pattern) return '';

  const tmp = pattern.trim().split(/\s+/); // remove extra spaces between tokens, which is valid cron
  if (tmp.length === 1) return pattern.charAt(0).toUpperCase() + pattern.slice(1); // case for 'never' - capitalize

  if (tmp.length === 5) tmp.unshift('0'); // if seconds is missing, add it

  if (tmp.length !== 6) return `Unrecognized pattern - ${pattern}`;

  const hours = tmp[2].split(',').sort((a, b) => Number(a) - Number(b));
  const days = tmp[5].split(',').sort((a, b) => Number(a) - Number(b));

  try {
    const prettyDay = (days.length === 7 || days[0] === '*') ? 'Every day' : days.map((day) => { return cronDays[parseInt(day, 10)].name.substr(0, 3); }).join(', ');
    const prettyHour = (hours.length === 24 || hours[0] === '*') ? 'hourly' : hours.map((hour) => { return cronHours[parseInt(hour, 10)]; }).sort((a,b) => a.id - b.id).map(h => h.name).join(', ');
    return `${prettyDay} @ ${prettyHour}`;
  } catch (error) {
    console.error('Unable to build pattern.', error);
    return `Unrecognized pattern - ${pattern}`;
  }
};

function parseSchedule(pattern) {
    const tmp = pattern.trim().split(/\s+/); // remove extra spaces between tokens, which is valid cron
    if (tmp.length === 1) return console.error(`Never pattern should not be passed here - ${pattern}`);

    if (tmp.length === 5) tmp.unshift('0'); // if seconds is missing, add it

    if (tmp.length !== 6) return console.error(`Unrecognized pattern - ${pattern}`);

    const tmpHours = tmp[2].split(',');
    const tmpDays = tmp[5].split(',');

    let days, hours;
    if (tmpDays[0] === '*') days = cronDays.map((day) => { return day.id; });
    else days = tmpDays.map((day) => { return parseInt(day, 10); });

    if (tmpHours[0] === '*') hours = cronHours.map(h => h.id);
    else hours = tmpHours.map((hour) => { return parseInt(hour, 10); });

    return { days, hours };
}

function getColor(numOfSteps, step) {
  const deg = 360/numOfSteps;
  return `hsl(${deg*step} 70% 50%)`;
}

// split path into a prefix (anything before timestamp or 'snapshot') and the remaining remotePath
function parseFullBackupPath(fullPath) {
    const parts = fullPath.split('/');
    const timestampRegex = /^\d{4}-\d{2}-\d{2}-\d{6}-\d{3}$/; // timestamp (tag)

    const idx = parts.findIndex(p => timestampRegex.test(p) || p === 'snapshot');

    let remotePath, prefix;
    if (idx === -1) {
      remotePath = parts.pop() || parts.pop(); // if fs+rsync there may be a trailing slash, so this removes it. this is basename()
      prefix = parts.join('/'); // this is dirname()
    } else {
      prefix = parts.slice(0, idx).join('/');
      remotePath = parts.slice(idx).join('/');
    }

    return { prefix, remotePath };
}

function base64urlEncode(buffer) {
  return btoa(String.fromCharCode(...buffer))
    .replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
}

function generateCodeVerifier() {
  const array = new Uint8Array(32);
  crypto.getRandomValues(array);
  return base64urlEncode(array);
}

async function computeCodeChallenge(verifier) {
  const hash = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(verifier));
  return base64urlEncode(new Uint8Array(hash));
}

async function startAuthFlow(clientId, apiOrigin) {
  const codeVerifier = generateCodeVerifier();
  const codeChallenge = await computeCodeChallenge(codeVerifier);

  sessionStorage.setItem('pkce_code_verifier', codeVerifier);
  sessionStorage.setItem('pkce_client_id', clientId);
  sessionStorage.setItem('pkce_api_origin', apiOrigin || '');

  const redirectUri = window.location.origin + '/authcallback.html';
  const base = apiOrigin || '';
  return `${base}/openid/auth?client_id=${clientId}&scope=openid email profile&response_type=code&redirect_uri=${redirectUri}&code_challenge=${codeChallenge}&code_challenge_method=S256`;
}

// named exports
export {
  renderSafeMarkdown,
  prettyRelayProviderName,
  download,
  mountlike,
  s3like,
  eventlogDetails,
  eventlogSource,
  taskNameFromInstallationState,
  redirectIfNeeded,
  stripSsoInfo,
  getDataURLFromFile,
  getTextFromFile,
  cronDays,
  cronHours,
  getColor,
  prettySchedule,
  parseSchedule,
  parseFullBackupPath,
  startAuthFlow
};

// default export
export default {
  renderSafeMarkdown,
  prettyRelayProviderName,
  download,
  mountlike,
  s3like,
  eventlogDetails,
  eventlogSource,
  taskNameFromInstallationState,
  redirectIfNeeded,
  stripSsoInfo,
  getDataURLFromFile,
  getTextFromFile,
  cronDays,
  cronHours,
  getColor,
  prettySchedule,
  parseSchedule,
  parseFullBackupPath,
  startAuthFlow
};
