<template>
  <q-card class="full-width no-shadow" style="max-width: 800px">
    <q-form v-if="model" ref="form">
      <q-card-section class="q-pb-md q-gutter-y-md">
        <q-input
          v-model="model.name"
          label="Название"
          :rules="[validateRequired]"
          filled
        />
        <q-input
          v-model="model.description"
          label="Описание"
          :rules="[validateRequired]"
          filled
        />
        <q-input
          v-model.trim="model.code"
          label="Код"
          :rules="[validateRequired, validateCode, codeIsUniq]"
          filled
          :disable="isRegistryExist && hasRegistryBeenEnabled"
        />

        <q-skeleton v-show="connectionsSelectLoading" type="QInput" />
        <q-select
          v-model="model.linkedRegistries"
          :class="{ 'connections-hidden': connectionsSelectLoading }"
          ref="connectionSelect"
          option-label="name"
          option-value="id"
          :options="filteredConnectionOptions"
          use-input
          use-chips
          multiple
          emit-value
          map-options
          :label="$t('registries.linkedRegistriesTitle')"
          filled
          @filter="filterConnection"
        />

        <ReferenceSelect
          v-model="model.territorialLevel"
          dictionary="territorial_levels"
          :label="$t('registries.territorialLevelTitle')"
        />
        <q-checkbox
          v-model="model.enabled"
          :label="$t('registries.activeLabel')"
        />

        <q-card
          :flat="!model.isSocialTreasure"
          :bordered="model.isSocialTreasure"
          :class="[
            {
              'q-mx-sm q-my-md q-px-md q-pt-md': model.isSocialTreasure,
            },
          ]"
        >
          <q-checkbox
            v-model="model.isSocialTreasure"
            :label="$t('registries.socialTreasuresRegistry')"
          />

          <div class="q-pt-md q-gutter-y-md" v-if="model.isSocialTreasure">
            <div class="registry-checkbox">
              <q-checkbox
                v-model="hasRegistryOrderCode"
                :label="$t('registries.addRegistryOrder')"
                :disable="
                  isRegistryExist &&
                  hasRegistryBeenEnabled &&
                  !!formModel.registryOrderCode
                "
              />
              <div class="text-caption text-grey" style="margin-left: 40px;">{{ model.registryOrderCode }}</div>
            </div>
            <div class="registry-checkbox">
              <q-checkbox
                v-model="hasRegistryPaymentCode"
                :label="$t('registries.addRegistryPayment')"
                :disable="
                  isRegistryExist &&
                  hasRegistryBeenEnabled &&
                  !!formModel.registryPaymentCode
                "
              />
              <div class="text-caption text-grey" style="margin-left: 40px;">{{ model.registryPaymentCode }}</div>
            </div>
            <div class="text-caption q-mb-md">
              {{ $t('registries.actionsAfterActivation') }}
            </div>
          </div>
        </q-card>
      </q-card-section>

      <q-card-section>
        <q-select
          :label="$t('registries.service')"
          multiple
          filled
          use-chips
          map-options
          use-input
          input-debounce="600"
          ref="services"
          :value="selectedServices"
          :rules="[isNotEmpty]"
          @input="onServicesSelected"
          :options="servicesOptions"
          @filter="filterServices"
        />
        <q-table
          v-if="serviceDocuments.length"
          :title="$t('registries.documents')"
          :data="serviceDocuments"
          :columns="columns"
          row-key="_id"
          selection="multiple"
          :selected="selectedDocuments"
          @update:selected="onDocumentsSelected"
        />
      </q-card-section>

      <q-card
        :flat="!model.isCustomForm"
        :bordered="model.isCustomForm"
        :class="[{ 'q-ma-md': model.isCustomForm }]"
      >
        <q-card-section class="q-pb-md q-gutter-y-md">
          <q-btn
            v-if="!model.isCustomForm"
            flat
            color="primary"
            icon="add"
            @click="addCustomForm"
          >
            {{ $t('registries.addCustomForm') }}
          </q-btn>
          <q-btn
            v-if="model.isCustomForm"
            flat
            color="primary"
            icon="delete"
            @click="deleteCustomForm"
          >
            {{ $t('registries.deleteCustomForm') }}
          </q-btn>
          <q-btn
            v-if="model.isCustomForm"
            flat
            color="primary"
            icon="refresh"
            @click="generateSchema"
          >
            {{ $t('registries.generateSchema') }}
          </q-btn>
          <v-json-editor
            v-if="!!form && model.isCustomForm"
            ref="jsoneditor"
            v-model="form"
            height="500px"
            :options="{
              mode: 'code',
              mainMenuBar: false,
            }"
          />
        </q-card-section>
      </q-card>
    </q-form>
  </q-card>
