import Visibility from 'visibilityjs';
import Vue from 'vue';
import { GlToast } from '@gitlab/ui';
import AccessorUtilities from '~/lib/utils/accessor';
import PersistentUserCallout from '../persistent_user_callout';
import { s__, sprintf } from '../locale';
import Flash from '../flash';
import Poll from '../lib/utils/poll';
import initSettingsPanels from '../settings_panels';
import eventHub from './event_hub';
import {
  APPLICATION_STATUS,
  INGRESS,
  INGRESS_DOMAIN_SUFFIX,
  CROSSPLANE,
  KNATIVE,
  FLUENTD,
} from './constants';
import ClustersService from './services/clusters_service';
import ClustersStore from './stores/clusters_store';
import Applications from './components/applications.vue';
import RemoveClusterConfirmation from './components/remove_cluster_confirmation.vue';
import setupToggleButtons from '../toggle_buttons';
import initProjectSelectDropdown from '~/project_select';
import initServerlessSurveyBanner from '~/serverless/survey_banner';

const Environments = () => import('ee_component/clusters/components/environments.vue');

Vue.use(GlToast);

/**
 * Cluster page has 2 separate parts:
 * Toggle button and applications section
 *
 * - Polling status while creating or scheduled
 * - Update status area with the response result
 */

export default class Clusters {
  constructor() {
    const {
      statusPath,
      installHelmPath,
      installIngressPath,
      installCertManagerPath,
      installRunnerPath,
      installJupyterPath,
      installKnativePath,
      updateKnativePath,
      installElasticStackPath,
      installCrossplanePath,
      installPrometheusPath,
      installFluentdPath,
      managePrometheusPath,
      clusterEnvironmentsPath,
      hasRbac,
      providerType,
      preInstalledKnative,
      clusterType,
      clusterStatus,
      clusterStatusReason,
      helpPath,
      ingressHelpPath,
      ingressDnsHelpPath,
      ingressModSecurityHelpPath,
      environmentsHelpPath,
      clustersHelpPath,
      deployBoardsHelpPath,
      cloudRunHelpPath,
      clusterId,
    } = document.querySelector('.js-edit-cluster-form').dataset;

    this.clusterId = clusterId;
    this.clusterNewlyCreatedKey = `cluster_${this.clusterId}_newly_created`;
    this.clusterBannerDismissedKey = `cluster_${this.clusterId}_banner_dismissed`;

    this.store = new ClustersStore();
    this.store.setHelpPaths(
      helpPath,
      ingressHelpPath,
      ingressDnsHelpPath,
      ingressModSecurityHelpPath,
      environmentsHelpPath,
      clustersHelpPath,
      deployBoardsHelpPath,
      cloudRunHelpPath,
    );
    this.store.setManagePrometheusPath(managePrometheusPath);
    this.store.updateStatus(clusterStatus);
    this.store.updateStatusReason(clusterStatusReason);
    this.store.updateProviderType(providerType);
    this.store.updatePreInstalledKnative(preInstalledKnative);
    this.store.updateRbac(hasRbac);
    this.service = new ClustersService({
      endpoint: statusPath,
      installHelmEndpoint: installHelmPath,
      installIngressEndpoint: installIngressPath,
      installCertManagerEndpoint: installCertManagerPath,
      installCrossplaneEndpoint: installCrossplanePath,
      installRunnerEndpoint: installRunnerPath,
      installPrometheusEndpoint: installPrometheusPath,
      installJupyterEndpoint: installJupyterPath,
      installKnativeEndpoint: installKnativePath,
      updateKnativeEndpoint: updateKnativePath,
      installElasticStackEndpoint: installElasticStackPath,
      clusterEnvironmentsEndpoint: clusterEnvironmentsPath,
      installFluentdEndpoint: installFluentdPath,
    });

    this.installApplication = this.installApplication.bind(this);

    this.errorContainer = document.querySelector('.js-cluster-error');
    this.successContainer = document.querySelector('.js-cluster-success');
    this.creatingContainer = document.querySelector('.js-cluster-creating');
    this.unreachableContainer = document.querySelector('.js-cluster-api-unreachable');
    this.authenticationFailureContainer = document.querySelector(
      '.js-cluster-authentication-failure',
    );
    this.errorReasonContainer = this.errorContainer.querySelector('.js-error-reason');
    this.successApplicationContainer = document.querySelector('.js-cluster-application-notice');
    this.tokenField = document.querySelector('.js-cluster-token');
    this.ingressDomainHelpText = document.querySelector('.js-ingress-domain-help-text');
    this.ingressDomainSnippet =
      this.ingressDomainHelpText &&
      this.ingressDomainHelpText.querySelector('.js-ingress-domain-snippet');

    initProjectSelectDropdown();
    Clusters.initDismissableCallout();
    initSettingsPanels();

    const toggleButtonsContainer = document.querySelector('.js-cluster-enable-toggle-area');
    if (toggleButtonsContainer) {
      setupToggleButtons(toggleButtonsContainer);
    }
    this.initApplications(clusterType);
    this.initEnvironments();

    if (clusterEnvironmentsPath && this.environments) {
      this.store.toggleFetchEnvironments(true);

      this.initPolling(
        'fetchClusterEnvironments',
        data => this.handleClusterEnvironmentsSuccess(data),
        () => this.handleEnvironmentsPollError(),
      );
    }

    this.updateContainer(null, this.store.state.status, this.store.state.statusReason);

    this.addListeners();
    if (statusPath && !this.environments) {
      this.initPolling(
        'fetchClusterStatus',
        data => this.handleClusterStatusSuccess(data),
        () => this.handlePollError(),
      );
    }

    this.initRemoveClusterActions();
  }

