import { Injectable } from '@angular/core';
import {
  roomMessagesQuery,
  dialogueMessagesQuery,
  createDialogueMessageQuery,
  deleteDialogueMessageQuery,
  editDialogueMessageQuery,
  seenDialogueMessageQuery,
  getDialogueDataQuery,
  editDialoguePlacementQuery,
  createDialogueQuery,
  sendDialogueTypingQuery,
  getDialogueDetailQuery,
  getSeenIdsByMessageQuery,
  reactDialogueMessageQuery,
  dialogueMessagesByIdQuery,
  messageQuery,
} from '../server-services/querys';
import { ServerRestApiService } from '../services/server-rest-api.service';
import { AccountService } from './account.service';
import { PeerAccountKeyring } from '../crypto/keyring/account_peer';
import { AuthService } from './auth.service';
import { DIALOGUE_CRYPTO_CONTEXT } from '../crypto/context/dialogue/__init__';
import { getTimeFromId } from '../crypto/context/timestamp';
import msgpack from 'msgpack-lite';
import { DialogueConfigDumpArgs, DialogueConfigLoadArgs } from '../crypto/context/dialogue/args';
import { ResourceBase } from './resource-base';
import {
  DialogueDetail,
  DialogueRecord,
  ReactionEnum,
  MessageRecord,
} from './query-records/room-records';
import { ID } from './query-records/common-records';
import { marker } from '@biesbjerg/ngx-translate-extract-marker';
import { WorkspaceQueryDialogueRecord } from './query-records/workspace-records';
import { DialogService } from '../services/dialog.service';
import { SnackBarService } from '../services/snackbar.service';
import { CacheService } from '../services/cache/cache.service';

@Injectable({
  providedIn: 'root',
})
export class DialogueService implements ResourceBase {
  /**
   * Map<resourceId, event handlers>
   * This collection is wrap all of the subscription handler for a specific resource (key)
   * We separate the CMD+Typing callbacks
   * You can use the subscription to unsubscribe an apollo subscription
   */

  private peerKeyrings = {};

  constructor(
    private serverRestApiService: ServerRestApiService,
    private accountService: AccountService,
    private authService: AuthService,
    private dialogService: DialogService,
    private snackbarService: SnackBarService,
    private cacheService: CacheService
  ) {}

  public getKeyring(resourceId): Promise<PeerAccountKeyring> {
    if (this.peerKeyrings[resourceId]) {
      return Promise.resolve(this.peerKeyrings[resourceId]);
    } else {
      return this.accountService.getPeerKeyring(resourceId).then((peer_kr) => {
        this.peerKeyrings[resourceId] = peer_kr;
        return peer_kr;
      });
    }
  }

  // direct api-calls --------------------------------------

  /**
   * @param id
   * @returns
   */
  public getData(id): Promise<DialogueRecord> {
    // check it from the cache
    return this.serverRestApiService
      .query({
        query: getDialogueDataQuery,
        variables: {
          id,
        },
      })
      .catch((err) => {
        console.warn('could not find the dialogue data, try to load it from the blocked list');
        // create or get if exists
        return this.createDialogue(id).then(() => {
          // retry to get it from cache after creation
          return this.serverRestApiService.query({
            query: getDialogueDataQuery,
            variables: {
              id,
            },
          });
        });
      });
  }

  public createDialogue(peerId: ID): Promise<WorkspaceQueryDialogueRecord> {
    return new Promise((resolve, reject) => {
      this.serverRestApiService
        .mutate({
          query: createDialogueQuery,
          variables: {
            peerId,
          },
        })
        .then((res) => {
          // this generate a dialogue, but it comes only via the subscription handler
          // we have to wait, while this merges into the cache, because the "getDialogue"
          // what will be called after this is an offline query
          let attempt = 0;

          let checkForDialogue = () => {
            this.getData(peerId)
              .then(() => {
                resolve(res);
              })
              .catch(() => {
                if (attempt < 30) {
                  attempt++;
                  setTimeout(checkForDialogue, 100);
                } else {
                  console.error('the dialogue is not loaded into the cache', peerId);
                  reject();
                }
              });
          };
          checkForDialogue();
        })
        .catch(reject);
    });
  }

  private decryptMessage(data, variables): Promise<Object> {
    //console.log('load', data, variables);

    let messages = data;

    let promises: Promise<void>[] = [];

    messages &&
      messages.forEach((msg) => {
        promises.push(
          this.getKeyring(variables['peerId']).then((peer_kr) => {
            return DIALOGUE_CRYPTO_CONTEXT.load(
              new DialogueConfigLoadArgs(
                msg['content'],
                this.authService.getSelfAccountKeyring(),
                peer_kr,
                getTimeFromId(msg['id'])
              )
            )
              .then((loadedData) => {
                msg['content'] = msgpack.decode(loadedData)[1];
                return;
              })
              .catch(() => {
                msg['error'] = true;
                msg['errorMessage'] = marker('Error: Can not decrypt message');
                return;
              });
          })
        );
      });

    return Promise.all(promises).then(() => {
      return data;
    });
  }

  /**
   * Note: Be sure, you have already called the getRoomData - need for creating the room_kr
   */
  public getLastMessages(resourceId): Promise<MessageRecord[]> {
    return this.serverRestApiService
      .query({
        query: dialogueMessagesQuery,
        variables: {
          peerId: resourceId,
          pivot: null,
          after: false,
          count: 20,
          prepareSub: true,
        },
        decrypt: this.decryptMessage.bind(this),
      })
      .then((data) => {
        return data;
      });
  }

