import { HttpClient, HttpHeaders } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { MatDialog, MatDialogRef } from '@angular/material/dialog';
import { marker } from '@biesbjerg/ngx-translate-extract-marker';
import msgpack from 'msgpack-lite';
import { base64 } from 'rfc4648';
import { Observable, Subject } from 'rxjs';
import { AppStorage } from 'src/app/shared/app-storage';
import { DialogService } from 'src/app/shared/services/dialog.service';
import { EnvironmentService } from 'src/environments/environment.service';
import { WebSocketClient } from 'src/app/shared/ws-client';
import { create_account_recovery_confirm_request_type } from '../crypto/auth/recovery';
import {
  create_account_register_confirm_request_result,
  create_account_register_keyring_request_result,
} from '../crypto/auth/register';
import { TwoFactorAlertDialogComponent } from '../dialogs/two-factor-dialog/two-factor-alert-dialog/two-factor-alert-dialog.component';
import {
  DialogData,
  TwoFactorDialogComponent,
} from '../dialogs/two-factor-dialog/two-factor-dialog.component';
import { RequestQueue } from '../request-queue';
import { deleteAccountQuery, pingQuery } from '../server-services/querys';
import { AppCookieService } from './app-cookie.service';
import { CacheService } from './cache/cache.service';
import { Query, Request, OperationType } from './cache/cache-logic/cache-logic-interface';

/**
 *
 */
export type QueryParam = {
  /**
   * it will be the string of the query, mutation, or subscription
   */
  query: Query;
  variables?: Object;
  useSecondarySocket?: boolean;

  /**
   * If you need some data to pass the encrypt/decrypt process, what you do not want to pass the server
   */
  extra?: Object;

  /**
   * Call crypto encryption callback with the variables before send it to the server,
   * but after the cache checking
   */
  encrypt?: (variables: Object, extra: Object) => Promise<Object>;

  /**
   * Call crypto decryption after the server returned with the value. Cache will be saved
   * decrypted. If the query already cached, return with the cached data and skip decryption
   */
  decrypt?: (data: Object, variables: Object, extra: Object) => Promise<Object>;
};

export enum TwoFactorAuthResponse {
  PREREQUISITE = 'prerequisite', // need 2fa, send otpSalt
  REQUIRED = 'required', // after otpSalt, we have got a code via email
  DENIED = 'denied', // wrong code
}

export enum TwoFactorMode {
  NONE = 0,
  CODE = 10,
  // TODO - 30
}

export enum TwoFactorDetail {
  LOCKOUT = 'lockout',
}

export enum SpecificWebsocketError {
  CSRF = 4003,
  AUTH_CLOSE = 4001,
}

export enum WebSocketStatus {
  DISCONNECTED = 'disconnected',
  ERROR = 'error',
  RECONNECTING = 'reconnecting',
  CONNECTED = 'connected',
  CONNECTING = 'connecting',
}

@Injectable({
  providedIn: 'root',
})
export class ServerRestApiService {
  private wsClientPrimary: WebSocketClient;
  private wsClientSecondary: WebSocketClient; // for large payloads
  private requestQueueHandler: RequestQueue;

  /**
   * public variable, header has access to it, is this good? TODO
   */
  public wsStatus: WebSocketStatus = WebSocketStatus.CONNECTING;

  private isConnected = false;

  private TWO_FACTOR_AUTH_RESPONSE_PROP = '2FA';
  private TWO_FACTOR_AUTH_CODE_PROP = '2faCode';
  private TWO_FACTOR_AUTH_OTP_SALT_PROP = 'otpSalt';

  private CSRF_KEY = '__Secure-csrf';
  private CSRF_HEADER_KEY = 'pcsrf';
  private isCSRFTokenPending = false;

  private pingTime;
  private peformanceStart = 0;

  private defaultQueryParam: QueryParam = {
    query: {
      operation: OperationType.QUERY,
      endpoint: 'notdefined',
    },
    variables: {},
    extra: {},
    useSecondarySocket: false,
    encrypt: (variables, extra) => {
      return Promise.resolve(variables);
    },
    decrypt: (data, variables, extra) => {
      return Promise.resolve(data);
    },
  };

  /**
   * make query string from typed param
   * result looks like this: 'query getMe {', 'mutation setPassword {', 'subscription sub {'
   */
  private parseQueryType(query: Query): string {
    return query.operation + ' ' + query.endpoint + ' {';
  }

