<i18n>
{
    "en": {
        "upload": {
            "SignDialog": {
                "document": "Document",
                "additionalDocuments": "Additional documents",
                "sign": "Sign",
                "fileToSignWasNotFound": "The file to sign was not found",
                "cryptoProIncorrectlyConfigured": "The CryptoPro program is incorrectly configured or the browser plug-in is not installed.",
                "failedGenerate": "Failed to generate the resulting document files.",
                "isNotValid": "Signature generation error. Invalid certificate.",
                "listOfCertificates": "List of certificates",
                "documentSignature": "Document signature",
                "validTo": "Valid to",
                "s3Error": "S3 service error",
                "s3PutError": "Error loading file into S3"
            }
        }
    },
    "ru": {
        "upload": {
            "SignDialog": {
                "document": "Документ",
                "additionalDocuments": "Дополнительные документы",
                "sign": "Подписать",
                "fileToSignWasNotFound": "Файлы для подписания не найдены.",
                "cryptoProIncorrectlyConfigured": "Неверно настроена программа \"КриптоПро\" или не установлен плагин для браузера.",
                "failedGenerate": "Не удалось сформировать файлы результирующего документа.",
                "isNotValid": "Ошибка формирования подписи. Невалидный сертификат или лицензия.",
                "listOfCertificates": "Список сертификатов",
                "documentSignature": "Подпись документа",
                "validTo": "Действителен до",
                "s3Error": "Ошибка сервиса s3",
                "s3PutError": "Ошибка при загрузке файла в S3"
            }
        }
    }
}
</i18n>

<template>
  <div>
    <q-dialog v-model="open" @hide="hideModal">
      <q-card class="full-width no-shadow" style="max-width: 580px">
        <q-card-section>
          <div class="text-h6">
            {{ $t('upload.SignDialog.documentSignature') }}
          </div>
        </q-card-section>
        <q-card-section class="q-gutter-y-md">
          <table class="text-primary">
            <tr>
              <td>
                <q-item>
                  <q-item-section>
                    <q-item-label class="text-grey-9">
                      {{ $t('statements.DocumentsCard.document') }}
                    </q-item-label>
                  </q-item-section>
                </q-item>
              </td>
              <td>
                <q-item>
                  <q-item-section>
                    <q-item-label>
                      {{ title }}
                    </q-item-label>
                  </q-item-section>
                </q-item>
              </td>
            </tr>
          </table>
          <q-select
            v-model="certificate"
            :disable="!certificates.length"
            :options="certificates"
            class="full-width"
            filled
            :label="$t('upload.SignDialog.listOfCertificates')"
          >
            <template #option="{ opt, toggleOption, selected }">
              <q-item
                @click="toggleOption(opt)"
                :class="{
                  'bg-red-1': isExpired(opt.data.validTo),
                  'bg-blue-1': selected,
                }"
                clickable
              >
                <div>
                  <div class="text-grey-8">{{ opt.label }}</div>
                  <div :style="{ fontSize: '0.75rem' }" class="">
                    {{ $t('upload.SignDialog.validTo') }}:
                    {{ convertDateFromBackendFormat(opt.data.validTo) }}
                  </div>
                </div>
              </q-item>
            </template>
          </q-select>
        </q-card-section>
        <q-card-actions align="right">
          <q-btn
            :disable="!systemSetupIsValid || !certificateHasChoosen"
            :loading="loading"
            :label="$t('upload.SignDialog.sign')"
            unelevated
            color="primary"
            @click="handler()"
          >
            <template #loading>
              <q-spinner-gears />
            </template>
          </q-btn>
        </q-card-actions>
        <q-inner-loading :showing="loading">
          <q-spinner-gears color="primary" size="50px" />
        </q-inner-loading>
      </q-card>
    </q-dialog>
  </div>
</template>

