<script>
import {
  GlAlert,
  GlButton,
  GlDrawer,
  GlFormCheckbox,
  GlFormGroup,
  GlFormInput,
  GlFormSelect,
} from '@gitlab/ui';
import { get as getPropValueByPath, isEmpty } from 'lodash';
import { produce } from 'immer';
import { MountingPortal } from 'portal-vue';
import { __ } from '~/locale';
import { logError } from '~/lib/logger';
import { getFirstPropertyValue } from '~/lib/utils/common_utils';
import { INDEX_ROUTE_NAME } from '../constants';

const MSG_SAVE_CHANGES = __('Save changes');
const MSG_ERROR = __('Something went wrong. Please try again.');
const MSG_OPTIONAL = __('(optional)');
const MSG_CANCEL = __('Cancel');

/**
 * This component is a first iteration towards a general reusable Create/Update component
 *
 * There's some opportunity to improve cohesion of this module which we are planning
 * to address after solidifying the abstraction's requirements.
 *
 * Please see https://gitlab.com/gitlab-org/gitlab/-/issues/349441
 */
export default {
  components: {
    GlAlert,
    GlButton,
    GlDrawer,
    GlFormCheckbox,
    GlFormGroup,
    GlFormInput,
    GlFormSelect,
    MountingPortal,
  },
  props: {
    drawerOpen: {
      type: Boolean,
      required: true,
    },
    fields: {
      type: Array,
      required: true,
    },
    title: {
      type: String,
      required: true,
    },
    successMessage: {
      type: String,
      required: true,
    },
    mutation: {
      type: Object,
      required: true,
    },
    getQuery: {
      type: Object,
      required: false,
      default: null,
    },
    getQueryNodePath: {
      type: String,
      required: false,
      default: null,
    },
    additionalCreateParams: {
      type: Object,
      required: false,
      default: () => ({}),
    },
    buttonLabel: {
      type: String,
      required: false,
      default: () => MSG_SAVE_CHANGES,
    },
    existingId: {
      type: String,
      required: false,
      default: null,
    },
  },
  data() {
    return {
      model: null,
      submitting: false,
      errorMessages: [],
      records: [],
      loading: true,
    };
  },
  apollo: {
    records: {
      query() {
        return this.getQuery.query;
      },
      variables() {
        return this.getQuery.variables;
      },
      update(data) {
        this.records = getPropValueByPath(data, this.getQueryNodePath).nodes || [];
        this.setInitialModel();
        this.loading = false;
      },
      error() {
        this.errorMessages = [MSG_ERROR];
      },
    },
  },
  computed: {
    isEditMode() {
      return this.existingId;
    },
    isInvalid() {
      const { fields, model } = this;

      return fields.some((field) => {
        return (
          field.required && isEmpty(model[field.name]) && typeof model[field.name] !== 'boolean'
        );
      });
    },
    variables() {
      const { additionalCreateParams, fields, isEditMode, model } = this;

      const variables = fields.reduce(
        (map, field) =>
          Object.assign(map, {
            [field.name]: this.formatValue(model, field),
          }),
        {},
      );

      if (isEditMode) {
        return { input: { id: this.existingId, ...variables } };
      }

      return { input: { ...additionalCreateParams, ...variables } };
    },
  },
  methods: {
    setInitialModel() {
      const existingModel = this.records.find(({ id }) => id === this.existingId);
      const noModel = !this.isEditMode || !existingModel;

      this.model = this.fields.reduce(
        (map, field) =>
          Object.assign(map, {
            [field.name]: noModel ? null : this.extractValue(existingModel, field.name),
          }),
        {},
      );
    },
    extractValue(existingModel, fieldName) {
      const value = existingModel[fieldName];
      if (value != null) return value;

      /* eslint-disable-next-line @gitlab/require-i18n-strings */
      if (!fieldName.endsWith('Id')) return null;

      return existingModel[fieldName.slice(0, -2)]?.id;
    },
    formatValue(model, field) {
      if (!isEmpty(model[field.name]) && field.input?.type === 'number') {
        return parseFloat(model[field.name]);
      }

      return model[field.name];
    },
    save() {
      const { mutation, variables, close } = this;

      this.submitting = true;

      return this.$apollo
        .mutate({
          mutation,
          variables,
          update: (store, { data }) => {
            const { errors, ...result } = getFirstPropertyValue(data);

            if (errors?.length) {
              this.errorMessages = errors;
            } else {
              this.updateCache(store, result);
              close(true);
            }
          },
        })
        .catch((e) => {
          logError(e);
          this.errorMessages = [MSG_ERROR];
        })
        .finally(() => {
          this.submitting = false;
        });
    },
    close(success) {
      if (success) {
        // This is needed so toast perists when route is changed
        this.$root.$toast.show(this.successMessage);
      }

      this.$router.replace({ name: this.$options.INDEX_ROUTE_NAME });
    },
    updateCache(store, result) {
      const { getQuery, isEditMode, getQueryNodePath } = this;

      if (isEditMode || !getQuery) return;

      const sourceData = store.readQuery(getQuery);

      const newData = produce(sourceData, (draftState) => {
        getPropValueByPath(draftState, getQueryNodePath).nodes.push(this.getPayload(result));
      });

      store.writeQuery({
        ...getQuery,
        data: newData,
      });
    },
    getFieldLabel(field) {
      if (field.bool) return null;

      const optionalSuffix = field.required ? '' : ` ${MSG_OPTIONAL}`;
      return field.label + optionalSuffix;
    },
    getPayload(data) {
      if (!data) return null;

      const keys = Object.keys(data);
      if (keys[0] === '__typename') return data[keys[1]];

      return data[keys[0]];
    },
    getDrawerHeaderHeight() {
      const wrapperEl = document.querySelector('.content-wrapper');

      if (wrapperEl) {
        return `${wrapperEl.offsetTop}px`;
      }

      return '';
    },
  },
  MSG_CANCEL,
  INDEX_ROUTE_NAME,
};
</script>

