<script>
import { GlLoadingIcon, GlModal } from '@gitlab/ui';
import { fetchPolicies } from '~/lib/graphql';
import { mergeUrlParams, queryToObject, redirectTo } from '~/lib/utils/url_utility';
import { __, s__ } from '~/locale';

import { unwrapStagesWithNeeds } from '~/pipelines/components/unwrapping_utils';

import ConfirmUnsavedChangesDialog from './components/ui/confirm_unsaved_changes_dialog.vue';
import PipelineEditorEmptyState from './components/ui/pipeline_editor_empty_state.vue';
import PipelineEditorMessages from './components/ui/pipeline_editor_messages.vue';
import {
  COMMIT_SHA_POLL_INTERVAL,
  COMMIT_SUCCESS_WITH_REDIRECT,
  EDITOR_APP_STATUS_EMPTY,
  EDITOR_APP_STATUS_LOADING,
  EDITOR_APP_STATUS_LINT_UNAVAILABLE,
  EDITOR_APP_VALID_STATUSES,
  LOAD_FAILURE_UNKNOWN,
  STARTER_TEMPLATE_NAME,
} from './constants';
import updateAppStatus from './graphql/mutations/client/update_app_status.mutation.graphql';
import getBlobContent from './graphql/queries/blob_content.query.graphql';
import getCiConfigData from './graphql/queries/ci_config.query.graphql';
import getAppStatus from './graphql/queries/client/app_status.query.graphql';
import getCurrentBranch from './graphql/queries/client/current_branch.query.graphql';
import getTemplate from './graphql/queries/get_starter_template.query.graphql';
import getLatestCommitShaQuery from './graphql/queries/latest_commit_sha.query.graphql';
import PipelineEditorHome from './pipeline_editor_home.vue';

const MR_SOURCE_BRANCH = 'merge_request[source_branch]';
const MR_TARGET_BRANCH = 'merge_request[target_branch]';