  initApplications(type) {
    const { store } = this;
    const el = document.querySelector('#js-cluster-applications');

    this.applications = new Vue({
      el,
      data() {
        return {
          state: store.state,
        };
      },
      render(createElement) {
        return createElement(Applications, {
          props: {
            type,
            applications: this.state.applications,
            helpPath: this.state.helpPath,
            ingressHelpPath: this.state.ingressHelpPath,
            managePrometheusPath: this.state.managePrometheusPath,
            ingressDnsHelpPath: this.state.ingressDnsHelpPath,
            ingressModSecurityHelpPath: this.state.ingressModSecurityHelpPath,
            cloudRunHelpPath: this.state.cloudRunHelpPath,
            providerType: this.state.providerType,
            preInstalledKnative: this.state.preInstalledKnative,
            rbac: this.state.rbac,
          },
        });
      },
    });
  }

  initEnvironments() {
    const { store } = this;
    const el = document.querySelector('#js-cluster-environments');

    if (!el) {
      return;
    }

    this.environments = new Vue({
      el,
      data() {
        return {
          state: store.state,
        };
      },
      render(createElement) {
        return createElement(Environments, {
          props: {
            isFetching: this.state.fetchingEnvironments,
            environments: this.state.environments,
            environmentsHelpPath: this.state.environmentsHelpPath,
            clustersHelpPath: this.state.clustersHelpPath,
            deployBoardsHelpPath: this.state.deployBoardsHelpPath,
          },
        });
      },
    });
  }

  initRemoveClusterActions() {
    const el = document.querySelector('#js-cluster-remove-actions');
    if (el && el.dataset) {
      const { clusterName, clusterPath } = el.dataset;

      this.removeClusterAction = new Vue({
        el,
        render(createElement) {
          return createElement(RemoveClusterConfirmation, {
            props: {
              clusterName,
              clusterPath,
            },
          });
        },
      });
    }
  }

  handleClusterEnvironmentsSuccess(data) {
    this.store.toggleFetchEnvironments(false);
    this.store.updateEnvironments(data.data);
  }

  static initDismissableCallout() {
    const callout = document.querySelector('.js-cluster-security-warning');
    PersistentUserCallout.factory(callout);
  }

  addBannerCloseHandler(el, status) {
    el.querySelector('.js-close-banner').addEventListener('click', () => {
      el.classList.add('hidden');
      this.setBannerDismissedState(status, true);
    });
  }

  addListeners() {
    eventHub.$on('installApplication', this.installApplication);
    eventHub.$on('updateApplication', data => this.updateApplication(data));
    eventHub.$on('saveKnativeDomain', data => this.saveKnativeDomain(data));
    eventHub.$on('setKnativeDomain', data => this.setKnativeDomain(data));
    eventHub.$on('uninstallApplication', data => this.uninstallApplication(data));
    eventHub.$on('setCrossplaneProviderStack', data => this.setCrossplaneProviderStack(data));
    eventHub.$on('setIngressModSecurityEnabled', data => this.setIngressModSecurityEnabled(data));
    eventHub.$on('setIngressModSecurityMode', data => this.setIngressModSecurityMode(data));
    eventHub.$on('resetIngressModSecurityChanges', id => this.resetIngressModSecurityChanges(id));
    eventHub.$on('setFluentdSettings', data => this.setFluentdSettings(data));
    // Add event listener to all the banner close buttons
    this.addBannerCloseHandler(this.unreachableContainer, 'unreachable');
    this.addBannerCloseHandler(this.authenticationFailureContainer, 'authentication_failure');
  }