  private getDataFromQueryResponse(query: Query, response: Object) {
    return response['data'][query.endpoint];
  }

  constructor(
    private http: HttpClient,
    private cookie: AppCookieService,
    private cache: CacheService,
    private dialog: MatDialog,
    private dialogService: DialogService,
    private environmentService: EnvironmentService // private authService: AuthService, // private routerHandler: RouterHandler
  ) {
    this.requestQueueHandler = new RequestQueue();
  }

  public makeTimestamp() {
    return (this.pingTime + performance.now() - this.peformanceStart) / 1000.0;
  }

  private onWSConnectedCallbacks = [];
  public subscribeOnWSConnected(cb: Function) {
    this.onWSConnectedCallbacks.push(cb);
  }

  public unsubscribeOnWSConnected(cb: Function) {
    let pos = this.onWSConnectedCallbacks.indexOf(cb);
    if (pos > -1) {
      this.onWSConnectedCallbacks.splice(pos, 1);
    }
  }

  public isPrimaryWebsocketOpen(): boolean {
    return this.wsClientPrimary && this.wsClientPrimary.is_open();
  }

  public isSecondaryWebsocketOpen(): boolean {
    return this.wsClientSecondary && this.wsClientSecondary.is_open();
  }

  private onWSDisconnectedCallbacks = [];
  public subscribeOnWSDisconnected(cb: Function) {
    this.onWSDisconnectedCallbacks.push(cb);
  }

  public unsubscribeOnWSDisconnected(cb: Function) {
    let pos = this.onWSDisconnectedCallbacks.indexOf(cb);
    if (pos > -1) {
      this.onWSDisconnectedCallbacks.splice(pos, 1);
    }
  }

  private onWSAuthClosedCallbacks = [];
  public subscribeOnWSAuthClosed(cb: Function) {
    this.onWSAuthClosedCallbacks.push(cb);
  }

  public unsubscribeOnWSAuthClosed(cb: Function) {
    let pos = this.onWSAuthClosedCallbacks.indexOf(cb);
    if (pos > -1) {
      this.onWSAuthClosedCallbacks.splice(pos, 1);
    }
  }

  private getHttpHeaderOptions(): Promise<any> {
    return this.getCSRFToken().then((csrf) => {
      return {
        headers: new HttpHeaders({
          'Content-type': 'application/json', // application/msgpack
          [this.CSRF_HEADER_KEY]: csrf || 'conn',
        }),
        withCredentials: true,
        //responseType: 'arraybuffer' as 'arraybuffer'
      };
    });
  }

  private resetCSRFToken(): Promise<any> {
    return this.http
      .get<any>(this.environmentService.url_api + '/r/csrf', {
        withCredentials: true,
      })
      .toPromise()
      .then(() => {
        return this.cookie.get(this.CSRF_KEY).then((csrf) => {
          console.log('new csrf');
          return csrf;
        });
      });
  }

  public getCSRFToken(): Promise<any> {
    console.log('get csrf');
    return this.cookie.get(this.CSRF_KEY).then((csrf) => {
      if (csrf) {
        return csrf;
      } else {
        return this.resetCSRFToken().then((token) => {
          return this.cookie.get(this.CSRF_KEY);
        });
      }
    });
  }

  // for 2FA
  private generateOtpSalt() {
    return crypto.getRandomValues(new Uint8Array(16));
  }

