import { Injectable } from '@angular/core';
import {
  getMeQuery,
  getAccountQuery,
  getAccountKeysQuery,
  modifyAvatarNameQuery,
  modifyAvatarQuery,
  editTwoFactorModeQuery,
  prepareEditEmailQuery,
  editEmailQuery,
  getTwoFactorModeQuery,
  getAllAccountIdQuery,
  accountAvatarsQuery,
  editDialoguePolicy,
  editDialogueHidden,
} from './querys';
import { ServerRestApiService } from '../services/server-rest-api.service';
import { AuthService } from './auth.service';
import { PeerAccountKeyring } from '../crypto/keyring/account_peer';
import {
  AccountAvatarRecord,
  AccountKeysRecord,
  DialoguePolicy,
  MeRecord,
  UserRole,
} from './query-records/account-records';
import { chunk } from 'lodash';
import { CacheService } from '../services/cache/cache.service';
import { AppStorage } from 'src/app/shared/app-storage';
import { AbstractAccountKeyring } from '../crypto/keyring/account_base';
export const enum UserVerificationState {
  VERIFIED,
  NOT_VERIFIED,
  DANGEROUS,
}

@Injectable({
  providedIn: 'root',
})
export class AccountService {
  private batchAccountId: string[] = [];
  private isGetAccountQueryStarted: boolean = false;
  /**
   * [resolve, reject] callbacks pair
   */
  private getAccountCallbacks: [Function, Function][] = [];
  /**
   * waiting and collecting time before starting the request in millisec
   */
  public static BATCH_TIME_FOR_ACCOUNTS_QUERY: number = 50;

  constructor(
    private serverRestApiService: ServerRestApiService,
    private authService: AuthService,
    private cacheService: CacheService
  ) {}

  private cachedPeerKeyring: { [key: string]: PeerAccountKeyring } = {}; // accondId => peerKeyring

  public getMe(): Promise<MeRecord> {
    return this.serverRestApiService.query({ query: getMeQuery });
  }

  public getAccount(id: string): Promise<AccountAvatarRecord> {
    // return immediately when possible
    let acc = this.cacheService.getCacheDataByRequest({
      query: getAccountQuery,
      variables: { id },
    });
    if (acc) {
      return Promise.resolve(acc);
    } else {
      return this.getAccounts([id]).then((accs) => {
        return accs[id];
      });
    }
  }

  /**
   *
   * @param {string[]} accountIds one or more ids
   * @return {Promise<{[key:string]: AccountAvatarRecord}>} result
   */
  public getAccounts(accountIds: string[]): Promise<{ [key: string]: AccountAvatarRecord }> {
    let result: { [key: string]: AccountAvatarRecord } = {};
    let notFoundList: string[] = [];
    accountIds.forEach((id) => {
      let acc = <AccountAvatarRecord>(
        this.cacheService.getCacheDataByRequest({ query: getAccountQuery, variables: { id } })
      );
      if (acc) {
        result[id] = acc;
      } else {
        notFoundList.push(id);
      }
    });

    if (notFoundList.length == 0) {
      // return immediately when possible
      return Promise.resolve(result);
    } else {
      // start an async batch query
      return new Promise((resolve, reject) => {
        // merge ids
        notFoundList.forEach((id) => {
          if (!this.batchAccountId.includes(id)) {
            this.batchAccountId.push(id);
          }
        });

        // save callbacks after query response
        this.getAccountCallbacks.push([resolve, reject]);

        if (!this.isGetAccountQueryStarted) {
          this.isGetAccountQueryStarted = true;

          // wait for a little bit and call the query
          window.setTimeout(() => {
            let list = this.batchAccountId;
            let callbacks = this.getAccountCallbacks;
            this.batchAccountId = [];
            this.getAccountCallbacks = [];
            this.isGetAccountQueryStarted = false;
            this.queryAccounts(list, callbacks);
          }, AccountService.BATCH_TIME_FOR_ACCOUNTS_QUERY);
        }
      }).then((accountsFromQuery) => {
        // we can get more accounts, because it is all of the batched request's response
        notFoundList.forEach((id) => {
          result[id] = accountsFromQuery[id];
        });
        return result;
      });
    }
  }

  /**
   *
   * @param idList id list
   * @param callbacks (resolve, reject) promise callbacks
   * @returns all the batched id
   */
  public queryAccounts(idList: string[], callbacks: [Function, Function][]) {
    let batchSize: number = 500;
    let idBatches: Array<Array<string>> = chunk(idList, batchSize);
    let batchPromises: Array<Promise<AccountAvatarRecord[]>> = [];

    idBatches.forEach((idBatch, i) => {
      let delayMs = i * 200;
      let delayedQueryPromise = new Promise((resolve) => setTimeout(resolve, delayMs)).then(() => {
        return this.serverRestApiService.query({
          query: accountAvatarsQuery,
          variables: { ids: idBatch },
        });
      });

      batchPromises.push(delayedQueryPromise);
    });

    Promise.all(batchPromises)
      .then((accountBatches) => {
        let allAccountAvatars: { [key: string]: AccountAvatarRecord } = {};

        accountBatches.forEach((accountsQueryResponse) => {
          accountsQueryResponse.forEach((account) => {
            allAccountAvatars[account.id] = account;
          });
        });

        callbacks.forEach(([resolve, reject]) => {
          resolve(allAccountAvatars);
        });
      })
      .catch((e) => {
        callbacks.forEach(([resolve, reject]) => {
          reject(e);
        });
      });
  }

