
import { API_ORIGIN, APP_TYPES, PROXY_APP_ID, HSTATES, ISTATES, RSTATES } from '../constants.js';
import { eachLimit } from 'async';
import { fetcher } from '@cloudron/pankow';
import { sleep } from '@cloudron/pankow/utils';
import moment from 'moment-timezone';
import DashboardModel from './DashboardModel.js';
import ProfileModel from './ProfileModel.js';

const dashboardModel = DashboardModel.create();
const profileModel = ProfileModel.create();

function installationStateLabel(app) {
  if (!app) return '';

  const waiting = app.progress === 0 ? ' (Queued)' : '';

  switch (app.installationState) {
    case ISTATES.PENDING_INSTALL:
      return 'Installing' + waiting;
    case ISTATES.PENDING_CLONE:
      return 'Cloning' + waiting;
    case ISTATES.PENDING_LOCATION_CHANGE:
    case ISTATES.PENDING_CONFIGURE:
    case ISTATES.PENDING_RECREATE_CONTAINER:
    case ISTATES.PENDING_SERVICES_CHANGE:
    case ISTATES.PENDING_DEBUG:
      return 'Configuring' + waiting;
    case ISTATES.PENDING_RESIZE:
      return 'Resizing' + waiting;
    case ISTATES.PENDING_DATA_DIR_MIGRATION:
      return 'Migrating data' + waiting;
    case ISTATES.PENDING_UNINSTALL: return 'Uninstalling' + waiting;
    case ISTATES.PENDING_RESTORE: return 'Restoring' + waiting;
    case ISTATES.PENDING_IMPORT: return 'Importing' + waiting;
    case ISTATES.PENDING_UPDATE: return 'Updating' + waiting;
    case ISTATES.PENDING_BACKUP: return 'Backing up' + waiting;
    case ISTATES.PENDING_START: return 'Starting' + waiting;
    case ISTATES.PENDING_STOP: return 'Stopping' + waiting;
    case ISTATES.PENDING_RESTART: return 'Restarting' + waiting;
    case ISTATES.ERROR: {
      if (app.error && app.error.message === 'ETRYAGAIN') return 'DNS Error';
      return 'Error';
    }
    case ISTATES.INSTALLED: {
      if (app.debugMode) {
        return 'Paused (recovery mode)';
      } else if (app.runState === RSTATES.RUNNING) {
        if (!app.health) return 'Starting...'; // no data yet
        if (app.type === APP_TYPES.LINK) return '';
        if (app.health === HSTATES.HEALTHY) return 'Running';
        return 'Not responding'; // dead/exit/unhealthy
      } else if (app.runState === RSTATES.STOPPED) {
        return 'Stopped';
      } else {
        return app.runState;
      }
    }
    default: return app.installationState;
    }
}

function installationActive(app) {
  if (app.installationState === ISTATES.ERROR) return false;
  if (app.installationState === ISTATES.INSTALLED) return false;
  return true;
}

function appProgressMessage(app) {
  return app.message || (app.error ? app.error.message : '');
}

function pendingChecklistItems(app) {
  if (!app.checklist) return 0;
  return Object.keys(app.checklist).filter(function (key) { return !app.checklist[key].acknowledged; }).length;
}

