import { Component, Prop, PropSync, Watch, Vue } from 'vue-property-decorator';
import get from 'lodash/get';
import isArray from 'lodash/isArray';
import isEmpty from 'lodash/isEmpty';
import { TaskProcess, UserTask, WorkflowServiceApi } from '@openapi/wf';
import { Certificate } from 'crypto-pro';
import { has } from 'lodash';
import { i18n } from '@plugins/i18n';
import { TagTranslation } from 'crypto-pro/lib/helpers/_parseCertInfo';
import { v4 as getUUID } from 'uuid';
import { notifyError } from '@/utils/notify';
import S3UrlSignerFilesService from '@/recruits/services/s3UrlService/files';

const wfApi = new WorkflowServiceApi();

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

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

export interface SignedS3File extends S3File {
  sig?: string;
  sigS3url: string;
  sigName: string;
}

export type FormModel = Record<string | symbol, unknown>;

export interface SocketMessage<T> {
  id: string;
  traceId: string;
  createDate: string;
  till: string;
  nextTry: string;
  attempts: number;
  forUser: string;
  armID: string;
  className: string;
  action: string;
  body: T;
}

export interface Task {
  userTask: {
    taskId?: string;
    defId?: string;
    procDefId?: string;
    formKey?: string;
    title?: string;
    createDate?: string;
    priority?: number;
    outData?: unknown;
    actions?: string[];
  };
  process?: {
    title: string;
    defKey: string;
    traceId: string;
    processTitles: [null, string];
  };
}

export interface PushMessage {
  msg: string;
  type: 'success' | 'error' | 'finish' | 'newTraceId';
  newTraceId?: string;
}

type Listener = { (m: SocketMessage<Task | PushMessage>): void };

export type OperationInfo = {
  operationType: string;
  recruitId: string;
  recruitFullName: string;
};

export interface SignData {
  data: unknown;
  signedFiles: SignedS3File;
  operationInfo: OperationInfo;
  certificate: Certificate;
}

interface Orig {
  bucket: string;
  path: string;
  name: string;
}

export class CryptoProIncorrectlyConfigured extends Error {
  constructor() {
    super(i18n.t('signDialog.cryptoProIncorrectlyConfigured').toString());
  }
}
export class ModuleIncorrectlyConfigured extends Error {
  constructor() {
    super(i18n.t('signDialog.cryptoProIncorrectlyConfigured').toString());
  }
}

export class CertificateIsNotValid extends Error {
  constructor() {
    super(i18n.t('signDialog.certificateIsNotValid').toString());
  }
}

export class FilesToSignWasNotFound extends Error {
  constructor() {
    super(i18n.t('signDialog.filesToSignWasNotFound').toString());
  }
}

// Может пригодиться, не удалять
// const CERT_DATA_PARSE = /((?!\s)[A-ZА-ЯЁ0-9\s.]+)=(?:"([-$#@"'\sа-яёА-ЯЁA-Za-z0-9,.]+)"|([-$#@\sа-яёА-ЯЁA-Za-z0-9.]+)(?=\s|,|$))/g;

/**
 *
 */
@Component
export default class CryptoProPlugin extends Vue {
  @PropSync('openDialog') open?: boolean;
  @Prop({ default: true }) enableSubmitForm: boolean;
  @Prop() processKey!: string;
  @Prop() userId!: string;
  @Prop() bucket?: string;
  @Prop() filesPath?: string;

  @Prop({ default: () => ({}) })
  userTask: UserTask;

  @Prop({ default: () => ({}) })
  taskProcess: TaskProcess;

  listener: { (e: MessageEvent<string>): void } | null = null;

  public systemSetupValid: boolean | null = null;
  public certificateIsValid: boolean | null = null;
  public selectedSertificate: CertificateOption | null = null;
  public certificateOptions: CertificateOption[] = [];

  public inProcess = false;
  public initialized = false;

  get certificate(): Certificate | null {
    return this.selectedSertificate?.value || null;
  }

