import { Injectable } from '@angular/core';
import {
  chatChangesSubscription,
  createNewIdQuery,
  createRoomConfigBlockQuery,
  createRoomMessageQuery,
  createRoomQuery,
  deleteRoomMessageQuery,
  editRoomDataQuery,
  editRoomMessageQuery,
  getSeenIdsByMessageQuery,
  messageQuery,
  reactRoomMessageQuery,
  roomMessagesByIdQuery,
  roomMessagesQuery,
  seenRoomMessageQuery,
  sendDialogueTypingQuery,
  sendRoomTypingQuery,
} from '../server-services/querys';
import { ServerRestApiService } from '../services/server-rest-api.service';
import { ROOM_CRYPTO_CONTEXT } from '../crypto/context/room/__init__';
import { RoomConfigDumpArgs, RoomConfigLoadArgs } from '../crypto/context/room/args';
import { getTimeFromId } from '../crypto/context/timestamp';
import msgpack from 'msgpack-lite';
import { ResourceBase } from './resource-base';
import { RoomService } from './room.service';
import { AuthService } from './auth.service';
import { NanoService } from '../nano/nano.service';
import { AccountService } from './account.service';
import {
  DialogueDetail,
  ReactionEnum,
  MessageRecord,
  RoomDetail,
} from './query-records/room-records';
import { marker } from '@biesbjerg/ngx-translate-extract-marker';
import { ResourceType } from './query-records/sidebar-records';
import { AppStorage } from '../app-storage';
import { ACCOUNT_CRYPTO_CONTEXT } from '../crypto/context/account/__init__';
import { AccountConfigDumpArgs, AccountConfigLoadArgs } from '../crypto/context/account/args';
import { base64 } from 'rfc4648';
import { chatInputStorageKey } from './chat-service-types';
import { ID } from './query-records/common-records';
import { CacheService } from '../services/cache/cache.service';

/**
 * these event types will come from the server
 * CMD is coming from the MessageEvent, and we have an independent typingEvent
 * but we will hide these separation from the api-user layer
 */
export enum SubscriptionChatEventType {
  CREATE = 'C',
  MODIFY = 'M',
  DELETE = 'D',
  TYPING = 'T',
}

export interface ChatEventSubscriptionWrapperObject {
  // subscriptions for the specific method
  C: Function[];
  M: Function[];
  D: Function[];
  T: Function[];
  subscription: any; // observable
}

