import { Injectable } from "@angular/core";
import {
  chatChangesSubscription,
  createNewIdQuery,
  createRoomConfigBlockQuery,
  createRoomMessageQuery,
  createRoomQuery,
  deleteRoomMessageQuery,
  editRoomDataQuery,
  editRoomMessageQuery,
  roomMessagesQuery,
  seenRoomMessageQuery,
} 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 { MessageRecord } from "./query-records/room-records";
import { marker } from "@biesbjerg/ngx-translate-extract-marker";

/**
 * 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
  ) {}

  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, pinnedKey?: Uint8Array): Promise<Object> {
    return this.roomService.getData(id, pinnedKey);
  }

  private decryptMessage(data, variables): Promise<Object> {
    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"] = msgpack.decode(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;
    });
  }

  /**
   * 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),
    });
  }

  private encryptMessage(data): Promise<Object> {
    return this.getKeyring(data.roomId).then((room_kr) => {
      return ROOM_CRYPTO_CONTEXT.dump(
        new RoomConfigDumpArgs(
          msgpack.encode([1, data["content"]]),
          room_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: 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,
      },
    });
  }

  // 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
            console.log("inc msg");
            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) => {
          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 = [];
    }
  }
}