export default {
  components: {
    ConfirmUnsavedChangesDialog,
    GlLoadingIcon,
    GlModal,
    PipelineEditorEmptyState,
    PipelineEditorHome,
    PipelineEditorMessages,
  },
  inject: ['ciConfigPath', 'newMergeRequestPath', 'projectFullPath', 'usesExternalConfig'],
  data() {
    return {
      ciConfigData: {},
      currentCiFileContent: '',
      failureType: null,
      failureReasons: [],
      hasBranchLoaded: false,
      initialCiFileContent: '',
      isFetchingCommitSha: false,
      isLintUnavailable: false,
      isNewCiConfigFile: false,
      lastCommittedContent: '',
      shouldSkipStartScreen: false,
      showFailure: false,
      showResetConfirmationModal: false,
      showStartScreen: false,
      showSuccess: false,
      starterTemplate: '',
      starterTemplateName: STARTER_TEMPLATE_NAME,
      successType: null,
    };
  },
  apollo: {
    initialCiFileContent: {
      fetchPolicy: fetchPolicies.NETWORK_ONLY,
      query: getBlobContent,
      // If it's a brand new file, we don't want to fetch the content.
      // Then when the user commits the first time, the query would run
      // to get the initial file content, but we already have it in `lastCommitedContent`
      // so we skip the loading altogether. We also wait for the currentBranch
      // to have been fetched
      skip() {
        return this.shouldSkipBlobContentQuery;
      },
      variables() {
        return {
          projectPath: this.projectFullPath,
          path: this.ciConfigPath,
          ref: this.currentBranch,
        };
      },
      update(data) {
        return data?.project?.repository?.blobs?.nodes[0]?.rawBlob;
      },
      result({ data }) {
        const nodes = data?.project?.repository?.blobs?.nodes;
        if (!nodes) {
          this.reportFailure(LOAD_FAILURE_UNKNOWN);
        } else {
          const rawBlob = nodes[0]?.rawBlob;
          const fileContent = rawBlob ?? '';

          this.lastCommittedContent = fileContent;
          this.currentCiFileContent = fileContent;

          // If rawBlob is defined and returns a string, it means that there is
          // a CI config file with empty content. If `rawBlob` is not defined
          // at all, it means there was no file found.
          const hasCIFile = rawBlob === '' || fileContent.length > 0;

          if (!fileContent.length) {
            this.setAppStatus(EDITOR_APP_STATUS_EMPTY);
          }

          this.isNewCiConfigFile = false;
          if (!hasCIFile) {
            if (this.shouldSkipStartScreen) {
              this.setNewEmptyCiConfigFile();
            } else {
              this.showStartScreen = true;
            }
          } else if (fileContent.length) {
            // If the file content is > 0, then we make sure to reset the
            // start screen flag during a refetch
            // e.g. when switching branches
            this.showStartScreen = false;
          }
        }
      },
      error() {
        this.reportFailure(LOAD_FAILURE_UNKNOWN);
      },
      watchLoading(isLoading) {
        if (isLoading) {
          this.setAppStatus(EDITOR_APP_STATUS_LOADING);
        }
      },
    },
    ciConfigData: {
      query: getCiConfigData,
      skip() {
        return this.shouldSkipCiConfigQuery;
      },
      variables() {
        return {
          projectPath: this.projectFullPath,
          sha: this.commitSha,
          content: this.currentCiFileContent,
        };
      },
      update(data) {
        const { ciConfig } = data || {};
        const stageNodes = ciConfig?.stages?.nodes || [];
        const stages = unwrapStagesWithNeeds(JSON.parse(JSON.stringify(stageNodes)));

        return { ...ciConfig, stages };
      },
      result({ data }) {
        if (data?.ciConfig?.status) {
          this.setAppStatus(data.ciConfig.status);
          if (this.isLintUnavailable) {
            this.isLintUnavailable = false;
          }
        }
      },
      error() {
        // We are not using `reportFailure` here because we don't
        // need to bring attention to the linter being down. We let
        // the user work on their file and if they look at their
        // lint status, they will notice that the service is down
        this.isLintUnavailable = true;
      },
      watchLoading(isLoading) {
        if (isLoading) {
          this.setAppStatus(EDITOR_APP_STATUS_LOADING);
        }
      },
    },
    appStatus: {
      query: getAppStatus,
      update(data) {
        return data.app.status;
      },
    },
    commitSha: {
      query: getLatestCommitShaQuery,
      skip({ currentBranch }) {
        return !currentBranch;
      },
      variables() {
        return {
          projectPath: this.projectFullPath,
          ref: this.currentBranch,
        };
      },
      update(data) {
        const latestCommitSha = data?.project?.repository?.tree?.lastCommit?.sha;

        if (this.isFetchingCommitSha && latestCommitSha === this.commitSha) {
          this.$apollo.queries.commitSha.startPolling(COMMIT_SHA_POLL_INTERVAL);
          return this.commitSha;
        }

        this.isFetchingCommitSha = false;
        this.$apollo.queries.commitSha.stopPolling();
        return latestCommitSha;
      },
      error() {
        this.reportFailure(LOAD_FAILURE_UNKNOWN);
      },
    },
    currentBranch: {
      query: getCurrentBranch,
      update(data) {
        return data.workBranches?.current?.name;
      },
    },
    starterTemplate: {
      query: getTemplate,
      variables() {
        return {
          projectPath: this.projectFullPath,
          templateName: this.starterTemplateName,
        };
      },
      skip({ isNewCiConfigFile }) {
        return !isNewCiConfigFile;
      },
      update(data) {
        return data.project?.ciTemplate?.content || '';
      },
      result({ data }) {
        this.updateCiConfig(data?.project?.ciTemplate?.content || '');
      },
      error() {
        this.reportFailure(LOAD_FAILURE_UNKNOWN);
      },
    },
  },
  computed: {
    hasUnsavedChanges() {
      return this.lastCommittedContent !== this.currentCiFileContent;
    },
    isBlobContentLoading() {
      return !this.hasBranchLoaded || this.$apollo.queries.initialCiFileContent.loading;
    },
    isCiConfigDataLoading() {
      return this.$apollo.queries.ciConfigData.loading;
    },
    isEmpty() {
      return this.currentCiFileContent === '';
    },
    shouldSkipBlobContentQuery() {
      return this.isNewCiConfigFile || this.lastCommittedContent || !this.hasBranchLoaded;
    },
    shouldSkipCiConfigQuery() {
      return !this.currentCiFileContent || !this.commitSha;
    },
  },
  i18n: {
    resetModal: {
      actionPrimary: {
        text: __('Reset file'),
      },
      actionCancel: {
        text: __('Cancel'),
      },
      body: s__(
        'Pipeline Editor|Are you sure you want to reset the file to its last committed version?',
      ),
      title: __('Discard changes'),
    },
  },
  watch: {
    currentBranch: {
      immediate: true,
      handler(branch) {
        // currentBranch is a client query so it starts off undefined. In the index.js,
        // write to the apollo cache. Once that operation is done, we can safely do operations
        // that require the branch to have loaded.
        if (branch) {
          this.hasBranchLoaded = true;
        }
      },
    },
    isEmpty(flag) {
      if (flag) {
        this.setAppStatus(EDITOR_APP_STATUS_EMPTY);
      }
    },
    isLintUnavailable(flag) {
      if (flag) {
        // We cannot set this status directly in the `error`
        // hook otherwise we get an infinite loop caused by apollo.
        this.setAppStatus(EDITOR_APP_STATUS_LINT_UNAVAILABLE);
      }
    },
  },
  mounted() {
    this.loadTemplateFromURL();
    this.checkShouldSkipStartScreen();
  },
  methods: {
    checkShouldSkipStartScreen() {
      const params = queryToObject(window.location.search);
      this.shouldSkipStartScreen = Boolean(params?.add_new_config_file);
    },
    confirmReset() {
      if (this.hasUnsavedChanges) {
        this.showResetConfirmationModal = true;
      }
    },
    hideFailure() {
      this.showFailure = false;
    },
    hideSuccess() {
      this.showSuccess = false;
    },
    loadTemplateFromURL() {
      const templateName = queryToObject(window.location.search)?.template;

      if (templateName) {
        this.starterTemplateName = templateName;
        this.setNewEmptyCiConfigFile();
      }
    },
    redirectToNewMergeRequest(sourceBranch, targetBranch) {
      const url = mergeUrlParams(
        {
          [MR_SOURCE_BRANCH]: sourceBranch,
          [MR_TARGET_BRANCH]: targetBranch,
        },
        this.newMergeRequestPath,
      );
      redirectTo(url);
    },
    async refetchContent() {
      this.$apollo.queries.initialCiFileContent.skip = false;
      await this.$apollo.queries.initialCiFileContent.refetch();
    },
    reportFailure(type, reasons = []) {
      this.showFailure = true;
      this.failureType = type;
      this.failureReasons = reasons;
      window.scrollTo({ top: 0, behavior: 'smooth' });
    },
    reportSuccess(type) {
      window.scrollTo({ top: 0, behavior: 'smooth' });
      this.showSuccess = true;
      this.successType = type;
    },
    resetContent() {
      this.showResetConfirmationModal = false;
      this.currentCiFileContent = this.lastCommittedContent;
    },
    setAppStatus(appStatus) {
      if (EDITOR_APP_VALID_STATUSES.includes(appStatus)) {
        this.$apollo.mutate({
          mutation: updateAppStatus,
          variables: { appStatus },
        });
      }
    },
    setNewEmptyCiConfigFile() {
      this.isNewCiConfigFile = true;
      this.showStartScreen = false;
    },
    showErrorAlert({ type, reasons = [] }) {
      this.reportFailure(type, reasons);
    },
    updateCiConfig(ciFileContent) {
      this.currentCiFileContent = ciFileContent;
    },
    updateCommitSha() {
      this.isFetchingCommitSha = true;
      this.$apollo.queries.commitSha.refetch();
    },
    async updateOnCommit({ type, params = {} }) {
      this.reportSuccess(type);

      if (this.isNewCiConfigFile) {
        this.isNewCiConfigFile = false;
      }

      // Keep track of the latest committed content to know
      // if the user has made changes to the file that are unsaved.
      this.lastCommittedContent = this.currentCiFileContent;

      if (type === COMMIT_SUCCESS_WITH_REDIRECT) {
        const { sourceBranch, targetBranch } = params;
        // This force update does 2 things for us:
        // 1. It make sure `hasUnsavedChanges` is updated so
        // we don't show a modal when the user creates an MR
        // 2. Ensure the commit success banner is visible.
        await this.$forceUpdate();
        this.redirectToNewMergeRequest(sourceBranch, targetBranch);
      }
    },
  },
};
</script>