  private handleHttpPostTwoFactorAuth(
    url: string,
    params: Object,
    requestResponse: Object
  ): Promise<any> {
    return new Promise((resolve, reject) => {
      // do we need a 2FA or not
      if (requestResponse && requestResponse.hasOwnProperty(this.TWO_FACTOR_AUTH_RESPONSE_PROP)) {
        let TwoFAPart = requestResponse[this.TWO_FACTOR_AUTH_RESPONSE_PROP];

        if (TwoFAPart.mode == TwoFactorMode.CODE) {
          if (TwoFAPart.error == TwoFactorAuthResponse.PREREQUISITE) {
            // prepare the server for the 2FA
            let otpSalt = this.generateOtpSalt();
            params[this.TWO_FACTOR_AUTH_OTP_SALT_PROP] = base64.stringify(otpSalt);

            this.makeAjaxPost(url, params, false).then((result) => {
              this.handleHttpPostTwoFactorAuth(url, params, result)
                .then((promiseResult) => {
                  resolve(promiseResult);
                })
                .catch(reject);
            });
          } else if (TwoFAPart.error == TwoFactorAuthResponse.REQUIRED) {
            // we prepared the server for the request, now use the code from the email

            // use a dialog for asking the code
            const dialogRef: MatDialogRef<TwoFactorDialogComponent, DialogData> =
              this.dialog.open<TwoFactorDialogComponent>(TwoFactorDialogComponent);

            dialogRef.afterClosed().subscribe((dialogResult) => {
              if (dialogResult?.code) {
                params[this.TWO_FACTOR_AUTH_CODE_PROP] = dialogResult.code;

                this.makeAjaxPost(url, params, false).then((result) => {
                  this.handleHttpPostTwoFactorAuth(url, params, result)
                    .then((promiseResult) => {
                      resolve(promiseResult);
                    })
                    .catch(reject);
                });
              } else {
                // pressed cancel on the dialog
                reject(new Error('Cancelled dialog'));
              }
            });
          } else if (TwoFAPart.error == TwoFactorAuthResponse.DENIED) {
            reject(new Error('Wrong code'));
          }
        } else if (
          TwoFAPart.mode == TwoFactorMode.NONE &&
          TwoFAPart.error == TwoFactorAuthResponse.DENIED &&
          TwoFAPart.detail == TwoFactorDetail.LOCKOUT
        ) {
          console.warn('Too many unsuccessful verification attempts.', TwoFAPart);
          reject(new Error('Too many unsuccessful verification attempts.'));
        } else {
          console.error('need to implement this 2FA mode', TwoFAPart.mode, TwoFAPart);
          reject(new Error('need to implement this 2FA mode: ' + TwoFAPart.mode));
        }
      } else {
        resolve(requestResponse);
      }
    });
  }

  private handleSocketTwoFactorAuth(param: QueryParam, requestResponse: Object): Promise<any> {
    if (!this.isConnected) return Promise.reject('Connection lost');

    return new Promise((resolve, reject) => {
      // do we need a 2FA or not
      if (requestResponse.hasOwnProperty(this.TWO_FACTOR_AUTH_RESPONSE_PROP)) {
        let TwoFAPart = requestResponse[this.TWO_FACTOR_AUTH_RESPONSE_PROP];

        if (TwoFAPart.mode == TwoFactorMode.CODE) {
          if (TwoFAPart.error == TwoFactorAuthResponse.PREREQUISITE) {
            // prepare the server for the 2FA
            let otpSalt = this.generateOtpSalt();
            param.variables[this.TWO_FACTOR_AUTH_OTP_SALT_PROP] = otpSalt;

            this.getWebsocketClient(param.useSecondarySocket).then((wsClient) => {
              wsClient
                .request({
                  query: this.parseQueryType(param.query),
                  variables: param.variables,
                })
                .subscribe({
                  next: (result) => {
                    this.handleSocketTwoFactorAuth(param, result['data'][param.query.endpoint])
                      .then((promiseResult) => {
                        resolve(promiseResult);
                      })
                      .catch(reject);
                  },
                  error: reject,
                });
            });
          } else if (TwoFAPart.error == TwoFactorAuthResponse.REQUIRED) {
            // we prepared the server for the request, now use the code from the email

            let DialogComponent =
              param.query.endpoint === deleteAccountQuery.endpoint
                ? TwoFactorAlertDialogComponent
                : TwoFactorDialogComponent;

            // use a dialog for asking the code
            const dialogRef: MatDialogRef<TwoFactorDialogComponent, DialogData> =
              this.dialog.open<TwoFactorDialogComponent>(DialogComponent);

            dialogRef.afterClosed().subscribe((dialogResult) => {
              if (dialogResult.code) {
                const waitingDialogRef =
                  param.query.endpoint === deleteAccountQuery.endpoint
                    ? this.dialogService.openWaitingDialog(
                        marker('Please wait'),
                        marker('Waiting for account deletion')
                      )
                    : null;
                param.variables[this.TWO_FACTOR_AUTH_CODE_PROP] = dialogResult.code;

                this.getWebsocketClient(param.useSecondarySocket).then((wsClient) => {
                  wsClient
                    .request({
                      query: this.parseQueryType(param.query),
                      variables: param.variables,
                    })
                    .subscribe({
                      next: (result) => {
                        this.handleSocketTwoFactorAuth(param, result['data'][param.query.endpoint])
                          .then((promiseResult) => {
                            resolve(promiseResult);
                          })
                          .catch(reject)
                          .finally(() => {
                            if (waitingDialogRef) waitingDialogRef.close();
                          });
                      },
                      error: reject,
                    });
                });
              } else {
                // pressed cancel on the dialog
                reject(new Error('Cancelled dialog'));
              }
            });
          } else if (TwoFAPart.error == TwoFactorAuthResponse.DENIED) {
            reject(new Error('Wrong code'));
          }
        } else if (
          TwoFAPart.mode == TwoFactorMode.NONE &&
          TwoFAPart.error == TwoFactorAuthResponse.DENIED &&
          TwoFAPart.detail == TwoFactorDetail.LOCKOUT
        ) {
          console.warn('Too many unsuccessful verification attempts.', TwoFAPart);
          reject(new Error('Too many unsuccessful verification attempts.'));
        } else {
          console.error('need to implement this 2FA mode', TwoFAPart.mode, TwoFAPart);
          reject(new Error('need to implement this 2FA mode: ' + TwoFAPart.mode));
        }
      } else {
        resolve(requestResponse);
      }
    });
  }