<script lang="ts">
import { convertDateFromBackendFormat, isPastDate } from '@utils/format-date';
import { notifyError, notifySuccess } from '@utils/notify';
import S3UrlSignerFilesService from '@services/s3urlsigner/files';
import { Component, Prop, PropSync, Watch, Vue } from 'vue-property-decorator';
import get from 'lodash/get';
import { WorkflowServiceApi } from '@openapi/wf';
import { Certificate } from 'crypto-pro';
import { isArray } from 'lodash';

const wfApi = new WorkflowServiceApi();

const PDF_FORMAT = 'PDF';
const XML_FORMAT = 'XML';

interface CertificateWithValue extends Certificate {
  value?: unknown;
}

interface ParentProcess {
  success: boolean;
  taskId: string;
  traceId: string;
}

interface CertificateOption {
  label: Certificate['subjectName'];
  value: Certificate;
  data: Certificate;
}

interface S3File {
  s3url: string;
  digest: string;
  format: string;
}

interface SignedS3File extends S3File {
  sig?: string;
}

@Component({
  methods: {
    convertDateFromBackendFormat,
  },
  components: {},
})
export default class UploadCertDialog extends Vue {
  convertDateFromBackendFormat: typeof convertDateFromBackendFormat;
  @PropSync('openDialog') open?: boolean;
  @Prop() parentProcess?: ParentProcess;
  @Prop() processKey!: string;
  @Prop() title!: string;
  @Prop() model!: Record<string, unknown>;

  public certificate: CertificateWithValue | null = null;
  public certificates: CertificateOption[] = [];
  private systemSetupValid = false;
  public loading = false;
  private positionFromAccount = null;
  private signatureHash = '';

  public get systemSetupIsValid(): boolean {
    return this.systemSetupValid;
  }

  public get certificateHasChoosen(): boolean {
    return this.certificate != null;
  }

  private get currentUser() {
    return this.$store.getters['oidcClient/currentUser'] || null;
  }

  @Watch('open', { immediate: true })
  onDialogDataChange(open: boolean): void {
    if (open) {
      this.$store
        .dispatch('utils/cryptoPro/isValidSystemSetup')
        .then((isValid) => {
          this.systemSetupValid = isValid;

          if (isValid) {
            this.$store
              .dispatch('utils/cryptoPro/fetchCertificates')
              .then((certificates: Certificate[]) => {
                this.certificates = certificates.map((certificate) => ({
                  label: certificate.subjectName,
                  value: certificate,
                  data: certificate,
                }));
              });
          } else {
            notifyError(
              this.$t('upload.SignDialog.cryptoProIncorrectlyConfigured').toString()
            );

            this.loading = false;
          }
        })
        .catch((e) => {
          const message = get(e, 'message');

          if (message) {
            notifyError(message);
          } else {
            notifyError(
              this.$t('upload.SignDialog.cryptoProIncorrectlyConfigured').toString()
            );
          }
          this.loading = false;
        });
    }
  }

  async validateCertificate(): Promise<boolean> {
    if (this.certificate == null) return false;
    const isValid = await this.$store
      .dispatch(
        'utils/cryptoPro/isValidSystemCertificate',
        this.certificate.value
      )
      .catch((e) => {
        const message = get(e, 'message');
        if (message) {
          notifyError(message);
        } else {
          notifyError(
            this.$t('upload.SignDialog.cryptoProIncorrectlyConfigured').toString()
          );
        }
        this.loading = false;
      });
    return !!isValid;
  }

  async startProcess(base64: string): Promise<string> {
    const { traceId } = await this.$store.dispatch('wf/processes/dispatchProcess', {
        processKey: this.processKey,
        data: {
          certificateAsBase64: base64,
          ...this.model,
        },
      }
    );
    return traceId;
  }

  async submitForm(base64: string): Promise<string> {
    if (!this.parentProcess?.taskId) throw new Error('Родительский процесс не найден');
    await wfApi.submitForm(this.parentProcess.taskId, {
      // @ts-ignore TODO Поправить openapi
      cert: base64,
    });
    return this.parentProcess.traceId;
  }