<template>
  <div class="gl-mt-4 gl-relative" data-qa-selector="pipeline_editor_app">
    <gl-loading-icon v-if="isBlobContentLoading" size="lg" class="gl-m-3" />
    <pipeline-editor-empty-state
      v-else-if="showStartScreen || usesExternalConfig"
      @createEmptyConfigFile="setNewEmptyCiConfigFile"
      @refetchContent="refetchContent"
    />
    <div v-else>
      <pipeline-editor-messages
        :failure-type="failureType"
        :failure-reasons="failureReasons"
        :show-failure="showFailure"
        :show-success="showSuccess"
        :success-type="successType"
        @hide-success="hideSuccess"
        @hide-failure="hideFailure"
      />
      <pipeline-editor-home
        :ci-config-data="ciConfigData"
        :ci-file-content="currentCiFileContent"
        :commit-sha="commitSha"
        :has-unsaved-changes="hasUnsavedChanges"
        :is-new-ci-config-file="isNewCiConfigFile"
        @commit="updateOnCommit"
        @resetContent="confirmReset"
        @showError="showErrorAlert"
        @refetchContent="refetchContent"
        @updateCiConfig="updateCiConfig"
        @updateCommitSha="updateCommitSha"
      />
      <gl-modal
        v-model="showResetConfirmationModal"
        modal-id="reset-content"
        :title="$options.i18n.resetModal.title"
        :action-cancel="$options.i18n.resetModal.actionCancel"
        :action-primary="$options.i18n.resetModal.actionPrimary"
        @primary="resetContent"
      >
        {{ $options.i18n.resetModal.body }}
      </gl-modal>
      <confirm-unsaved-changes-dialog :has-unsaved-changes="hasUnsavedChanges" />
    </div>
  </div>
</template>