  public closeConnection() {
    if (this.wsClientPrimary) {
      console.log('connection close primary');

      this.currOnConnectedSubscription();
      this.currOnDisconnectedSubscription();
      this.wsClientPrimary.stop();
      this.wsClientPrimary = null;
      this.primaryWebSocketCreatingInProgress = null;
    }
    if (this.wsClientSecondary) {
      console.log('connection close secondary');
      this.wsClientSecondary.stop();
      this.wsClientSecondary = null;
      this.secondaryWebSocketCreatingInpProgress = null;
    }
    this.isConnected = false;
  }

  public startConnection(): Promise<void> {
    return this.getWebsocketClient().then((client) => {
      console.log('start ws client');
      //client.start();
      return;
    });
  }

  public isWebsocketConnected(): boolean {
    return this.isConnected;
  }

  private primaryWebSocketCreatingInProgress: Promise<WebSocketClient>;
  private secondaryWebSocketCreatingInpProgress: Promise<WebSocketClient>;
  private getWebsocketClient(useSecondarySocket: boolean = false): Promise<WebSocketClient> {
    if (useSecondarySocket) {
      if (this.wsClientSecondary) {
        return Promise.resolve(this.wsClientSecondary);
      } else if (this.secondaryWebSocketCreatingInpProgress) {
        return Promise.resolve(this.secondaryWebSocketCreatingInpProgress);
      } else {
        return this.createSecondaryWebsocket(this.environmentService.url_sws).then((client) => {
          this.wsClientSecondary = client;
          this.secondaryWebSocketCreatingInpProgress = null;
          return client;
        });
      }
    } else {
      if (this.wsClientPrimary) {
        return Promise.resolve(this.wsClientPrimary);
      } else if (this.primaryWebSocketCreatingInProgress) {
        return Promise.resolve(this.primaryWebSocketCreatingInProgress);
      } else {
        console.log('create new primary ws');
        return this.createPrimaryWebsocket(this.environmentService.url_ws).then((client) => {
          this.primaryWebSocketCreatingInProgress = null;

          return this.query({
            query: pingQuery,
          }).then((ping) => {
            this.pingTime = Math.round(ping['time'] * 1000);
            this.peformanceStart = performance.now();
            console.log('conn :4 - ws ping confirmed');

            let cbCopy = [...this.onWSConnectedCallbacks];
            cbCopy.forEach((cb) => {
              cb();
            });
            return client;
          });
        });
      }
    }
  }