@Injectable({
  providedIn: 'root',
})
export class ChatService 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 subscriptions: Map<string, ChatEventSubscriptionWrapperObject> = new Map<
    string,
    ChatEventSubscriptionWrapperObject
  >();

  constructor(
    private serverRestApiService: ServerRestApiService,
    private roomService: RoomService,
    private authService: AuthService,
    private nanoService: NanoService,
    private accountService: AccountService,
    private cacheService: CacheService
  ) {}

  public getKeyring(resourceId) {
    return this.roomService.getKeyring(resourceId);
  }

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

  /**
   * get Data for Room. As a side effect it will save the room_kr for future requestes
   * @param id
   * @returns
   */
  public getData(id): Promise<Object> {
    return this.roomService.getRoom(id);
  }

  private decryptMessage: (data, variables) => Promise<Object> = (data, variables) => {
    if (!this.getKeyring(variables.roomId))
      throw (
        'Room keyring missing. Get the room data before calling any other api with the specific room. ' +
        variables.roomId
      );

    let messages = data;

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

    messages &&
      messages.forEach((msg) => {
        promises.push(
          this.getKeyring(variables.roomId).then((room_kr) => {
            return ROOM_CRYPTO_CONTEXT.load(
              new RoomConfigLoadArgs(msg['content'], room_kr, getTimeFromId(msg['id']))
            )
              .then((loadedData) => {
                msg['content'] = this.convertMessageFromEncryptableObject(loadedData)[1];
                return;
              })
              .catch(() => {
                msg['error'] = true;
                msg['errorMessage'] = marker('Error: Can not decrypt message');
                return;
              });
          })
        );
      });

    //let now = performance.now();
    return Promise.all(promises).then(() => {
      //console.log("message decrypt", performance.now() - now)
      return data;
    });
  };

  private convertMessageToEncryptableObject(message: string): Uint8Array {
    return msgpack.encode([1, message]);
  }

  private convertMessageFromEncryptableObject(object: Uint8Array): [number, string] {
    return msgpack.decode(object);
  }

  /**
   * 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: roomMessagesQuery,
      variables: {
        roomId: resourceId,
        pivot: null,
        after: false,
        count: 10,
        prepareSub: true,
      },
      decrypt: this.decryptMessage.bind(this),
    });
  }

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

  public sendMessage(resourceId, content): Promise<string> {
    return this.serverRestApiService.mutate({
      query: createRoomMessageQuery,
      variables: {
        roomId: resourceId,
        content,
      },
      encrypt: this.encryptMessage.bind(this),
    });
  }

  public sendTyping(resourceId: string): Promise<boolean> {
    return this.serverRestApiService.mutate({
      query: sendRoomTypingQuery,
      variables: {
        roomId: resourceId,
      },
    });
  }

  private encryptMessage(data): Promise<Object> {
    return this.getKeyring(data.roomId).then((room_kr) => {
      return ROOM_CRYPTO_CONTEXT.dump(
        new RoomConfigDumpArgs(
          this.convertMessageToEncryptableObject(data['content']),
          room_kr,
          data.messageId ? getTimeFromId(data.messageId) : this.serverRestApiService.makeTimestamp()
        )
      ).then((content) => {
        data['content'] = content;
        return data;
      });
    });
  }

  public saveMessageInStorage(id: string, message: string): Promise<void> {
    let messageObject = this.convertMessageToEncryptableObject(message);

    return ACCOUNT_CRYPTO_CONTEXT.dump(
      new AccountConfigDumpArgs(messageObject, this.authService.getSelfAccountKeyring())
    ).then((dump) => {
      AppStorage.setItem(chatInputStorageKey + id, base64.stringify(dump));
    });
  }

  public loadMessageFromStorage(id: string): Promise<string> {
    let savedObject = AppStorage.getItem(chatInputStorageKey + id);

    if (!savedObject) return Promise.resolve('');

    let dump;
    try {
      dump = base64.parse(savedObject);
    } catch (e) {
      return Promise.resolve('');
    }

    return ACCOUNT_CRYPTO_CONTEXT.load(
      new AccountConfigLoadArgs(dump, this.authService.getSelfAccountKeyring())
    )
      .then((obj) => {
        let savedData = this.convertMessageFromEncryptableObject(obj);
        return savedData[1];
      })
      .catch(() => {
        return '';
      });
  }

  public removeMessageFromStorage(id: string) {
    AppStorage.removeItem(chatInputStorageKey + id);
  }

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

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

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

  public getDetail(resourceId): Promise<RoomDetail | DialogueDetail> {
    return this.roomService.getDetail(resourceId);
  }

  // Note: basic chat subscription is moved to the workspace, but for anonym user we still need this feature
  // subscriptions handler ------------------------------------------

  /**
   * When you subscribe for a new chat changes, you have to prepare the inner dataset with a
   * new callback handler for all of the operation
   * @param resourceId
   * @returns
   */
  private generateNewChatSubscriptionWrapper(): ChatEventSubscriptionWrapperObject {
    return {
      C: [],
      M: [],
      D: [],
      T: [],
      subscription: null,
    };
  }

  /**
   * The subscribeChatChanges will connect this handler with the correct ChatEventSubscriptionWrapperObject
   * This function is handling the server data via subscription
   * and call the actual subscription operations like create/modify/delete/typing
   * @param subWrapper ChatEventSubscriptionWrapperObject
   * @param data query result from the server
   */
  private handleSubscriptionChanges(subWrapper: ChatEventSubscriptionWrapperObject, data: any) {
    let msg = data;

    if ('roomMessageEvent' in msg) {
      let result = msg['roomMessageEvent'];

      // call the current (C, M, D) array foreach
      subWrapper[result['cmd']].forEach((cb) => {
        cb(result);
      });
    }
    if ('typingEvent' in msg) {
      subWrapper.T.forEach((cb) => {
        cb(msg['typingEvent']);
      });
    }
  }

  public subscribeChatChanges(resourceId, event: SubscriptionChatEventType, handler: Function) {
    if (!this.subscriptions.has(resourceId)) {
      // check if subscription is already exist for resource

      // generate new subscription handler for this resource
      let subWrapper = this.generateNewChatSubscriptionWrapper();
      subWrapper[event].push(handler);
      this.subscriptions.set(resourceId, subWrapper); // register handler

      subWrapper.subscription = this.serverRestApiService
        .subscribe({
          // set subscription
          query: chatChangesSubscription,
          variables: {
            roomId: resourceId,
            prepared: true,
          },
          decrypt: this.decryptSubscribeMessage.bind(this),
        })
        .subscribe({
          next: (res) => {
            // pass the core handler, to divide the response into CMD+typing
            this.handleSubscriptionChanges(subWrapper, res);
          },
        });
    } else {
      this.subscriptions.get(resourceId)[event].push(handler); // just simply register the handler
    }
  }

  private decryptSubscribeMessage(data): Promise<Object> {
    let p: Promise<void>;

    if (data['roomMessageEvent']) {
      let msg = data['roomMessageEvent'];

      if (msg['content']) {
        // if already opened, you can use keyring, else just skip
        p = this.getKeyring(msg['id']).then((room_kr) => {
          return ROOM_CRYPTO_CONTEXT.load(
            new RoomConfigLoadArgs(msg['content'], room_kr, getTimeFromId(msg['messageId']))
          )
            .then((loadedData) => {
              msg['content'] = msgpack.decode(loadedData)[1];
              return;
            })
            .catch(() => {
              msg['error'] = true;
              msg['errorMessage'] = marker('Error: Can not decrypt message');
              return;
            });
        });
      }
    }

    if (p) {
      return p.then(() => {
        return data;
      });
    } else {
      return Promise.resolve(data);
    }
  }

  /**
   * as an anonym user, you dont have workspace, you can get the news with this
   * @param resourceId
   */
  public unsubscribeAllChatChanges(resourceId) {
    let chatChangeHandlerObject = this.subscriptions.get(resourceId);
    if (chatChangeHandlerObject) {
      // we shouldn't close the observer, because the cache can be invalid after modification/delete
      //chatChangeHandlerObject.subscription.unsubscribe(); // unsubscribe form the observer

      // reset all the handler
      chatChangeHandlerObject.C = [];
      chatChangeHandlerObject.M = [];
      chatChangeHandlerObject.D = [];
      chatChangeHandlerObject.T = [];
    }
  }

  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: reactRoomMessageQuery,
      variables: {
        roomId: 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: roomMessagesByIdQuery,
        variables: {
          roomId: resourceId,
          messageIds: notFoundIds,
        },
        decrypt: this.decryptMessage,
      })
      .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 },
    });
  }
}
