<template>
  <Modal
    :right-sidebar="!isTableCreation"
    :right-sidebar-scrollable="true"
    :close-button="false"
    :content-scrollable="true"
    @show=";[(importer = ''), reset()]"
    @hide="stopPollIfRunning()"
  >
    <template #content>
      <div class="import-modal__header">
        <h2 class="import-modal__title">
          {{
            isTableCreation
              ? $t('importFileModal.title')
              : $t('importFileModal.additionalImportTitle', {
                  table: table.name,
                })
          }}
        </h2>
        <div class="modal__actions">
          <a v-if="isTableCreation" class="modal__close" @click="hide()">
            <i class="iconoir-cancel"></i>
          </a>
        </div>
      </div>

      <div class="control">
        <label class="control__label">
          {{ $t('importFileModal.importLabel') }}
        </label>
        <div class="control__elements">
          <ul class="choice-items">
            <li v-if="isTableCreation">
              <a
                class="choice-items__link"
                :class="{ active: importer === '' }"
                @click=";[(importer = ''), reset()]"
              >
                <i class="choice-items__icon iconoir-copy"></i>
                <span>{{ $t('importFileModal.newTable') }}</span>
                <i
                  v-if="importer === ''"
                  class="choice-items__icon-active iconoir-check-circle"
                ></i>
              </a>
            </li>
            <li v-for="importerType in importerTypes" :key="importerType.type">
              <a
                class="choice-items__link"
                :class="{ active: importer === importerType.type }"
                @click=";[(importer = importerType.type), reset()]"
              >
                <i
                  class="choice-items__icon"
                  :class="importerType.iconClass"
                ></i>
                <span> {{ importerType.getName() }}</span>
                <i
                  v-if="importer === importerType.type"
                  class="choice-items__icon-active iconoir-check-circle"
                ></i>
              </a>
            </li>
          </ul>
        </div>
      </div>

      <TableForm
        ref="tableForm"
        :default-name="getDefaultName()"
        :creation="isTableCreation"
        @submitted="submitted"
      >
        <component
          :is="importerComponent"
          :disabled="importInProgress"
          @changed="reset()"
          @header="onHeader($event)"
          @data="onData($event)"
          @getData="onGetData($event)"
        />
      </TableForm>

      <Error :error="error"></Error>
      <Alert v-if="errorReport.length > 0 && error.visible" type="warning">
        <template #title>{{
          $t('importFileModal.reportTitleFailure')
        }}</template>

        {{ $t('importFileModal.reportMessage') }} {{ errorReport.join(', ') }}
      </Alert>
      <Alert v-if="errorReport.length > 0 && !error.visible" type="warning">
        <template #title>
          {{ $t('importFileModal.reportTitleSuccess') }}</template
        >

        {{ $t('importFileModal.reportMessage') }}
        {{ errorReport.join(', ') }}
      </Alert>

      <Tabs v-if="dataLoaded" :no-separation="true">
        <Tab
          v-if="!isTableCreation"
          :title="$t('importFileModal.importPreview')"
        >
          <SimpleGrid
            class="import-modal__preview"
            :rows="previewImportData"
            :fields="fields"
          />
        </Tab>
        <Tab :title="$t('importFileModal.filePreview')">
          <SimpleGrid
            class="import-modal__preview"
            :rows="previewFileData"
            :fields="fileFields"
          />
        </Tab>
      </Tabs>

      <div
        v-if="!jobHasSucceeded || errorReport.length === 0"
        class="modal-progress__actions"
      >
        <ProgressBar
          v-if="importInProgress && showProgressBar"
          :value="progressPercentage"
          :status="humanReadableState"
        />
        <div class="align-right">
          <button
            class="button button--large"
            :class="{
              'button--loading':
                importInProgress || (jobHasSucceeded && !isTableCreated),
            }"
            :disabled="
              importInProgress ||
              !canBeSubmitted ||
              (jobHasSucceeded && !isTableCreated)
            "
            @click="$refs.tableForm.submit()"
          >
            {{
              isTableCreation
                ? $t('importFileModal.addButton')
                : $t('importFileModal.importButton')
            }}
          </button>
        </div>
      </div>
      <div v-else class="align-right">
        <button
          class="button button--large button--success"
          :class="{ 'button--loading': !isTableCreated }"
          @click="openTable()"
        >
          {{
            isTableCreation
              ? $t('importFileModal.openCreatedTable')
              : $t('importFileModal.showTable')
          }}
        </button>
      </div>
    </template>
    <template v-if="!isTableCreation" #sidebar>
      <div class="import-modal__field-mapping">
        <div v-if="header.length > 0" class="import-modal__field-mapping-body">
          <h3>{{ $t('importFileModal.fieldMappingTitle') }}</h3>
          <p>{{ $t('importFileModal.fieldMappingDescription') }}</p>
          <div v-for="(head, index) in header" :key="head" class="control">
            <label class="control__label control__label--small">
              {{ head }}
            </label>
            <Dropdown v-model="mapping[index]">
              <DropdownItem name="Skip" :value="0" icon="ban" />
              <DropdownItem
                v-for="field in availableFields"
                :key="field.id"
                :name="field.name"
                :value="field.id"
                :icon="field._.type.iconClass"
                :disabled="
                  selectedFields.includes(field.id) &&
                  field.id !== mapping[index]
                "
              />
            </Dropdown>
          </div>
        </div>
        <div v-else class="import-modal__field-mapping--empty">
          <i class="import-modal__field-mapping-empty-icon iconoir-shuffle" />
          <div class="import-modal__field-mapping-empty-text">
            {{ $t('importFileModal.selectImportMessage') }}
          </div>
        </div>
      </div>
      <div class="modal__actions">
        <a class="modal__close" @click="hide()">
          <i class="iconoir-cancel"></i>
        </a>
      </div>
    </template>
  </Modal>