  private currOnDisconnectedSubscription;
  private currOnConnectedSubscription;
  private createPrimaryWebsocket(url): Promise<WebSocketClient> {
    return new Promise((resolve, reject) => {
      let debug = false;
      let client = new WebSocketClient(
        url,
        debug
          ? (data) => {
              let decodedData = msgpack.decode(data);
              console.log('REST decode', decodedData);
              return decodedData;
            }
          : msgpack.decode,
        debug
          ? (data) => {
              console.log('REST encode', data);
              return msgpack.encode(data);
            }
          : msgpack.encode,
        () => {
          return this.getCSRFToken().then((csrf) => {
            return {
              headers: {
                [this.CSRF_HEADER_KEY]: csrf,
              },
            };
          });
        },
        undefined,
        undefined,
        undefined,
        true
      );

      this.currOnDisconnectedSubscription = client.onDisconnected(
        (connected: boolean, opened: boolean, close_code?: number) => {
          console.log('ws disconnected', connected, opened, close_code);
          // ws can be closed by the server before the client reaches "open" state, so these handlers must always run
          if (close_code === SpecificWebsocketError.CSRF) {
            if (!this.isCSRFTokenPending) {
              this.isCSRFTokenPending = true;
              this.resetCSRFToken().finally(() => {
                setTimeout(() => {
                  this.isCSRFTokenPending = false;
                }, 1000); // wait for SubscriptionClient repair
              });
            }
          } else if (close_code == SpecificWebsocketError.AUTH_CLOSE) {
            let cbCopy = [...this.onWSAuthClosedCallbacks];
            cbCopy.forEach((cb) => {
              cb();
            });
          }
          // call disconnect callbacks (cleanup pair of connect after open)
          if (opened) {
            this.requestQueueHandler.reset();
            let cbCopy = [...this.onWSDisconnectedCallbacks];
            cbCopy.forEach((cb) => {
              cb(close_code);
            });
          }
        }
      );

      client.onConnected(() => {
        console.log('ws connected');
      });

      /*client.onState((st) => {
        console.log('ws state', st);
      });*/

      let firstStart = true;

      this.currOnConnectedSubscription = client.onOpen(() => {
        console.log('ws onopen');
        this.isConnected = true;

        if (firstStart) {
          firstStart = false;
          this.primaryWebSocketCreatingInProgress = null;
          resolve(client);
        } else {
          // reconnect
          console.log('ping recall after reconnect');
          this.query({
            query: pingQuery,
          }).then((ping) => {
            this.pingTime = Math.round(ping['time'] * 1000);
            this.peformanceStart = performance.now();

            console.log('reconn: ws ping confirmed');

            let cbCopy = [...this.onWSConnectedCallbacks];
            cbCopy.forEach((cb) => {
              cb();
            });
          });
        }
      });

      this.wsClientPrimary = client;
      client.start();
    });
  }

  //TODO: this can be deleted when the https://dc.clarabot.zrt/T162 task is finished. This is a workaround to that issue.
  public manuallyInitSessionThrow(): void {
    let cbCopy = [...this.onWSAuthClosedCallbacks];
    cbCopy.forEach((cb) => {
      cb();
    });
  }

  private createSecondaryWebsocket(url): Promise<WebSocketClient> {
    return new Promise((resolve, reject) => {
      let debug = false;
      let client = new WebSocketClient(
        url,
        debug
          ? (data) => {
              let decodedData = msgpack.decode(data);
              console.log('REST2 decode', decodedData);
              return decodedData;
            }
          : msgpack.decode,
        debug
          ? (data) => {
              console.log('REST2 encode', data);
              return msgpack.encode(data);
            }
          : msgpack.encode,
        () => {
          return this.getCSRFToken().then((csrf) => {
            return {
              headers: {
                [this.CSRF_HEADER_KEY]: csrf,
              },
            };
          });
        },

        undefined,
        undefined,
        undefined,
        true
      );

      /*client.onState((st) => {
        console.log('ws2 state', st);
      });*/

      let sub = client.onOpen(() => {
        console.log('ws2 onopen');
        sub();
        resolve(client);
      });

      client.start();
    });
  }

  /*public makeDebugAjaxPost(url: string, params: Object){
    let p = this.http
      .post(
        this.environmentService.url_api + "/" + url,
        params //new Uint8Array(param).buffer,
        {
          headers: new HttpHeaders({
            "Content-type": "application/json",
            [this.CSRF_HEADER_KEY]: this.cookie.get(this.CSRF_KEY) || "conn",
          }),
          responseType: 'arraybuffer' as 'arraybuffer'
        }
      )
      .toPromise()
      .then((res) => {
        return res;
      })
      .catch((err) => {
        if (err["status"] === 403) {
          return this.getCSRFToken().then((token) => {
            return this.makeDebugAjaxPost(url, params);
          });
        } else {
          throw err;
        }
      });

    return p;
  }*/