  removeListeners() {
    eventHub.$off('installApplication', this.installApplication);
    eventHub.$off('updateApplication', this.updateApplication);
    eventHub.$off('saveKnativeDomain');
    eventHub.$off('setKnativeDomain');
    eventHub.$off('setCrossplaneProviderStack');
    eventHub.$off('uninstallApplication');
    eventHub.$off('setIngressModSecurityEnabled');
    eventHub.$off('setIngressModSecurityMode');
    eventHub.$off('resetIngressModSecurityChanges');
    eventHub.$off('setFluentdSettings');
  }

  initPolling(method, successCallback, errorCallback) {
    this.poll = new Poll({
      resource: this.service,
      method,
      successCallback,
      errorCallback,
    });

    if (!Visibility.hidden()) {
      this.poll.makeRequest();
    }

    Visibility.change(() => {
      if (!Visibility.hidden() && !this.destroyed) {
        this.poll.restart();
      } else {
        this.poll.stop();
      }
    });
  }

  handlePollError() {
    this.constructor.handleError();
  }

  handleEnvironmentsPollError() {
    this.store.toggleFetchEnvironments(false);

    this.handlePollError();
  }

  static handleError() {
    Flash(s__('ClusterIntegration|Something went wrong on our end.'));
  }

  handleClusterStatusSuccess(data) {
    const prevStatus = this.store.state.status;
    const prevApplicationMap = { ...this.store.state.applications };

    this.store.updateStateFromServer(data.data);

    this.checkForNewInstalls(prevApplicationMap, this.store.state.applications);
    this.updateContainer(prevStatus, this.store.state.status, this.store.state.statusReason);

    if (this.ingressDomainHelpText) {
      this.toggleIngressDomainHelpText(
        prevApplicationMap[INGRESS],
        this.store.state.applications[INGRESS],
      );
    }

    if (this.store.state.applications[KNATIVE]?.status === APPLICATION_STATUS.INSTALLED) {
      initServerlessSurveyBanner();
    }
  }

  hideAll() {
    this.errorContainer.classList.add('hidden');
    this.successContainer.classList.add('hidden');
    this.creatingContainer.classList.add('hidden');
    this.unreachableContainer.classList.add('hidden');
    this.authenticationFailureContainer.classList.add('hidden');
  }

  checkForNewInstalls(prevApplicationMap, newApplicationMap) {
    const appTitles = Object.keys(newApplicationMap)
      .filter(
        appId =>
          newApplicationMap[appId].status === APPLICATION_STATUS.INSTALLED &&
          prevApplicationMap[appId].status !== APPLICATION_STATUS.INSTALLED &&
          prevApplicationMap[appId].status !== null,
      )
      .map(appId => newApplicationMap[appId].title);

    if (appTitles.length > 0) {
      const text = sprintf(
        s__('ClusterIntegration|%{appList} was successfully installed on your Kubernetes cluster'),
        {
          appList: appTitles.join(', '),
        },
      );
      Flash(text, 'notice', this.successApplicationContainer);
    }
  }

  setBannerDismissedState(status, isDismissed) {
    if (AccessorUtilities.isLocalStorageAccessSafe()) {
      window.localStorage.setItem(this.clusterBannerDismissedKey, `${status}_${isDismissed}`);
    }
  }

  isBannerDismissed(status) {
    let bannerState;
    if (AccessorUtilities.isLocalStorageAccessSafe()) {
      bannerState = window.localStorage.getItem(this.clusterBannerDismissedKey);
    }

    return bannerState === `${status}_true`;
  }

  setClusterNewlyCreated(state) {
    if (AccessorUtilities.isLocalStorageAccessSafe()) {
      window.localStorage.setItem(this.clusterNewlyCreatedKey, Boolean(state));
    }
  }

  isClusterNewlyCreated() {
    // once this is true, it will always be true for a given page load
    if (!this.isNewlyCreated) {
      let newlyCreated;
      if (AccessorUtilities.isLocalStorageAccessSafe()) {
        newlyCreated = window.localStorage.getItem(this.clusterNewlyCreatedKey);
      }

      this.isNewlyCreated = newlyCreated === 'true';
    }
    return this.isNewlyCreated;
  }

  updateContainer(prevStatus, status, error) {
    if (status !== 'created' && this.isBannerDismissed(status)) {
      return;
    }
    this.setBannerDismissedState(status, false);

    if (prevStatus !== status) {
      this.hideAll();

      switch (status) {
        case 'created':
          if (this.isClusterNewlyCreated()) {
            this.setClusterNewlyCreated(false);
            this.successContainer.classList.remove('hidden');
          } else if (prevStatus) {
            this.setClusterNewlyCreated(true);
            window.location.reload();
          }
          break;
        case 'errored':
          this.errorContainer.classList.remove('hidden');
          this.errorReasonContainer.textContent = error;
          break;
        case 'unreachable':
          this.unreachableContainer.classList.remove('hidden');
          break;
        case 'authentication_failure':
          this.authenticationFailureContainer.classList.remove('hidden');
          break;
        case 'scheduled':
        case 'creating':
          this.creatingContainer.classList.remove('hidden');
          break;
        default:
      }
    }
  }