<template>
  <mounting-portal v-if="!loading" mount-to="#js-crm-form-portal" append>
    <gl-drawer
      :header-height="getDrawerHeaderHeight()"
      class="gl-drawer-responsive"
      :open="drawerOpen"
      @close="close(false)"
    >
      <template #title>
        <h3>{{ title }}</h3>
      </template>
      <gl-alert v-if="errorMessages.length" variant="danger" @dismiss="errorMessages = []">
        <ul class="gl-mb-0! gl-ml-5">
          <li v-for="error in errorMessages" :key="error">
            {{ error }}
          </li>
        </ul>
      </gl-alert>
      <form @submit.prevent="save">
        <gl-form-group
          v-for="field in fields"
          :key="field.name"
          :label="getFieldLabel(field)"
          :label-for="field.name"
        >
          <gl-form-select
            v-if="field.values"
            :id="field.name"
            v-model="model[field.name]"
            :options="field.values"
          />
          <gl-form-checkbox v-else-if="field.bool" :id="field.name" v-model="model[field.name]"
            ><span class="gl-font-weight-bold">{{ field.label }}</span></gl-form-checkbox
          >
          <gl-form-input v-else :id="field.name" v-bind="field.input" v-model="model[field.name]" />
        </gl-form-group>
        <span class="gl-float-right">
          <gl-button data-testid="cancel-button" @click="close(false)">
            {{ $options.MSG_CANCEL }}
          </gl-button>
          <gl-button
            variant="confirm"
            :disabled="isInvalid"
            :loading="submitting"
            data-testid="save-button"
            type="submit"
            >{{ buttonLabel }}</gl-button
          >
        </span>
      </form>
    </gl-drawer>
  </mounting-portal>
</template>