  public makeAjaxPost(
    url: string,
    params: Object,
    checkTwoFactor: boolean = true,
    autoRepairCSRF: boolean = true
  ) {
    return this.getHttpHeaderOptions().then((options) => {
      let p = this.http
        .post(
          this.environmentService.url_api + '/' + url,
          params /*new Uint8Array(param).buffer*/,
          options
        )
        .toPromise()
        .then((res) => {
          return res;
        })
        .catch((err) => {
          if (err['status'] === 403 && autoRepairCSRF) {
            return this.getCSRFToken().then((token) => {
              return this.makeAjaxPost(url, params, checkTwoFactor, false);
            });
          } else {
            throw err;
          }
        });

      if (checkTwoFactor) {
        return p.then((result) => {
          return this.handleHttpPostTwoFactorAuth(url, params, result);
        });
      } else {
        return p;
      }
    });
  }

  /*public makeDebugAjaxGet(url: string){
    return this.http
    .get(
      this.environmentService.url_api + "/" + url,
      {
        headers: new HttpHeaders({
          "Content-type": "application/msgpack",
          [this.CSRF_HEADER_KEY]: this.cookie.get(this.CSRF_KEY) || "conn",
        }),
        responseType: 'arraybuffer' as 'arraybuffer'
      }
    )
    .toPromise()
    .then((res) => {
      return msgpack.decode(new Uint8Array(res));
    })
    .catch((err) => {
      if (err["status"] === 403) {
        return this.getCSRFToken().then((token) => {
          return this.makeDebugAjaxGet(url);
        });
      } else {
        throw err;
      }
    });
  }*/

  public makeAjaxGet(url: string) {
    return this.getHttpHeaderOptions().then((options) => {
      return this.http
        .get(this.environmentService.url_api + '/' + url, options)
        .toPromise()
        .then((res) => {
          return res;
        })
        .catch((err) => {
          if (err['status'] === 403) {
            return this.getCSRFToken().then((token) => {
              return this.makeAjaxGet(url);
            });
          } else {
            throw err;
          }
        });
    });
  }

  public getLoginSalt(email) {
    // rest/login-salt
    return this.makeAjaxPost('r/login-salt', {
      email: email,
    });
  }

  public login(
    email: string,
    authKey: string,
    sessionKey: string,
    otpSalt?: string,
    tfaCode?: string,
    rememberMe: boolean = false
  ) {
    // rest/login

    let params = {
      email,
      authentication_key: authKey,
      session_enc_key: sessionKey,
    };

    if (otpSalt) {
      params['otpSalt'] = otpSalt;
    }

    if (tfaCode) {
      params['2faCode'] = tfaCode;
    }

    if (rememberMe) {
      params['remember_me'] = 1;
    }

    if (AppStorage.getItem('lang')) {
      params['locale'] = AppStorage.getItem('lang');
    }

    return this.makeAjaxPost('r/login', params);
  }

  public logout(): Promise<void> {
    return this.makeAjaxPost('r/logout', {}).then(() => {
      this.requestQueueHandler.reset();
      return;
    });
  }

  /**
   * Register prepare
   */
  public register(
    email: string,
    client_salt_input: string,
    encrypted_master_key: string,
    hashed_authentication_key: string
  ) {
    let params = {
      email,
      client_salt_input,
      encrypted_master_key,
      hashed_authentication_key,
    };

    if (AppStorage.getItem('lang')) {
      params['locale'] = AppStorage.getItem('lang');
    }

    return this.makeAjaxPost('r/register-prepare', params);
  }

  public registerLoginSalt(registerToken: string, email): Promise<{ salt: string }> {
    return this.makeAjaxPost('r/register-login-salt', {
      register_token: registerToken,
      email,
    });
  }

  public registerLoginCheck(
    registerToken,
    email,
    authKey
  ): Promise<{ account_id: string; encrypted_master_key: string }> {
    return this.makeAjaxPost('r/register-login-check', {
      register_token: registerToken,
      email,
      authentication_key: authKey,
    });
  }

  public registerChallenge(
    registerToken: string,
    authKey: string,
    keyringInfo: create_account_register_keyring_request_result
  ): Promise<{ public_key: string; challenge: string }> {
    let params = Object.assign({}, keyringInfo);
    params['register_token'] = registerToken;
    params['authentication_key'] = authKey;

    return this.makeAjaxPost('r/register-challenge', params);
  }