  public getMoreMessages(resourceId, cursorMessageId): Promise<MessageRecord[]> {
    return this.serverRestApiService.query({
      query: dialogueMessagesQuery,
      variables: {
        peerId: resourceId,
        pivot: cursorMessageId,
        after: false,
        count: 20,
        prepareSub: false,
      },
      decrypt: this.decryptMessage.bind(this),
    });
  }

  public sendMessage(resourceId, content): Promise<string> {
    return this.serverRestApiService
      .mutate({
        query: createDialogueMessageQuery,
        variables: {
          peerId: resourceId,
          content,
        },
        encrypt: this.encryptMessage.bind(this),
      })
      .catch((err) => {
        if (err.message == 'Blocked by self!') {
          this.snackbarService.showSnackbar(marker('Can not send message to a blocked user.'));
        } else if (err.message == 'Blocked by peer!') {
          this.snackbarService.showSnackbar(
            marker('You can not start conversation with this user.')
          );
        } else {
          console.error('error during dialogue message send', err);
          throw err;
        }
      });
  }

  public sendTyping(resourceId: string): Promise<boolean> {
    return this.serverRestApiService
      .mutate({
        query: sendDialogueTypingQuery,
        variables: {
          peerId: resourceId,
        },
      })
      .catch((err) => {
        if (err.message == 'Blocked by self!') {
          this.snackbarService.showSnackbar(marker('Can not send message to a blocked user.'));
        } else if (err.message == 'Blocked by peer!') {
          this.snackbarService.showSnackbar(
            marker('You can not start conversation with this user.')
          );
        } else {
          console.error('error during dialogue message send', err);
          throw err;
        }
      });
  }

  private encryptMessage(data): Promise<Object> {
    return this.getKeyring(data['peerId']).then((peer_kr) => {
      return DIALOGUE_CRYPTO_CONTEXT.dump(
        new DialogueConfigDumpArgs(
          msgpack.encode([1, data['content']]),
          this.authService.getSelfAccountKeyring(),
          peer_kr,
          data.messageId ? getTimeFromId(data.messageId) : this.serverRestApiService.makeTimestamp()
        )
      ).then((content) => {
        data['content'] = content;
        return data;
      });
    });
  }

  public deleteMessage(resourceId, messageId): Promise<any> {
    return this.serverRestApiService.mutate({
      query: deleteDialogueMessageQuery,
      variables: {
        peerId: resourceId,
        messageId,
      },
    });
  }

  public editMessage(resourceId, messageId, content): Promise<any> {
    return this.serverRestApiService.mutate({
      query: editDialogueMessageQuery,
      variables: {
        peerId: resourceId,
        messageId,
        content,
      },
      encrypt: this.encryptMessage.bind(this),
    });
  }

  public seenMessage(resourceId, messageId): Promise<any> {
    return this.serverRestApiService.mutate({
      query: seenDialogueMessageQuery,
      variables: {
        peerId: resourceId,
        seenId: messageId,
      },
    });
  }

  public getDetail(userId): Promise<DialogueDetail> {
    return this.serverRestApiService.query({
      query: getDialogueDetailQuery,
      variables: {
        id: userId,
      },
    });
  }

  public getSeenIdsByMessage(resourceId, messageId): Promise<ID[]> {
    return this.serverRestApiService.query({
      query: getSeenIdsByMessageQuery,
      variables: { resourceId, messageId },
    });
  }

  public reactMessage(
    resourceId: any,
    messageId: any,
    reaction: ReactionEnum | null
  ): Promise<void> {
    return this.serverRestApiService.mutate({
      query: reactDialogueMessageQuery,
      variables: {
        peerId: resourceId,
        messageId,
        reaction,
      },
    });
  }

  public getMessagesById(resourceId, messageIds: ID[]): Promise<MessageRecord[]> {
    let resultMessages: (MessageRecord | null)[] = [];

    let notFoundPositions: number[] = [];
    let notFoundIds: ID[] = [];

    for (let i = 0; i < messageIds.length; i++) {
      resultMessages[i] = this.getOneMessageByIdFromCache(resourceId, messageIds[i]);
      if (!resultMessages[i]) {
        notFoundPositions.push(i);
        notFoundIds.push(messageIds[i]);
      }
    }

    if (notFoundIds.length == 0) {
      return Promise.resolve(resultMessages);
    }

    return this.serverRestApiService
      .query({
        query: dialogueMessagesByIdQuery,
        variables: {
          roomId: resourceId,
          messageIds: notFoundIds,
        },
      })
      .then((messages: MessageRecord[]) => {
        for (let i = 0; i < notFoundIds.length; i++) {
          resultMessages[notFoundPositions[i]] = messages[i];
        }

        return resultMessages;
      });
  }

  /**
   * Call getMessagesById if you need collection
   */
  public getOneMessageById(resourceId, messageId: ID): Promise<MessageRecord> {
    return this.getMessagesById(resourceId, [messageId]).then((allMessage) => {
      return allMessage[0];
    });
  }

  private getOneMessageByIdFromCache(resourceId, messageId): MessageRecord | null {
    return this.cacheService.getCacheDataByRequest({
      query: messageQuery,
      variables: { resourceId, messageId },
    });
  }
}