</template>

<script>
import VueRouter from 'vue-router'
import { clone } from '@baserow/modules/core/utils/object'
import modal from '@baserow/modules/core/mixins/modal'
import error from '@baserow/modules/core/mixins/error'
import jobProgress from '@baserow/modules/core/mixins/jobProgress'
import TableService from '@baserow/modules/database/services/table'
import {
  uuid,
  getNextAvailableNameInSequence,
} from '@baserow/modules/core/utils/string'
import SimpleGrid from '@baserow/modules/database/components/view/grid/SimpleGrid'
import _ from 'lodash'

import { ResponseErrorMessage } from '@baserow/modules/core/plugins/clientHandler'

import TableForm from './TableForm'

export default {
  name: 'ImportFileModal',
  components: { TableForm, SimpleGrid },
  mixins: [modal, error, jobProgress],
  props: {
    database: {
      type: Object,
      required: true,
    },
    table: {
      type: Object,
      required: false,
      default: null,
    },
    fields: {
      type: Array,
      required: false,
      default: () => [],
    },
  },
  data() {
    return {
      importer: '',
      uploadProgressPercentage: 0,
      importState: null,
      showProgressBar: false,
      header: [],
      mapping: {},
      getData: null,
      previewData: [],
      dataLoaded: false,
    }
  },
  computed: {
    isTableCreation() {
      return this.table === null
    },
    isTableCreated() {
      if (!this.job?.table_id) {
        return false
      }
      return this.database.tables.some(({ id }) => id === this.job.table_id)
    },
    canBeSubmitted() {
      return (
        this.isTableCreation ||
        (this.importer &&
          Object.values(this.mapping).some(
            (value) => this.fieldIndexMap[value] !== undefined
          ))
      )
    },
    fieldTypes() {
      return this.$registry.getAll('field')
    },
    fileFields() {
      return this.header.map((header, index) => ({
        type: 'text',
        name: header,
        id: uuid(),
        order: index,
      }))
    },
    /**
     * All writable fields.
     */
    writableFields() {
      return this.fields.filter(
        ({ type }) => !this.fieldTypes[type].getIsReadOnly()
      )
    },
    /**
     * Map beetween the field id and its index in the array.
     */
    fieldIndexMap() {
      return Object.fromEntries(
        this.writableFields.map((field, index) => [field.id, index])
      )
    },
    /**
     * All writable fields that can be imported into
     */
    availableFields() {
      return this.writableFields.filter(({ type }) =>
        this.fieldTypes[type].getCanImport()
      )
    },
    fieldMapping() {
      return Object.entries(this.mapping)
        .filter(
          ([, targetFieldId]) =>
            !!targetFieldId ||
            // Check if we have an id from a removed field
            this.fieldIndexMap[targetFieldId] !== undefined
        )
        .map(([importIndex, targetFieldId]) => {
          return [importIndex, this.fieldIndexMap[targetFieldId]]
        })
    },
    // Template row with default values
    defaultRow() {
      return this.writableFields.map((field) =>
        this.fieldTypes[field.type].getEmptyValue(field)
      )
    },
    previewFileData() {
      return this.previewData.map((row) => {
        const newRow = Object.fromEntries(
          this.fileFields.map((field, index) => [
            `field_${field.id}`,
            `${row[index]}`,
          ])
        )
        newRow.id = uuid()
        return newRow
      })
    },
    previewImportData() {
      return this.previewData.map((row) => {
        const newRow = Object.fromEntries(
          this.fieldMapping.map(([importIndex, fieldIndex]) => {
            const field = this.writableFields[fieldIndex]
            return [
              `field_${field.id}`,
              this.fieldTypes[field.type].prepareValueForPaste(
                field,
                `${row[importIndex]}`,
                row[importIndex]
              ),
            ]
          })
        )
        newRow.id = uuid()
        return newRow
      })
    },

    /**
     * Fields that are mapped to a column
     */
    selectedFields() {
      return Object.values(this.mapping)
    },
    progressPercentage() {
      switch (this.state) {
        case null:
          return 0
        case 'preparingData':
          return 1
        case 'uploading':
          // 10% -> 50%
          return (this.uploadProgressPercentage / 100) * 40 + 10
        default:
          // 50% -> 100%
          return 50 + this.job.progress_percentage / 2
      }
    },
    state() {
      if (this.job === null) {
        return this.importState
      } else {
        return this.job.state
      }
    },
    importInProgress() {
      return this.state !== null && !this.jobIsFinished && !this.error.visible
    },
    importerTypes() {
      return this.$registry.getAll('importer')
    },
    importerComponent() {
      return this.importer === ''
        ? null
        : this.$registry.get('importer', this.importer).getFormComponent()
    },
    humanReadableState() {
      switch (this.state) {
        case null:
          return ''
        case 'preparingData':
          return this.$t('importFileModal.preparing')
        case 'uploading':
          if (this.uploadProgressPercentage === 100) {
            return this.$t('job.statePending')
          } else {
            return this.$t('importFileModal.uploading')
          }
        default:
          return this.jobHumanReadableState
      }
    },
    errorReport() {
      if (this.job && Object.keys(this.job.report.failing_rows).length > 0) {
        return Object.keys(this.job.report.failing_rows)
          .map((key) => parseInt(key, 10) + 1)
          .sort((a, b) => a - b)
      } else {
        return []
      }
    },
  },
  beforeDestroy() {
    this.stopPollIfRunning()
  },
  methods: {
    getDefaultName() {
      const excludeNames = this.database.tables.map((table) => table.name)
      const baseName = this.$t('importFileModal.defaultName')
      return getNextAvailableNameInSequence(baseName, excludeNames)
    },
    reset(full = true) {
      this.job = null
      this.uploadProgressPercentage = 0
      if (full) {
        this.header = []
        this.importState = null
        this.mapping = {}
        this.getData = null
        this.previewData = []
        this.dataLoaded = false
      }
      this.hideError()
    },
    onData({ header, previewData }) {
      this.header = header
      this.previewData = previewData
      this.mapping = Object.fromEntries(
        header.map((name, index) => {
          const foundField = this.availableFields.find(
            ({ name: fieldName }) => fieldName === name
          )
          return [index, foundField ? foundField.id : 0]
        })
      )
      this.dataLoaded = header.length > 0 || previewData.length > 0
    },
    onGetData(getData) {
      this.getData = getData
    },
    onHeader(header) {
      this.header = header
      this.mapping = Object.fromEntries(
        header.map((name, index) => {
          const foundField = this.availableFields.find(
            ({ name: fieldName }) => fieldName === name
          )
          return [index, foundField ? foundField.id : 0]
        })
      )
    },
    /**
     * When the form is submitted we try to extract the initial data and first row
     * header setting from the values. An importer could have added those, but they
     * need to be removed from the values.
     */
    async submitted(formValues) {
      this.showProgressBar = false
      this.reset(false)
      let data = null
      const values = { ...formValues }

      if (typeof this.getData === 'function') {
        try {
          this.showProgressBar = true
          this.importState = 'preparingData'
          await this.$ensureRender()

          data = await this.getData()

          if (!this.isTableCreation) {
            const fieldMapping = Object.entries(this.mapping)
              .filter(
                ([, targetFieldId]) =>
                  !!targetFieldId ||
                  // Check if we have an id from a removed field
                  this.fieldIndexMap[targetFieldId] !== undefined
              )
              .map(([importIndex, targetFieldId]) => {
                return [importIndex, this.fieldIndexMap[targetFieldId]]
              })

            // Template row with default values
            const defaultRow = this.writableFields.map((field) =>
              this.fieldTypes[field.type].getEmptyValue(field)
            )

            // Precompute the prepare value function for each field
            const prepareValueByField = this.writableFields.map(
              (field) => (value) =>
                this.fieldTypes[field.type].prepareValueForUpdate(
                  field,
                  this.fieldTypes[field.type].prepareValueForPaste(
                    field,
                    `${value}`,
                    value
                  )
                )
            )

            // Processes the data by chunk to avoid UI freezes
            const result = []
            for (const chunk of _.chunk(data, 1000)) {
              result.push(
                chunk.map((row) => {
                  const newRow = clone(defaultRow)
                  fieldMapping.forEach(([importIndex, targetIndex]) => {
                    newRow[targetIndex] = prepareValueByField[targetIndex](
                      row[importIndex]
                    )
                  })

                  return newRow
                })
              )
              await this.$ensureRender()
            }
            data = result.flat()
          } else {
            // Add the header in case of table creation
            data = [this.header, ...data]
          }
        } catch (error) {
          this.reset()
          this.handleError(error, 'application')
        }
      }

      this.importState = 'uploading'

      const onUploadProgress = ({ loaded, total }) =>
        (this.uploadProgressPercentage = (loaded / total) * 100)

      try {
        if (data && data.length > 0) {
          this.showProgressBar = true
        }

        if (this.isTableCreation) {
          const { data: job } = await TableService(this.$client).create(
            this.database.id,
            values,
            data,
            true,
            {
              onUploadProgress,
            }
          )
          this.startJobPoller(job)
        } else {
          const { data: job } = await TableService(this.$client).importData(
            this.table.id,
            data,
            {
              onUploadProgress,
            }
          )
          this.startJobPoller(job)
        }
      } catch (error) {
        this.stopPollAndHandleError(error, {
          ERROR_MAX_JOB_COUNT_EXCEEDED: new ResponseErrorMessage(
            this.$t('job.errorJobAlreadyRunningTitle'),
            this.$t('job.errorJobAlreadyRunningDescription')
          ),
        })
      }
    },
    getCustomHumanReadableJobState(jobState) {
      const translations = {
        'row-import-creation': this.$t('importFileModal.stateRowCreation'),
        'row-import-validation': this.$t('importFileModal.statePreValidation'),
        'import-create-table': this.$t('importFileModal.stateCreateTable'),
      }
      return translations[jobState]
    },
    async openTable() {
      // Redirect to the newly created table.
      try {
        await this.$nuxt.$router.push({
          name: 'database-table',
          params: {
            databaseId: this.database.id,
            tableId: this.job.table_id,
          },
        })
      } catch (error) {
        // When redirecting to the `database-table`, it can happen that it redirects
        // to another view. For some reason, this is causing the router throw an
        // error. In our case, it's perfectly fine, so we're suppressing this error
        // here. More information:
        // https://stackoverflow.com/questions/62223195/vue-router-uncaught-in-promise-
        // error-redirected-from-login-to-via-a
        const { isNavigationFailure, NavigationFailureType } = VueRouter
        if (!isNavigationFailure(error, NavigationFailureType.redirected)) {
          throw error
        }
      }
      this.hide()
    },
    async onJobDone() {
      if (this.isTableCreation) {
        // Let's add the table to the store...
        const { data: table } = await TableService(this.$client).get(
          this.job.table_id
        )

        await this.$store.dispatch('table/forceUpsert', {
          database: this.database,
          data: table,
        })

        if (this.errorReport.length === 0) {
          await this.openTable()
        }
      } else {
        this.$bus.$emit('table-refresh', {
          tableId: this.job.table_id,
        })
        if (this.errorReport.length === 0) {
          this.hide()
        }
      }
    },
    onJobFailed() {
      const error = new ResponseErrorMessage(
        this.$t('importFileModal.importError'),
        this.job.human_readable_error
      )
      this.stopPollAndHandleError(error)
    },
    onJobPollingError(error) {
      this.stopPollAndHandleError(error)
    },
    stopPollAndHandleError(error, specificErrorMap = null) {
      this.stopPollIfRunning()
      error.handler
        ? this.handleError(error, 'application', specificErrorMap)
        : this.showError(error)
    },
  },
}
</script>