</template>

<script lang="ts">
import { Component, Prop, Vue, Watch, Ref } from 'vue-property-decorator';
import ReferenceSelect from '@components/common/reference-select/index.vue';

// @ts-ignore
import VJsonEditor from 'v-jsoneditor';
import createView from '@utils/registry-view-generator';
import { MetaClassSchemaRaw } from '@devsun/eva-models/dist/models/Schema';
import { ExtendedRegistry } from '@models/ExtendedRegistry';
import {
  FormApi,
  Form,
  RegistryApi,
  RegistryMapping,
  RegistryMappingApi,
  Service,
  ServiceApi,
  Registry,
} from '@openapi/registry-service';
import get from 'lodash/get';
import isEqual from 'lodash/isEqual';
import cloneDeep from 'lodash/cloneDeep';
import { notifyError, notifySuccess } from '@utils/notify';
import { TableColumn } from '@/interfaces';
import { RegistryWithForm } from '@/models/RegistryWithForm';
import { QForm, QSelect } from 'quasar';

/*
 * q-select with linkedRegistries
 * remove map-options
 * later planning near 200 items in select
 *
 * */

interface ServiceOption {
  value: string;
  label: string;
  service: Service;
}

interface ServiceDocumentOption {
  _id: string;
  service: Service;
  document: ServiceDocument;
}

// todo use also from ServiceApi
interface ServiceDocument {
  id: string;
  code: string;
  name: string;
  source: string;
  form: any[];
}

export interface Mapping extends Omit<RegistryMapping, 'id'> {
  id?: string;
  registryOrderCode?: string;
  registryPaymentCode?: string;
}

const serviceApi = new ServiceApi();
const registryApi = new RegistryApi();
const registryMappingApi = new RegistryMappingApi();
const formApi = new FormApi();

function makeCode(code: string | undefined, suffix: string, active: boolean): string | undefined {
  return !!code && active ? `${code}${suffix}` : undefined;
}

@Component({
  components: {
    ReferenceSelect,
    VJsonEditor,
  },
})
export default class RegistryForm extends Vue {
  @Ref('form') formRef: QForm;
  @Ref('services') servicesSelectRef: QSelect;

  @Prop({ default: () => ({}) }) formModel!: Partial<RegistryWithForm>;

  model: Partial<ExtendedRegistry> = {};

  services: Service[] = [];

  mapping: Mapping[] = [];
  mappingStored: Mapping[] = [];

  form: Form | MetaClassSchemaRaw | null = null;

  get hasRegistryOrderCode(): boolean {
    return !!this.registryOrderCode;
  }

  set hasRegistryOrderCode(val: boolean) {
    if (!this.model?.code) {
      notifyError('Код реестра должен быть установлен');
      return;
    }
    this.registryOrderCode = makeCode(this.model.code, 'Order', val);
  }

  get hasRegistryPaymentCode(): boolean {
    return !!this.registryPaymentCode;
  }

  set hasRegistryPaymentCode(val: boolean) {
    if (!this.model?.code) {
      notifyError('Код реестра должен быть установлен');
      return;
    }
    this.registryPaymentCode = makeCode(this.model.code, 'Payment', val);
  }

  registryOrderCode: string | undefined = '';
  registryPaymentCode: string | undefined = '';

  @Watch('registryOrderCode')
  onRegistryOrderCodeChenge(code: string): void {
    this.model.registryOrderCode = code;
  }

  @Watch('registryPaymentCode')
  onRegistryPaymentCodeChenge(code: string): void {
    this.model.registryPaymentCode = code;
  }

  @Watch('model.code')
  onCodeChenge(code: string): void {
    /*
      Если реестр уже активирован и есть какойто из кодов, то не тргаем его
      Инасе генерируем код автоматически
    */
    this.registryOrderCode = this.hasRegistryBeenEnabled
      ? this.formModel.registryOrderCode
      : makeCode(code, 'Order', !!this.model.registryOrderCode);

    this.registryPaymentCode = this.hasRegistryBeenEnabled
      ? this.formModel.registryPaymentCode
      : makeCode(code, 'Payment', !!this.model.registryPaymentCode);
  }