  public registerConfirm(confirmRequest: create_account_register_confirm_request_result) {
    return this.makeAjaxPost('r/register-confirm', confirmRequest);
  }

  public accountRecoveryPrepare(email: string) {
    let params = {
      email,
    };

    if (AppStorage.getItem('lang')) {
      params['locale'] = AppStorage.getItem('lang');
    }

    return this.makeAjaxPost('r/recover-prepare', params);
  }

  public accountRecoveryChallenge(
    token: string
  ): Promise<{ account_id: string; signing_secret_ec: string }> {
    return this.makeAjaxPost('r/recover-challenge', { recovery_token: token });
  }

  public accountRecoverConfirm(param: create_account_recovery_confirm_request_type) {
    return this.makeAjaxPost('r/recover-confirm', param);
  }

  /**
   * Start a query request, use cache if can
   * @param {queryParam} params - check queryParam doc for details
   * @return Promise with the result
   */
  query(params: QueryParam): Promise<any> {
    if (!this.isConnected) {
      console.error('could not use query without stable connection', params.query);
      return Promise.reject('Connection lost');
    }

    let param = Object.assign(Object.assign({}, this.defaultQueryParam), params);
    if (param.query.operation != OperationType.QUERY) {
      console.warn('wrong operation type', param.query.endpoint, param.query.operation);
    }
    param.query.operation = OperationType.QUERY;

    return new Promise((resolve, reject) => {
      param
        .encrypt(param.variables, param.extra)
        .then((encryptedVariables) => {
          let request: Request = { query: param.query, variables: encryptedVariables };

          let cacheState = this.cache.getRequestCacheState(request);

          if (cacheState.shouldFetch) {
            if (this.requestQueueHandler.isRequestAlreadyFetched(cacheState.cacheKey)) {
              this.requestQueueHandler.registerToRequestQueue(cacheState.cacheKey, resolve);
            } else {
              this.requestQueueHandler.registerToRequestQueue(cacheState.cacheKey, resolve);

              this.getWebsocketClient(param.useSecondarySocket).then((wsClient) => {
                try {
                  /**
                   * Get the handlers before the request, so when we reset the RQ
                   * after a dc it wont lost
                   */
                  let handlers = this.requestQueueHandler.getRequestQueue(cacheState.cacheKey);
                  wsClient
                    .request({
                      query: this.parseQueryType(param.query),
                      variables: encryptedVariables,
                    })
                    .subscribe({
                      next: (result) => {
                        // cut the query call tree structure, and return with the data only
                        result = this.getDataFromQueryResponse(params.query, result);

                        try {
                          param
                            .decrypt(result, param.variables, param.extra)
                            .then((result) => {
                              this.cache.mergeRequestResult(request, result);

                              //let cachedData = this.cache.getCachedData(cacheKey, query, variables)
                              handlers &&
                                handlers.forEach((cb) => {
                                  cb(result); // the current resolver
                                });
                              this.requestQueueHandler.emptyRequestQueue(cacheState.cacheKey);
                            })
                            .catch((e) => {
                              reject(e);
                            });
                        } catch (e) {
                          reject(e);
                        }
                      },
                      error: (err) => {
                        this.requestQueueHandler.emptyRequestQueue(cacheState.cacheKey);
                        if (err.message == 'Invalid endpoint name!') {
                          console.warn(
                            'server query endpoint does not exist: ',
                            param?.query?.endpoint
                          );
                        }
                        reject(err);
                      },
                    });
                } catch (e) {
                  reject(e);
                }
              });
            }
          } else {
            // simulate the result of a normal request cycle, but give the result immediatly,
            // because we can read it from the cache

            if (cacheState.data === undefined) {
              return reject(
                'Can not find data in the cache but this query was an offline query: ' +
                  request.query.endpoint
              );
            } else {
              return resolve(cacheState.data);
            }
          }
        })
        .catch((err) => {
          reject(err);
        });
    });
  }