  /**
   * Get the all direct contact from your workspace.
   */
  public getAllAccount(): Promise<AccountAvatarRecord[]> {
    return this.serverRestApiService
      .query({
        query: getAllAccountIdQuery,
      })
      .then((ids: string[]) => {
        return this.getAccounts(ids).then((accs) => {
          return Object.values(accs);
        });
      });
  }

  public getAccountKeys(id): Promise<AccountKeysRecord> {
    return this.serverRestApiService.query({
      query: getAccountKeysQuery,
      variables: {
        id,
      },
    });
  }

  public getPeerKeyring(accountId): Promise<PeerAccountKeyring> {
    if (accountId in this.cachedPeerKeyring) {
      return Promise.resolve(this.cachedPeerKeyring[accountId]);
    } else {
      return this.getAccountKeys(accountId).then((keys) => {
        return PeerAccountKeyring.load(
          accountId,
          keys.encryptingPublicEc,
          keys.encryptingPublicEcSignature,
          keys.signingPublicEc,
          keys.trust,
          this.authService.getSelfAccountKeyring()
        ).then((peer_kr) => {
          this.cachedPeerKeyring[accountId] = peer_kr;
          return peer_kr;
        });
      });
    }
  }

  public getAvatar(userId: string): Promise<Object> {
    return this.serverRestApiService.query({
      query: getAccountQuery,
      variables: {
        id: userId,
      },
    });
  }

  public setAvatar(avatarImage): Promise<any> {
    return this.serverRestApiService.mutate({
      query: modifyAvatarQuery,
      variables: {
        image: avatarImage,
      },
    });
  }

  public changeAvatarName(avatarName: string): Promise<boolean> {
    return this.serverRestApiService.mutate({
      query: modifyAvatarNameQuery,
      variables: {
        name: avatarName,
      },
    });
  }

  public get2FA(): Promise<any> {
    return this.serverRestApiService.query({
      query: getTwoFactorModeQuery,
    });
  }

  public set2FA(enabled: boolean): Promise<boolean> {
    var mode = enabled ? 10 : 0;
    let params = {
      mode,
    };
    if (AppStorage.getItem('lang')) {
      params['locale'] = AppStorage.getItem('lang');
    }

    return this.serverRestApiService.mutate({
      query: editTwoFactorModeQuery,
      variables: params,
    });
  }

  public prepareEditEmail(newEmail: string): Promise<boolean> {
    let params = {
      email: newEmail,
    };

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

    return this.serverRestApiService.mutate({
      query: prepareEditEmailQuery,
      variables: params,
    });
  }

  public editEmail(token: string): Promise<boolean> {
    let params = {
      token,
    };

    if (AppStorage.getItem('lang')) {
      params['locale'] = AppStorage.getItem('lang');
    }
    return this.serverRestApiService.mutate({
      query: editEmailQuery,
      variables: params,
    });
  }

  public isCurrentUserInRole(role: UserRole, me: MeRecord): boolean {
    return me.scopes.some((scope) => scope == role);
  }

  public startDeleteProcess(avatarId: string): Promise<any> {
    return this.serverRestApiService.makeAjaxPost('debug/account/delete', { id: avatarId });
  }

  public changeDialoguePolicy(policy: DialoguePolicy): Promise<any> {
    return this.serverRestApiService.mutate({
      query: editDialoguePolicy,
      variables: { policy: policy },
    });
  }

  public blockUser(userId: string): Promise<any> {
    return this.serverRestApiService.mutate({
      query: editDialogueHidden,
      variables: { peerId: userId, hidden: true },
    });
  }

  public unblockUser(userId: string): Promise<any> {
    return this.serverRestApiService.mutate({
      query: editDialogueHidden,
      variables: { peerId: userId, hidden: false },
    });
  }

  public getUserVerificationState(userId: string): Promise<UserVerificationState> {
    return this.getPeerKeyring(userId).then((peer_kr) => {
      if (peer_kr.trusted == AbstractAccountKeyring.TRUSTED_FULLY)
        return UserVerificationState.VERIFIED;
      if (peer_kr.trusted == AbstractAccountKeyring.TRUSTED_NOT)
        return UserVerificationState.DANGEROUS;
      return UserVerificationState.NOT_VERIFIED;
    });
  }
}