  columns: TableColumn<ServiceDocumentOption>[] = [
    {
      name: 'service',
      required: true,
      label: this.$t('registries.service'),
      align: 'left',
      field: (row: ServiceDocumentOption): string => row.service.shortName,
      sortable: true,
    },
    {
      name: 'code',
      required: true,
      label: this.$t('registries.documentCode'),
      align: 'left',
      field: (row: ServiceDocumentOption): string => row.document.code,
      sortable: true,
    },
    {
      name: 'name',
      required: true,
      label: this.$t('registries.documentName'),
      align: 'left',
      field: (row: ServiceDocumentOption): string => row.document.name,
      sortable: true,
      style: 'white-space: pre-line',
    },
  ];

  get isRegistryExist(): boolean {
    return !!this.formModel?.id;
  }

  get hasRegistryBeenEnabled(): boolean {
    return !!this.formModel.hasBeenEnabled;
  }

  get servicesOptions(): ServiceOption[] {
    return this.services.map((srv) => ({
      value: srv.id,
      label: srv.shortName,
      service: srv,
    }));
  }

  get serviceDocuments(): ServiceDocumentOption[] {
    return this.selectedServices.flatMap((item: ServiceOption) =>
      // @ts-ignore todo expanded interface
      (item.service.documents || [])
        .filter((d: ServiceDocument) => d.source === 'result')
        .map((d: ServiceDocument) => ({
          _id: `${item.service.id}_${d.id}`,
          service: item.service,
          document: d,
        }))
    );
  }

  get selectedAndPresentDocuments(): ServiceDocumentOption[] {
    const presentDocIds = this.serviceDocuments.map((d) => d._id);
    return this.selectedDocuments.filter((d) => presentDocIds.includes(d._id));
  }

  addCustomForm(): void {
    this.model.isCustomForm = true;
    this.generateSchema();
  }

  deleteCustomForm(): void {
    this.model.isCustomForm = false;
    this.form = null;
    this.$forceUpdate();
  }

  generateSchema(): void {
    this.form = createView(
      this.selectedAndPresentDocuments.map((d) => d.document)
    ) as MetaClassSchemaRaw;
    this.$forceUpdate();
  }

  async saveSchema(): Promise<void> {
    if (!this.form) return;
    const id = get(this, 'form.id');

    try {
      const request = () => {
        return id
          ? formApi.updateForm([this.form as Form])
          : formApi.createForm([this.form as Form]);
      };
      const { data } = await request();
      this.$set(this.form, 'modified', data.modified);
      this.$set(this.form, 'id', (data.objectIds || [])[0]);

      notifySuccess(this.$t('registries.schemaSavedSuccessfully').toString());
    } catch (error) {
      notifyError(this.$t('registries.schemaSaveFailed').toString());
      console.error(error);
    }
  }

  private async createMapping(toCreate: RegistryMapping[]): Promise<void> {
    const { data } = await registryMappingApi.createRegistryMapping(toCreate);

    toCreate.forEach((pair, i) => {
      if (!data.objectIds) return;
      const obj = toCreate[i];
      const id = data.objectIds[i];
      if (obj) {
        obj.id = id;
        obj.modified = data.modified;
      }
    });
  }

  private async updateMapping(toUpdate: RegistryMapping[]): Promise<void> {
    const { data } = await registryMappingApi.updateRegistryMapping(toUpdate);
    toUpdate.forEach((obj) => {
      obj.modified = data.modified;
    });
  }

  private async deleteMapping(toDelete: string[]): Promise<void> {
    await registryMappingApi.deleteRegistryMapping(toDelete);
  }

  async saveMapping(): Promise<void> {
    if (!this.model?.id) {
      notifyError(this.$t('registries.saveRegistryBeforeSave').toString());
      return;
    }

    this.mapping.forEach((m) => {
      m.registryCode = this.model.code || '';
      m.name = this.model.name || '';

      if (this.model.isSocialTreasure) {
        m.registryOrderCode = this.model.registryOrderCode;
        m.registryPaymentCode = this.model.registryPaymentCode;
      }
    });

    const toDelete = this.mappingStored.filter(
      (old) => !this.mapping.find((m) => m.serviceId === old.serviceId)
    );

    const toCreate = this.mapping.filter(
      (m) => !this.mappingStored.find((old) => old.serviceId === m.serviceId)
    );

    const toUpdate = this.mapping.flatMap((newMapping) => {
      const oldItem = this.mappingStored.find(
        (m) => m.serviceId === newMapping.serviceId
      );
      if (!oldItem) {
        return [];
      } else {
        newMapping.id = oldItem.id;
        newMapping.modified = oldItem.modified;
        if (isEqual(newMapping, oldItem)) {
          return [];
        } else {
          return [newMapping];
        }
      }
    });

    const requests: Promise<unknown>[] = [];

    if (toCreate.length !== 0) {
      requests.push(this.createMapping(toCreate as RegistryMapping[]));
    }

    if (toUpdate.length !== 0) {
      requests.push(this.updateMapping(toUpdate as RegistryMapping[]));
    }

    if (toDelete.length !== 0) {
      requests.push(
        this.deleteMapping(toDelete.map((obj) => obj.id) as string[])
      );
    }

    try {
      await Promise.all(requests);
      notifySuccess(this.$t('registries.mappingSavedSuccessfully').toString());
    } catch (error) {
      notifyError(this.$t('registries.mappingSaveFailed').toString());
      console.error(error);
    }
    this.mappingStored = cloneDeep(this.mapping);
  }