  async doAction(base64: string): Promise<string> {
    if (this.parentProcess) {
      return this.submitForm(base64);
    }
    return this.startProcess(base64);
  }

  async handler(): Promise<void> {
    if (this.certificate == null) return;
    const isValid = await this.validateCertificate();
    if (!isValid) return notifyError(this.$t('upload.SignDialog.isNotValid').toString());
    this.loading = true;

    try {
      const certificate = this.certificate.value;
      const base64 = await this.$store
        .dispatch('utils/cryptoPro/generateBase64ByCertificate', {
          certificate,
        })
        .catch((e) => notifyError(e));

      const traceId = await this.doAction(base64);

      const listener = async (event: any) => {
        const data = JSON.parse(get(event, 'data', '{}'));
        console.log(traceId, data);
        if (traceId !== get(data, 'traceId')) return;
        if (!data?.body) return;

        this.$websockets.removeListener(listener);
        const signedList: S3File[] = this.extractSignedList(data.body);
        // .filter(
        //   (s3File: S3File) => [XML_FORMAT, PDF_FORMAT].includes(s3File.format)
        // );
        const signedFiles = await Promise.all(
          signedList.map((s3File) => {
            return this.S3UrlSigner(
              s3File.s3url,
              s3File.digest,
              s3File.format,
              certificate
            );
          })
        );
        this.$emit('signed', {
          exportId: data.body.exportId || this.model.exportId,
          signedFiles,
          signed: true,
          state: this.$t('registries.unloadingList.signed'),
          sigHash: this.signatureHash,
          certificate,
        });
        this.hideModal();
        notifySuccess('Документы успешно подписаны');
      };
      this.$websockets.addListener(listener);
    } catch (e) {
      if (e instanceof Error) notifyError(e.message);
    }
  }

  extractSignedList(data: unknown): SignedS3File[] {
    if (isArray(data)) return data;
    return get(data, 'signedList', []);
  }

  private async S3UrlSigner(
    s3url: string,
    hash: string,
    format: string,
    certificate: unknown
  ): Promise<SignedS3File> {
    try {
      const name = s3url.split('/').slice(-1).join('/') ?? '';

      if (format === PDF_FORMAT) {
        this.signatureHash = hash;
      }
      const signature = await this.$store.dispatch(
        'utils/cryptoPro/createSignature',
        {
          hash,
          certificate,
        }
      );
      const blob = new Blob([signature], {
        type: 'text/plain',
      });
      const signFile = new File([blob], `${name}.sig`);
      const sig = await S3UrlSignerFilesService.uploadFileToS3ByPath({
        file: signFile,
        path: `Applications/${this.model?.departmentId}/sign/${signFile.name}`,
      });
      return Promise.resolve({
        s3url,
        format,
        digest: hash,
        sig,
      });
    } catch (e) {
      return Promise.reject(e);
    }
  }

  isExpired(date: string): boolean {
    return isPastDate(date);
  }

  private abortSignProcess(): void {
    if (!this.parentProcess) return;
    wfApi.submitForm(this.parentProcess?.taskId, {
      // @ts-ignore TODO Поправить openapi
      cert: null,
    });
  }

  public hideModal(): void {
    if (this.parentProcess && !this.parentProcess.success) {
      this.abortSignProcess();
    }
    this.open = false;
    this.certificate = null;
    this.certificates = [];
    this.systemSetupValid = false;
    this.loading = false;
    this.signatureHash = '';
  }

  async getPositionFromAccount(): Promise<void> {
    const accId = get(this.currentUser, 'sub');
    const account = await this.$store.dispatch('idm/accounts/dispatchAccount', {
      id: accId,
    });

    this.positionFromAccount = get(account, 'position', null);
  }

  create(): void {
    this.getPositionFromAccount();
  }
}
</script>