  get certificateName(): string {
    return this.certificate?.name || '';
  }

  get certificateThumbprint(): string {
    return this.certificate?.thumbprint || '';
  }

  // Закладка на будущее
  public get certificateValidFrom(): string {
    return this.certificate?.validFrom || '';
  }

  get certificateValidTo(): string {
    return this.certificate?.validTo || '';
  }

  get hasParentProcess(): boolean {
    return !isEmpty(this.taskProcess) && !isEmpty(this.userTask);
  }

  loadingIssuerInfo = false;
  certificateIssuerInfo: TagTranslation[] = [];
  loadingOwnerInfo = false;
  certificateOwnerInfo: TagTranslation[] = [];

  protected async validateCertificate(
    certificate: Certificate
  ): Promise<boolean> {
    if (certificate == null) return false;
    try {
      const isValid = await this.$store.dispatch(
        'utils/cryptoPro/isValidSystemCertificate',
        certificate
      );
      return !!isValid;
    } catch (error) {
      if (error instanceof Error) {
        throw error;
      }
      throw new CryptoProIncorrectlyConfigured();
    }
  }

  protected async getIssuerInfo(
    certificate?: Certificate
  ): Promise<TagTranslation[]> {
    if (!certificate) return [];
    try {
      this.loadingIssuerInfo = true;
      const info = await certificate.getIssuerInfo();
      this.loadingIssuerInfo = false;
      return info;
    } catch {
      return [];
    }
  }

  protected async getOwnerInfo(
    certificate?: Certificate
  ): Promise<TagTranslation[]> {
    if (!certificate) return [];
    try {
      this.loadingOwnerInfo = true;
      const info = await certificate.getOwnerInfo();
      this.loadingOwnerInfo = false;
      return info;
    } catch {
      return [];
    }
  }

  async getCertificateAlgorithm(): Promise<string | undefined> {
    return await this.certificate?.getAlgorithm();
  }

  @Watch('certificate')
  async onCertificateChange(certificate: Certificate): Promise<void> {
    this.certificateIsValid = await this.validateCertificate(certificate);
    this.certificateIssuerInfo = await this.getIssuerInfo(certificate);
    this.certificateOwnerInfo = await this.getOwnerInfo(certificate);
    this.$emit('certificateSelected', certificate);
  }

  protected async initCryptoProPlugin(): Promise<void> {
    try {
      this.systemSetupValid = await this.$store.dispatch(
        'utils/cryptoPro/isValidSystemSetup'
      );

      if (this.systemSetupValid) {
        const certificates: Certificate[] = await this.$store.dispatch(
          'utils/cryptoPro/fetchCertificates'
        );
        this.certificateOptions = certificates.map((certificate) => ({
          label: certificate.name,
          value: certificate,
          data: certificate,
        }));
        this.selectedSertificate = this.certificateOptions[0] || null;

        this.initialized = true;
      } else {
        throw new CryptoProIncorrectlyConfigured();
      }
    } catch (error) {
      if (error instanceof Error && !!error.message) {
        throw error;
      }
      throw new CryptoProIncorrectlyConfigured();
    }
  }

  public async submitFormToParentProcess(
    base64: string,
    formModel: FormModel
  ): Promise<string> {
    if (!this.userTask?.taskId || !this.taskProcess.traceId) {
      throw new ModuleIncorrectlyConfigured();
    }
    await this.$store.dispatch(
      //
      'wfActions/processes/submitForm',
      {
        taskId: this.userTask.taskId,
        traceId: this.taskProcess.traceId,
        formModel: {
          cert: base64,
          ...formModel,
        },
      }
    );
    return this.taskProcess.traceId;
  }

  public async startProcess(
    base64: string,
    formModel: FormModel
  ): Promise<string> {
    console.log('startProcess', formModel);
    const { traceId } = await this.$store.dispatch(
      'wfActions/processes/dispatchProcess',
      {
        processKey: this.processKey,
        syncMode: false,
        utStepMode: false,
        userId: this.userId,
        data: {
          certificateAsBase64: base64,
          ...formModel,
        },
      }
    );
    return traceId;
  }