  filterServices(val: string, doneFn: () => void): void {
    this.fetchServices(val).then(() => doneFn());
  }

  validateCode(code: string): boolean | string {
    if (/^[a-zA-Z][a-zA-Z0-9]+$/.test(code)) {
      return true;
    } else {
      return /^\d$/.test(code.slice(0, 1))
        ? this.$t('registries.codeAttrInvalidMsg2').toString()
        : this.$t('registries.codeAttrInvalidMsg1').toString();
    }
  }

  validateRequired(val: string): string | boolean {
    return !!val || this.$t('registries.fieldIsRequired').toString();
  }

  async codeIsUniq(val: string): Promise<string | boolean> {
    if (!val) return true;
    try {
      const { data } = await registryApi.searchRegistry(`code==${val}`);
      // searchPayment
      const reg = data[0] as RegistryWithForm | undefined;
      if (this.formModel.code && !!reg) {
        return reg?.code === this.formModel.code || this.$t('registries.notUniq').toString();
      }
      return reg ? this.$t('registries.notUniq').toString() : true;
    } catch (e) {
      return true;
    }
  }

  isNotEmpty(val: ServiceOption[]): string | boolean {
    return val.length ? true : this.$t('registries.fieldIsRequired').toString();
  }

  saving = false;

  async saveRegistry(): Promise<void> {
    this.saving = true;
    const registry = cloneDeep(this.model);

    if (registry.enabled) {
      registry.hasBeenEnabled = Math.trunc(new Date().getTime() / 1000);
    }

    try {
      // Сохраняем форму если есть и получаем её id
      if (this.form) {
        await this.saveSchema();
        registry.form = get(this.form, 'id');
      }

      const request = () => {
        return this.isRegistryExist
          ? registryApi.updateRegistry([registry as Registry])
          : registryApi.createRegistry([registry as Registry]);
      };

      const { data } = await request();

      if (this.model && data.objectIds) {
        this.model.modified = data.modified;
        this.model.id = data.objectIds[0];
        this.$store.commit(`utils/cards/addModel`, {
          model: this.model,
          path: this.model.code,
        });
      }

      await this.saveMapping();
      notifySuccess(this.$t('registries.registrySavedSuccessfully').toString());
    } catch (error) {
      notifyError(this.$t('registries.registrySaveFailed').toString());
      console.error(error);
    }
  }

  // todo refactor 'connectionSelect' to more lazy
  connectionOptions: Registry[] = [];
  connectionsSelectLoading = true;
  filteredConnectionOptions: Registry[] = [];
  currentRegistry = '';

  @Watch('formModel', { immediate: true })
  onFormModelChange(model: ExtendedRegistry): void {
    model.enabled = model.enabled ?? false;
    this.model = cloneDeep(model);
    this.currentRegistry = model.code;
  }

  async created(): Promise<void> {
    this.connectionOptions = await this.getOptions();

    if (this.model.linkedRegistries?.length) {
      const select = this.$refs.connectionSelect as any;
      select.showPopup();
      this.$nextTick(() => {
        select.hidePopup();
        this.connectionsSelectLoading = false;
      });
    } else {
      this.connectionsSelectLoading = false;
    }

    this.form = this.formModel?.form || null;
    await this.fetchRegistryMapping();
    await this.fetchServicesFromMapping();
  }

  async fetchRegistryMapping(): Promise<void> {
    if (!this.model.code) {
      return;
    }
    return await new RegistryMappingApi()
      .pagingRegistryMapping(
        undefined,
        undefined,
        `registryCode==${this.model.code}`
      )
      .then(({ data }) => {
        if (data.records) {
          data.records.forEach((m) => {
            m.resultCodes = m.resultCodes || [];
          });
          this.mapping = data.records;
          this.mappingStored = cloneDeep(data.records);
        }
      })
      .catch((e) => {
        notifyError(this.$t('registries.mappingLoadFailed').toString());
        console.error(e);
      });
  }