  installApplication({ id: appId, params }) {
    return Clusters.validateInstallation(appId, params)
      .then(() => {
        this.store.updateAppProperty(appId, 'requestReason', null);
        this.store.updateAppProperty(appId, 'statusReason', null);
        this.store.installApplication(appId);

        // eslint-disable-next-line promise/no-nesting
        this.service.installApplication(appId, params).catch(() => {
          this.store.notifyInstallFailure(appId);
          this.store.updateAppProperty(
            appId,
            'requestReason',
            s__('ClusterIntegration|Request to begin installing failed'),
          );
        });
      })
      .catch(error => this.store.updateAppProperty(appId, 'validationError', error));
  }

  static validateInstallation(appId, params) {
    return new Promise((resolve, reject) => {
      if (appId === CROSSPLANE && !params.stack) {
        reject(s__('ClusterIntegration|Select a stack to install Crossplane.'));
        return;
      }

      if (appId === KNATIVE && !params.hostname && !params.pages_domain_id) {
        reject(s__('ClusterIntegration|You must specify a domain before you can install Knative.'));
        return;
      }

      resolve();
    });
  }

  uninstallApplication({ id: appId }) {
    this.store.updateAppProperty(appId, 'requestReason', null);
    this.store.updateAppProperty(appId, 'statusReason', null);

    this.store.uninstallApplication(appId);

    return this.service.uninstallApplication(appId).catch(() => {
      this.store.notifyUninstallFailure(appId);
      this.store.updateAppProperty(
        appId,
        'requestReason',
        s__('ClusterIntegration|Request to begin uninstalling failed'),
      );
    });
  }

  updateApplication({ id: appId, params }) {
    this.store.updateApplication(appId);
    this.service.installApplication(appId, params).catch(() => {
      this.store.notifyUpdateFailure(appId);
    });
  }

  setFluentdSettings(settings = {}) {
    Object.entries(settings).forEach(([key, value]) => {
      this.store.updateAppProperty(FLUENTD, key, value);
    });
  }

  toggleIngressDomainHelpText({ externalIp }, { externalIp: newExternalIp }) {
    if (externalIp !== newExternalIp) {
      this.ingressDomainHelpText.classList.toggle('hide', !newExternalIp);
      this.ingressDomainSnippet.textContent = `${newExternalIp}${INGRESS_DOMAIN_SUFFIX}`;
    }
  }

  saveKnativeDomain(data) {
    const appId = data.id;
    this.store.updateApplication(appId);
    this.service.updateApplication(appId, data.params).catch(() => {
      this.store.notifyUpdateFailure(appId);
    });
  }

  setKnativeDomain({ id: appId, domain, domainId }) {
    this.store.updateAppProperty(appId, 'isEditingDomain', true);
    this.store.updateAppProperty(appId, 'hostname', domain);
    this.store.updateAppProperty(appId, 'pagesDomain', domainId ? { id: domainId, domain } : null);
    this.store.updateAppProperty(appId, 'validationError', null);
  }

  setCrossplaneProviderStack(data) {
    const appId = data.id;
    this.store.updateAppProperty(appId, 'stack', data.stack.code);
    this.store.updateAppProperty(appId, 'validationError', null);
  }

  setIngressModSecurityEnabled({ id, modSecurityEnabled }) {
    this.store.updateAppProperty(id, 'isEditingModSecurityEnabled', true);
    this.store.updateAppProperty(id, 'modsecurity_enabled', modSecurityEnabled);
  }

  setIngressModSecurityMode({ id, modSecurityMode }) {
    this.store.updateAppProperty(id, 'isEditingModSecurityMode', true);
    this.store.updateAppProperty(id, 'modsecurity_mode', modSecurityMode);
  }

  resetIngressModSecurityChanges(id) {
    this.store.updateAppProperty(id, 'isEditingModSecurityEnabled', false);
    this.store.updateAppProperty(id, 'isEditingModSecurityMode', false);
  }

  destroy() {
    this.destroyed = true;

    this.removeListeners();

    if (this.poll) {
      this.poll.stop();
    }

    if (this.environments) {
      this.environments.$destroy();
    }

    this.applications.$destroy();
  }
}