  protected async requestFilesForSign(
    base64: string,
    formModel: FormModel
  ): Promise<string> {
    // Добавляем Алгоритм ключа в данные
    const algorhitm = await this.getCertificateAlgorithm();
    formModel = {
      algorhitm,
      ...formModel,
    };
    //
    this.inProcess = true;
    if (this.hasParentProcess && this.enableSubmitForm) {
      return this.submitFormToParentProcess(base64, formModel);
    }
    return this.startProcess(base64, formModel);
  }

  async sendDataForSign(formModel: FormModel): Promise<void> {
    console.log('sendDataForSign', formModel);
    if (this.certificate == null) return;
    const isValid = await this.validateCertificate(this.certificate);
    if (!isValid) throw new CertificateIsNotValid();

    try {
      const certificate = this.certificate;
      const base64 = await this.$store.dispatch(
        'utils/cryptoPro/generateBase64ByCertificate',
        {
          certificate,
        }
      );
      const traceId = await this.requestFilesForSign(base64, formModel);

      const uuid = getUUID();
      this.registerListener((message) => {
        if (traceId !== message.traceId) return;
        console.log('traceId', traceId);
        console.log('listner', uuid);

        console.log('message', message);
        if (
          get(message, 'body.type') === 'newTraceId' &&
          (message as SocketMessage<PushMessage>).body.newTraceId
        ) {
          this.newTraceIdListener(
            (message as SocketMessage<Required<PushMessage>>).body.newTraceId
          );
        }
        if (get(message, 'body.userTask.formKey') === 'waitSignForm')
          return this.listnerOfPreparedFiles(message.body);

        if (get(message, 'body.userTask.formKey') === 'errorSignForm')
          return this.listnerOfError(message.body);
        if (this.parseEndPush(message)) this.listnerOfConfirm(message.body);
      });
      // Запускаем таймер на случай если процесс ляжет
      this.resetErrorTimeout();
    } catch (error) {
      if (error instanceof Error) throw error;
      throw new Error(String(error));
    }
  }

  errorTimeout: ReturnType<typeof setTimeout> | null = null;

  resetErrorTimeout(): void {
    this.clearErrorTimeout();
    this.errorTimeout = setTimeout(() => {
      this.removeListner('error');
      this.listnerOfError({
        type: 'error',
        msg: 'Процесс отвалился по таймауту',
      });
    }, 300000);
  }

  clearErrorTimeout(): void {
    if (this.errorTimeout) clearTimeout(this.errorTimeout);
  }

  public parseEndPush(message: SocketMessage<Task | PushMessage>): boolean {
    if ('userTask' in message.body) {
      return has(message, 'body.userTask.outData.success');
    }
    return get(message, 'body.type') === 'finish';
  }

  newTraceIdListener(traceId: string) {
    this.registerListener((message) => {
      console.log(message);
      if (traceId !== message.traceId) return;
      if (get(message, 'body.type') === 'finish') {
        this.$emit('finish');
        // Очищаем таймаут если всё хорошо
        this.clearErrorTimeout();
      }
    });
  }

  extractListOfFilesToSign(data: unknown): S3File[] {
    if (isArray(data)) return data;
    return get(data, 'filesForSign', []);
  }