function create() {
  const accessToken = localStorage.token;

  let config = null;
  let profile = null;

  async function loadConfigAndProfile() {
    let [error, result] = await dashboardModel.config();
    if (error) return console.error(error);
    config = result;

    [error, result] = await profileModel.get();
    if (error) return console.error(error);
    profile = result;
  }

  async function getTask(appId) {
    let error, result;
    try {
      result = await fetcher.get(`${API_ORIGIN}/api/v1/apps/${appId}/task`, { access_token: accessToken });
    } catch (e) {
      error = e;
    }

    if (error || result.status !== 200) {
      console.error('Failed to get task for app.', error || result.status);
      return null;
    }

    return result.body;
  }

  async function postProcess(app) {
    if (!profile || !config) await loadConfigAndProfile();

    app.ssoAuth = app.sso && (app.manifest.addons['ldap'] || app.manifest.addons['oidc'] || app.manifest.addons['proxyAuth']); // checking app.sso first ensures app.manifest.addons is not null
    app.type = app.manifest.id === PROXY_APP_ID ? APP_TYPES.PROXIED : APP_TYPES.APP;
    app.iconUrl = app.iconUrl ? `${API_ORIGIN}${app.iconUrl}?ts=${new Date(app.ts).getTime()}` : `${API_ORIGIN}/img/appicon_fallback.png`; // calculate full icon url with cache busting
    app.origin = 'https://' + app.fqdn;

    // only fetch if we have permissions and a taskId is set/active
    if (!app.taskId || (app.accessLevel !== 'operator' && app.accessLevel !== 'admin')) {
      app.progress = 0;
      app.message = '';
      app.taskMinutesActive = 0;
    } else {
      const task = await getTask(app.id);
      if (task) {
        app.progress = task.percent || 5; // avoid starting with empty
        app.message = task.message;
        app.taskMinutesActive = moment.duration(moment.utc().diff(moment.utc(task.creationTime))).asMinutes();
      } else {
        app.progress = 0;
        app.message = '';
        app.taskMinutesActive = 0;
      }
    }

    function expandTemplateVars(text) {
      // we chose - because underscore has special meaning in markdown
      text = text.replace(/\$CLOUDRON-APP-LOCATION/g, app.subdomain);
      text = text.replace(/\$CLOUDRON-APP-DOMAIN/g, app.domain);
      text = text.replace(/\$CLOUDRON-APP-FQDN/g, app.fqdn);
      text = text.replace(/\$CLOUDRON-APP-ORIGIN/g, app.origin);
      text = text.replace(/\$CLOUDRON-API-DOMAIN/g, config.adminFqdn);
      text = text.replace(/\$CLOUDRON-API-ORIGIN/g, 'https://' + config.adminFqdn);
      text = text.replace(/\$CLOUDRON-USERNAME/g, profile.username);
      text = text.replace(/\$CLOUDRON-APP-ID/g, app.id);

      return text;
    }

    if (app.manifest.postInstallMessage) {
      let text = expandTemplateVars(app.manifest.postInstallMessage);

      // [^] matches even newlines. '?' makes it non-greedy
      if (app.sso) text = text.replace(/<nosso>[^]*?<\/nosso>/g, '');
      else text = text.replace(/<sso>[^]*?<\/sso>/g, '');

      app.manifest.postInstallMessage = text.trim();
    }

    // only admins have this property
    if (app.checklist) {
      for (const key of Object.keys(app.checklist)) {
        app.checklist[key].message = expandTemplateVars(app.checklist[key].message);
      }
    } else {
      app.checklist = {};
    }

    return app;
  }

  return {
    name: 'AppsModel',
    getTask,
    async listTasks(appId) {
      let error, result;
      try {
        result = await fetcher.get(`${API_ORIGIN}/api/v1/apps/${appId}/tasks`, { access_token: accessToken });
      } catch (e) {
        error = e;
      }

      if (error || result.status !== 200) return [error || result];
      return [null, result.body.tasks];
    },
    async getAppTask(appId, taskId) {
      let error, result;
      try {
        result = await fetcher.get(`${API_ORIGIN}/api/v1/apps/${appId}/tasks/${taskId}`, { access_token: accessToken });
      } catch (e) {
        error = e;
      }

      if (error || result.status !== 200) return [error || result];
      return [null, result.body];
    },
    async stopAppTask(appId, taskId) {
      let error, result;
      try {
        result = await fetcher.post(`${API_ORIGIN}/api/v1/apps/${appId}/tasks/${taskId}/stop`, {}, { access_token: accessToken });
      } catch (e) {
        error = e;
      }

      if (error || result.status !== 204) return [error || result];
      return [null];
    },
    async install(appData, config) {
      const data = {
        subdomain: config.subdomain,
        domain: config.domain,
        secondaryDomains: config.secondaryDomains,
        ports: config.ports,
        accessRestriction: config.accessRestriction,
        cert: config.cert,
        key: config.key,
        sso: config.sso,
        overwriteDns: config.overwriteDns,
        upstreamUri: config.upstreamUri,
        backupId: config.backupId // when restoring from archive
      };

      // Support both appstore apps (manifest) and community apps (versionsUrl)
      if (appData.versionsUrl) {
        data.versionsUrl = appData.versionsUrl;
      } else if (appData.manifest) {
        data.appStoreId = `${appData.manifest.id}@${appData.manifest.version}`;
      }

      let result;
      try {
        result = await fetcher.post(`${API_ORIGIN}/api/v1/apps`, data, { access_token: accessToken });
      } catch (e) {
        return [e];
      }

      if (result.status !== 202) return [result];

      return [null, result.body];
    },
    async list() {
      let error, result;
      try {
        result = await fetcher.get(`${API_ORIGIN}/api/v1/apps`, { access_token: accessToken });
      } catch (e) {
        error = e;
      }

      if (error || result.status !== 200) return [error || result];

      await eachLimit(result.body.apps, 10, postProcess);

      return [null, result.body.apps];
    },
    async get(id) {
      let error, result;
      try {
        result = await fetcher.get(`${API_ORIGIN}/api/v1/apps/${id}`, { access_token: accessToken });
      } catch (e) {
        error = e;
      }

      if (error || result.status !== 200) return [error || result];

      return [null, await postProcess(result.body)];
    },
    async restart(id) {
      let result;
      try {
        result = await fetcher.post(`${API_ORIGIN}/api/v1/apps/${id}/restart`, {}, { access_token: accessToken });
      } catch (e) {
        return [e];
      }

      if (result.status !== 202) return [result];

      while(true) {
        let result;
        try {
          result = await fetcher.get(`${API_ORIGIN}/api/v1/apps/${id}`, { access_token: accessToken });
        } catch (e) {
          return [e];
        }

        if (result.status !== 200) return [result];

        // are we done here?
        if (result.body.installationState !== ISTATES.INSTALLED) await sleep(2000);
        else return [null];
      }
    },
    async start(id) {
      let result;
      try {
        result = await fetcher.post(`${API_ORIGIN}/api/v1/apps/${id}/start`, {}, { access_token: accessToken });
      } catch (e) {
        return [e];
      }

      if (result.status !== 202) return [result];
      return [null, result.body.taskId];

    },
    async stop(id) {
      let result;
      try {
        result = await fetcher.post(`${API_ORIGIN}/api/v1/apps/${id}/stop`, {}, { access_token: accessToken });
      } catch (e) {
        return [e];
      }

      if (result.status !== 202) return [result];
      return [null, result.body.taskId];

    },
    async configure(id, setting, data) {
      let result;
      try {
        result = await fetcher.post(`${API_ORIGIN}/api/v1/apps/${id}/configure/${setting}`, data, { access_token: accessToken });
      } catch (e) {
        return [e];
      }

      if (result.status !== 200 && result.status !== 202) return [result];
      return [null];
    },
    async uninstall(id) {
      let result;
      try {
        result = await fetcher.post(`${API_ORIGIN}/api/v1/apps/${id}/uninstall`, {}, { access_token: accessToken });
      } catch (e) {
        return [e];
      }

      if (result.status !== 202) return [result];
      return [null];
    },
    async archive(id, backupId) {
      let result;
      try {
        result = await fetcher.post(`${API_ORIGIN}/api/v1/apps/${id}/archive`, { backupId }, { access_token: accessToken });
      } catch (e) {
        return [e];
      }

      if (result.status !== 202) return [result];
      return [null];
    },
    async getEvents(id, filter = {}, page = 1, per_page = 100) {
      let result;
      try {
        result = await fetcher.get(`${API_ORIGIN}/api/v1/apps/${id}/eventlog`, { ...filter, page, per_page, access_token: accessToken });
      } catch (e) {
        return [e];
      }

      if (result.status !== 200) return [result];
      return [null, result.body.eventlogs];
    },
    async checkUpdate(id) {
      let result;
      try {
        result = await fetcher.post(`${API_ORIGIN}/api/v1/apps/${id}/check_update`, {}, { access_token: accessToken });
      } catch (e) {
        return [e];
      }

      if (result.status !== 200) return [result];
      return [null, result.body.update];
    },
    async update(id, appData, skipBackup = false) {
      const data = {
        skipBackup: !!skipBackup,
      };

      // Support both appstore apps (manifest) and community apps (versionsUrl)
      if (appData.versionsUrl) {
        data.versionsUrl = appData.versionsUrl;
      } else if (appData.manifest) {
        data.appStoreId = `${appData.manifest.id}@${appData.manifest.version}`;
      }

      let result;
      try {
        result = await fetcher.post(`${API_ORIGIN}/api/v1/apps/${id}/update`, data, { access_token: accessToken });
      } catch (e) {
        return [e];
      }

      if (result.status !== 202) return [result];
      return [null, result.body.taskId];
    },
    async backups(id) {
      // we fetch probably enough to avoid pagination
      let result;
      try {
        result = await fetcher.get(`${API_ORIGIN}/api/v1/apps/${id}/backups`, { page: 1, per_page: 100, access_token: accessToken });
      } catch (e) {
        return [e];
      }

      if (result.status !== 200) return [result];
      return [null, result.body.backups];
    },
    async listBackupSites(id) {
      let result;
      try {
        result = await fetcher.get(`${API_ORIGIN}/api/v1/apps/${id}/backup_sites`, { access_token: accessToken });
      } catch (e) {
        return [e];
      }

      if (result.status !== 200) return [result];
      return [null, result.body.backupSites];
    },

    async backup(id, backupSiteId) {
      let result;
      try {
        result = await fetcher.post(`${API_ORIGIN}/api/v1/apps/${id}/backup`, { backupSiteId }, { access_token: accessToken });
      } catch (e) {
        return [e];
      }

      if (result.status !== 202) return [result];
      return [null, result.body.taskId];
    },
    async updateBackup(id, backupId, label, preserveSecs) {
      let result;
      try {
        result = await fetcher.post(`${API_ORIGIN}/api/v1/apps/${id}/backups/${backupId}`, { label, preserveSecs }, { access_token: accessToken });
      } catch (e) {
        return [e];
      }

      if (result.status !== 200) return [result];
      return [null];
    },
    async restore(id, backupId) {
      let result;
      try {
        result = await fetcher.post(`${API_ORIGIN}/api/v1/apps/${id}/restore`, { backupId }, { access_token: accessToken });
      } catch (e) {
        return [e];
      }

      if (result.status !== 202) return [result];
      return [null];
    },
    async getMetrics(id, options) {
      let result;
      try {
        result = await fetcher.get(`${API_ORIGIN}/api/v1/apps/${id}/metrics`, { fromSecs: options.fromSecs, intervalSecs: options.intervalSecs, access_token: accessToken });
      } catch (e) {
        return [e];
      }

      if (result.status !== 200) return [result];
      return [null, result.body];
    },
    async getMetricStream(id) {
      return new EventSource(`${API_ORIGIN}/api/v1/apps/${id}/metricstream?access_token=${accessToken}`);
    },
    async repair(id, data) {
      let result;
      try {
        result = await fetcher.post(`${API_ORIGIN}/api/v1/apps/${id}/repair`, data, { access_token: accessToken });
      } catch (e) {
        return [e];
      }

      if (result.status !== 200 && result.status !== 202) return [result];
      return [null, result.body];
    },
    async ackChecklistItem(id, key, done) {
      let result;
      try {
        result = await fetcher.put(`${API_ORIGIN}/api/v1/apps/${id}/checklist/${key}`, { done }, { access_token: accessToken });
      } catch (e) {
        return [e];
      }

      if (result.status !== 202) return [result];
      return [null];
    },
    async clone(id, config) {
      const data = {
        subdomain: config.subdomain,
        domain: config.domain,
        secondaryDomains: config.secondaryDomains,
        ports: config.ports,
        backupId: config.backupId,
        overwriteDns: !!config.overwriteDns
      };

      let result;
      try {
        result = await fetcher.post(`${API_ORIGIN}/api/v1/apps/${id}/clone`, data, { access_token: accessToken });
      } catch (e) {
        return [e];
      }

      if (result.status !== 201) return [result];
      return [null, result.body];
    },
    async import(id, data) {
      let result;
      try {
        result = await fetcher.post(`${API_ORIGIN}/api/v1/apps/${id}/import`, data, { access_token: accessToken });
      } catch (e) {
        return [e];
      }

      if (result.status !== 202) return [result];
      return [null, result.body];
    },
  };
}

export default {
  create,
  installationStateLabel,
  installationActive,
  appProgressMessage,
  pendingChecklistItems,
};