  /**
   * Start a mutate request
   * @param {queryParam} params - check queryParam doc for details
   * @return Promise with the result
   */
  mutate(params: QueryParam): Promise<any> {
    if (!this.isConnected) {
      console.error('could not use mutate without stable connection', params.query);
      return Promise.reject('Connection lost');
    }

    let param = Object.assign(Object.assign({}, this.defaultQueryParam), params);
    if (param.query.operation != OperationType.MUTATION) {
      console.warn('wrong operation type', param.query.endpoint, param.query.operation);
    }
    param.query.operation = OperationType.MUTATION;

    return new Promise((resolve, reject) => {
      param
        .encrypt(param.variables, param.extra)
        .then((encryptedVariables) => {
          this.getWebsocketClient(param.useSecondarySocket)
            .then((wsClient) => {
              try {
                let sub = wsClient
                  .request({
                    query: this.parseQueryType(param.query),
                    variables: encryptedVariables,
                  })
                  .subscribe({
                    next: (result) => {
                      // cut the query call tree structure, and return with the data only
                      result = this.getDataFromQueryResponse(params.query, result);

                      this.handleSocketTwoFactorAuth(params, result)
                        .then((result) => {
                          try {
                            param
                              .decrypt(result, param.variables, param.extra)
                              .then((result) => {
                                this.cache.mergeRequestResult(
                                  { query: param.query, variables: encryptedVariables },
                                  result
                                );
                                resolve(result);
                                sub.unsubscribe();
                              })
                              .catch((e) => {
                                reject(e);
                              });
                          } catch (e) {
                            reject(e);
                          }
                        })
                        .catch(reject);
                    },
                    error: reject,
                  });
              } catch (e) {
                reject(e);
              }
            })

            .catch((err) => {
              reject(err);
            });
        })
        .catch((err) => {
          reject(err);
        });
    });
  }

  /**
   * Register a subscription request
   * @param {queryParam} params - check queryParam doc for details
   * @return Observer
   */
  subscribe(params: QueryParam) {
    if (!this.isConnected) {
      console.error('could not use subscribe without stable connection', params.query);
      return new Observable((observer) => {
        observer.error('Connection lost');
        return () => {};
      });
    }

    let param = Object.assign(Object.assign({}, this.defaultQueryParam), params);
    if (param.query.operation != OperationType.SUBSCRIPTION) {
      console.warn('wrong operation type', param.query.endpoint, param.query.operation);
    }
    param.query.operation = OperationType.SUBSCRIPTION;

    return new Observable((observer) => {
      let sub;
      let offError;
      let offDisconnect;

      let errorHandling = (err) => {
        observer.error(err);
        //sub && sub.unsubscribe();
        offError && offError(); // must or memory leak in request cuz of scope
        offDisconnect && offDisconnect();
      };

      param
        .encrypt(param.variables, param.extra)
        .then((encryptedVariables) => {
          this.getWebsocketClient(param.useSecondarySocket)
            .then((wsClient) => {
              offError = wsClient.onError((err) => {
                console.error('<- ws client error', err);
                errorHandling(err);
              });
              offDisconnect = wsClient.onDisconnected(
                (connected: boolean, opened: boolean, close_code?: number) => {
                  console.error('<- ws client dc', connected, opened, close_code);
                  errorHandling(close_code);
                }
              );
              try {
                sub = wsClient
                  .request({
                    query: this.parseQueryType(param.query),
                    variables: encryptedVariables,
                  })
                  .subscribe({
                    next: async (result) => {
                      // cut the query call tree structure, and return with the data only
                      result = this.getDataFromQueryResponse(params.query, result);

                      try {
                        await param
                          .decrypt(result, param.variables, param.extra)
                          .then((result) => {
                            this.cache.mergeRequestResult(
                              { query: param.query, variables: encryptedVariables },
                              result
                            );
                            observer.next(result);
                          })
                          .catch((e) => {
                            errorHandling(e);
                          });
                      } catch (e) {
                        errorHandling(e);
                      }
                    },
                    error: (e) => {
                      errorHandling(e);
                    },
                    complete: () => {
                      // can not end here, because the last x part still in decryption
                      //observer.complete();
                      offError && offError();
                      offDisconnect && offDisconnect();
                    },
                  });
              } catch (e) {
                errorHandling(e);
              }
            })
            .catch((err) => {
              errorHandling(err);
            });
        })
        .catch((err) => {
          errorHandling(err);
        });

      // observer unsubscribe result
      return () => {
        offDisconnect && offDisconnect();
        offError && offError();

        if (sub) {
          //sub.unsubscribe();
        } else {
          setTimeout(() => {
            // if you unsubscribed immediatly, you couldn't reset the inner observer
            // so just to be sure
            //sub && sub.unsubscribe();
          }, 1000);
        }
      };
    });
  }
}