  private async signAndSaveToS3(
    s3url: string,
    hash: string,
    format: string,
    certificate: unknown
  ): Promise<SignedS3File> {
    try {
      const [, bucket, path, name] =
        s3url?.match(
          /s3:\/\/([-a-z_]+)\/((?:[-a-z0-9_]+\/)*)([-a-z0-9._]+)$/i
        ) || [];

      const signature = await this.$store.dispatch(
        'utils/cryptoPro/createSignature',
        {
          hash,
          certificate,
        }
      );
      const blob = new Blob([signature], {
        type: 'text/plain',
      });
      const sigName = `${name}.sig`;
      const signFile = new File([blob], sigName);
      const sigS3url = await this.uploadFileToS3(signFile, sigName, {
        bucket,
        path: path || '',
        name,
      });
      return Promise.resolve({
        s3url,
        format,
        digest: hash,
        sigS3url,
        sigName,
      });
    } catch (e) {
      this.listnerOfError({
        type: 'error',
        msg: 'Не удалось загрузить sig',
      });
      throw new Error(`Не удалось подписать файлы на стороне клиента\n${e}`);
    }
  }

  /**
   * Метод загрузки в s3 может быть специфичным для места применения
   * @param file Текстовый файл подписи sig
   * @param name Имя файла
   */
  async uploadFileToS3(file: File, name: string, orig: Orig): Promise<string> {
    console.log('Старт загрузки sig файла', file, name);
    const bucket = this.bucket || orig.bucket;
    const path = this.filesPath || orig.path;
    const s3Url = await S3UrlSignerFilesService.uploadFileToS3ByPath({
      file,
      path: `${path}${name}`,
      bucket: bucket,
      contentType: 'application/x-pkcs7-signature',
    });

    return s3Url || '';
  }

  removeListner(reason?: string): void {
    console.log('removeListner by', reason);
    if (this.listener) {
      this.$websockets.removeListener(this.listener);
    } else {
      console.log('🤷🏻‍♂️ listner already removed');
    }
    this.listener = null;
    console.log('Listner remowed');
  }

  registerListener(cb: Listener): void {
    this.listener = (e: MessageEvent<string>) => {
      const traceData: SocketMessage<Task | PushMessage> = JSON.parse(
        get(e, 'data', '{}')
      );
      return cb(traceData);
    };
    this.$websockets.addListener(this.listener);
    console.log('👂🏻 Listner registered');
  }

  listnerOfConfirm(task?: Task | PushMessage): void {
    this.$emit('finish', task);
    // Очищаем таймаут если всё хорошо
    this.clearErrorTimeout();
  }

  listnerOfError(task?: Task | PushMessage): void {
    this.$emit('error', task);
    // Очищаем таймаут если пришла настоящая ошибка
    this.clearErrorTimeout();
  }

  async listnerOfPreparedFiles(task: Task | PushMessage): Promise<void> {
    if (!('userTask' in task)) return;

    // Перезапускаем таймер если процесс жив и отдает пуши
    this.resetErrorTimeout();

    const filesToSign = this.extractListOfFilesToSign(task.userTask.outData);
    const certificate = this.certificate;
    console.log('filesToSign recived', filesToSign);

    try {
      if (isEmpty(filesToSign)) throw new FilesToSignWasNotFound();

      const signedFiles = await Promise.all(
        filesToSign.map((s3File) => {
          return this.signAndSaveToS3(
            s3File.s3url,
            s3File.digest,
            s3File.format,
            certificate
          );
        })
      );
      if (!task.userTask?.taskId) throw new Error('Не передан taskId');
      await wfApi.submitForm(task.userTask?.taskId, {
        id: 'fileSigned',
        signedFiles,
        certificate,
      });
      console.log('✍🏻 form sent', task);

      const operationInfo = get(task.userTask.outData, 'operationInfo');

      this.$emit('signed', {
        operationInfo,
        signedFiles,
        certificate,
      });
      this.inProcess = false;
      console.log('operationInfo', operationInfo);
    } catch (error) {
      // TODO как реализают возможность принять это
      // if (task.userTask?.taskId) {
      //   await wfApi.submitForm(task.userTask?.taskId, {
      //     id: 'error',
      //     message: get(error, 'message'),
      //   });
      // }
      console.error(error);
      if (error instanceof Error) notifyError(error.message);
    }
  }

  destroyed(): void {
    this.clearErrorTimeout();
    this.removeListner('destroyed');
  }
}