  get selectedServices(): ServiceOption[] {
    return this.servicesOptions.filter((s) =>
      this.mapping.find((m) => s.service.id === m.serviceId)
    );
  }

  get selectedDocuments(): ServiceDocumentOption[] {
    return this.mapping.flatMap((m) =>
      this.serviceDocuments.filter(
        (d) =>
          d.service.id === m.serviceId &&
          m.resultCodes.includes(d.document.code)
      )
    );
  }

  onServicesSelected(services: ServiceOption[]): void {
    this.servicesSelectRef.hidePopup();

    this.mapping = this.mapping.filter((m) =>
      services.find((s) => s.service.id === m.serviceId)
    );

    services
      .filter((s) => !this.mapping.find((m) => m.serviceId === s.service.id))
      .forEach((s) => {
        const registryMapping: Mapping = {
          schema: 'RegistryMapping',
          name: this.model.name || '',
          serviceId: s.service.id,
          registryCode: this.model.code || '',

          resultCodes: [],
        };
        this.mapping.push(registryMapping);
      });

    const targetId = services[0]?.service.targetId;
    if (targetId) {
      this.services = this.services.filter((s) => s.targetId === targetId);
    }
  }

  onDocumentsSelected(documents: ServiceDocumentOption[]): void {
    this.mapping.forEach((m) => {
      m.resultCodes = m.resultCodes.filter((code) =>
        documents.find(
          (doc) => doc.service.id === m.serviceId && doc.document.code === code
        )
      );
      documents
        .filter(
          (doc) =>
            doc.service.id === m.serviceId &&
            !m.resultCodes.includes(doc.document.code)
        )
        .forEach((doc) => m.resultCodes.push(doc.document.code));
    });
  }

  async fetchServices(search: string): Promise<void> {
    const fulltextQuery = !search
      ? ''
      : [
          `(shortName=searchprefix="${search}" or shortName=search="${search}")`,
          `(name=searchprefix="${search}" or name=search="${search}")`,
          `(fullName=searchprefix="${search}" or fullName=search="${search}")`,
        ].join(' or ');

    const targetId = this.selectedServices.length ? this.selectedServices[0].service.targetId : '';
    const query = targetId ? `targetId=='${targetId}'` : '';

    try {
      const { data } = await serviceApi.pagingService(
        1,
        10,
        query,
        fulltextQuery,
        undefined,
        'documents,documents.form'
      );
      this.services = data.records || [];
    } catch (error) {
      notifyError(this.$t('registries.registryLoadFailed').toString());
      console.error(error);
    }
  }

  async fetchServicesFromMapping(): Promise<void> {
    const serviceIds = (this.mapping || []).map((m) => m.serviceId);
    if (!serviceIds.length) {
      return;
    }
    try {
      const { data } = await serviceApi.pagingService(
        1,
        1000,
        `id=in=(${serviceIds.join(',')})`,
        '',
        undefined,
        'documents,documents.form'
      );
      const newServices =
        data.records?.filter(
          (r) => !this.services.find((s) => s.id === r.id)
        ) || [];
      this.services.push(...newServices);
    } catch (error) {
      notifyError(this.$t('registries.schemaLoadFailed').toString());
      console.error(error);
    }
  }

  async filterConnection(inputValue = '', doneFn: () => void): Promise<void> {
    const filter = inputValue.toLowerCase();
    this.filteredConnectionOptions = this.connectionOptions.filter((el) => {
      const name = el.name.toLowerCase();
      const notCurrent = el.code !== this.currentRegistry;
      const filtered = inputValue ? name.includes(filter) : true;

      return notCurrent && filtered;
    });
    doneFn();
  }

  async getOptions(
    page = 1,
    totalRows = 0,
    result = [] as Registry[]
  ): Promise<Registry[]> {
    let _totalRows: number = totalRows;
    let _result = result;

    const { data: response } = await registryApi.pagingRegistry(page, 1000);

    if (_totalRows === 0 && response.totalRows) {
      _totalRows = response.totalRows;
    }

    if (response.records) {
      _result = _result.concat(response.records);
    }

    if (_result.length < _totalRows) {
      return this.getOptions(page + 1, _totalRows, _result);
    } else {
      return _result;
    }
  }
}
</script>

<style scoped>
.connections-hidden {
  opacity: 0;
  height: 0;
  padding: 0;
  overflow: hidden;
}
</style>
