import { Injectable } from "@angular/core";
import { AccountConfigLoadArgs } from "../crypto/context/account/args";
import { ACCOUNT_CRYPTO_CONTEXT } from "../crypto/context/account/__init__";
import { ServerRestApiService } from "../services/server-rest-api.service";
import { AccountService } from "./account.service";
import { AuthService } from "./auth.service";
import { workspaceQuery, workspaceSubscription } from "./querys";
import { ROOM_CRYPTO_CONTEXT } from "../crypto/context/room/__init__";
import { RoomConfigLoadArgs } from "../crypto/context/room/args";
import { RoomKeyring } from "../crypto/keyring/room";
import msgpack from "msgpack-lite";
import { getTimeFromId } from "../crypto/context/timestamp";
import { ChatService } from "./chat.service";
import { DialogueService } from "./dialogue.service";
import { DIALOGUE_CRYPTO_CONTEXT } from "../crypto/context/dialogue/__init__";
import { DialogueConfigLoadArgs } from "../crypto/context/dialogue/args";
import { RoomService } from "./room.service";
import {
  RawWorkspaceQueryDialogueRecord,
  RawWorkspaceQueryRoomRecord,
  RawWorkspaceRecord,
  RawWorkspaceSubscriptionRecord,
  WorkspaceQueryDialogueRecord,
  WorkspaceQueryRecord,
  WorkspaceQueryResourceGroupRecord,
  WorkspaceSubscriptionRecord,
} from "../server-services/query-records/workspace-records";
import {
  SubscriptionServiceEvent,
  SubscriptionServiceEventType,
} from "./subscription-event";
import { marker } from "@biesbjerg/ngx-translate-extract-marker";
import { ResourceGroupCrypto } from "../crypto/top/resource-group";
import { RoomCrypto } from "../crypto/top/room";
import { ResourceType } from "src/app/components/resources-sidebar/sidebar-data";
import { CacheService } from "../services/cache.service";
import { DialogService } from "../services/dialog.service";

/**
 * This service is a core layer between the components and the apollo.
 * We have some necessary data, like account/sidebar infos, what we will query and cache (workspace)
 * the main subscription must be called after the first query with "prepareSub"
 * we will handle globally all the main subscription what comes from the "workspaceChanges"
 * other services can subscribe on this handler
 */
@Injectable({
  providedIn: "root",
})
export class SubscriptionService {
  // cache the subscription callbacks
  private subsciptions = {
    [SubscriptionServiceEvent.ACCOUNT_EVENT]: { C: [], M: [], D: [] },
    [SubscriptionServiceEvent.ACCOUNT_CONTACT_EVENT]: { C: [], M: [], D: [] },
    [SubscriptionServiceEvent.ACCOUNT_TRUST_EVENT]: { C: [], M: [], D: [] },
    [SubscriptionServiceEvent.DIALOGUE_EVENT]: { C: [], M: [], D: [] },
    [SubscriptionServiceEvent.DIALOGUE_MESSAGE_EVENT]: { C: [], M: [], D: [] },
    [SubscriptionServiceEvent.RESOURCE_GROUP_EVENT]: { C: [], M: [], D: [] },
    [SubscriptionServiceEvent.ROOM_EVENT]: { C: [], M: [], D: [] },
    [SubscriptionServiceEvent.ROOM_MESSAGE_EVENT]: { C: [], M: [], D: [] },
    [SubscriptionServiceEvent.ROOM_PERMISSION_EVENT]: { C: [], M: [], D: [] },
    [SubscriptionServiceEvent.ROOM_ACCOUNT_PERMISSION_EVENT]: {
      C: [],
      M: [],
      D: [],
    },
    [SubscriptionServiceEvent.NANO_WAITING_EVENT]: { C: [], M: [], D: [] },
    [SubscriptionServiceEvent.ACCOUNT_OFFER_EVENT]: { C: [], M: [], D: [] },
  };

  private initCallbacks = [];
  private isWorkspaceLoaded: boolean = false;
  constructor(
    private serverRestApiService: ServerRestApiService,
    private authService: AuthService,
    private accountService: AccountService,
    private chatService: ChatService,
    private roomService: RoomService,
    private dialogueService: DialogueService,
    private cacheService: CacheService,
    private dialogService: DialogService
  ) {
    this.authService.onSelfAccountKeyringLoaded(() => {
      this.initWorkspace();
    });
  }

  private initWorkspace() {
    console.log("conn :5 - subscription setup started");
    this.cacheService.resetCache();
    this.isWorkspaceLoaded = false;
    // only the logined user has workspace
    // query (account, resources) to prepare the cache
    let p: Promise<void>;

    if (this.authService.isAnonym()) {
      console.log("anonym ws");
      p = Promise.resolve();
    } else {
      p = this.queryWorkspace()
        .then((allWorkspace) => {
          // subscribe the incoming changes (account, resources, server event, chat specific event globally like new/edited, permissions)
          this.subscribeToWorkspaceChanges((res) => {
            for (let prop in res) {
              if (
                (<any>Object).values(SubscriptionServiceEvent).includes(prop)
              ) {
                let cmd = res[prop]["cmd"];
                this.subsciptions[prop][cmd].forEach((cb) => {
                  cb(res[prop]);
                });
              }
            }
          });

          return;
        })
        .catch((err) => {
          console.warn("workspace subscription closed: ", err.message);
          if (err.message == "Unauthenticated!") {
            this.dialogService
              .openAlertDialog(
                marker("Authentication Error"),
                marker(
                  "Could not load your workspace, the system will reload the app."
                )
              )
              .toPromise()
              .then(() => {
                location.reload();
              });
          }
        });
    }

    p.then(() => {
      console.log("conn :6 - subscription service OK");
      this.isWorkspaceLoaded = true;
      this.initCallbacks.forEach((cb) => {
        cb();
      });
    });
  }

  public subscribeWorkspaceLoaded(cb: Function): void {
    this.initCallbacks.push(cb); // add if later need this after reset
    if (this.isWorkspaceLoaded) cb();
  }

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

  subscribe(
    eventName: SubscriptionServiceEvent,
    eventType: SubscriptionServiceEventType,
    handler: (event) => any
  ) {
    if (eventType !== SubscriptionServiceEventType.ALL) {
      this.subsciptions[eventName][eventType].push(handler);
    } else {
      this.subscribe(eventName, SubscriptionServiceEventType.CREATE, handler);
      this.subscribe(eventName, SubscriptionServiceEventType.MODIFY, handler);
      this.subscribe(eventName, SubscriptionServiceEventType.DELETE, handler);
    }
  }

  /**
   * are services permanent or can they be destroyed via destructor (ngOnDestroy) in angular?
   */
  unsubscribe(
    eventName: SubscriptionServiceEvent,
    eventType: SubscriptionServiceEventType,
    handler: (event) => any
  ) {
    if (eventType !== SubscriptionServiceEventType.ALL) {
      let pos = this.subsciptions[eventName][eventType].indexOf(handler);
      this.subsciptions[eventName][eventType].splice(pos, 1);
    } else {
      this.unsubscribe(eventName, SubscriptionServiceEventType.CREATE, handler);
      this.unsubscribe(eventName, SubscriptionServiceEventType.MODIFY, handler);
      this.unsubscribe(eventName, SubscriptionServiceEventType.DELETE, handler);
    }
  }

  /**
   * get and cache the necessary datas into the apollo (me, resources-tree)
   * we will prepare for the workspaceChanges subscription
   * @returns
   */
  public queryWorkspace(): Promise<WorkspaceQueryRecord> {
    return this.serverRestApiService.query({
      query: workspaceQuery,
      variables: {
        prepareSub: true,
      },
      decrypt: this.decryptWorkspaceData.bind(this),
    });
  }

  private decryptWorkspaceData(
    data: RawWorkspaceRecord
  ): Promise<WorkspaceQueryRecord> {
    console.log("raw ws", JSON.parse(JSON.stringify(data)));

    let ws: WorkspaceQueryRecord = {
      grantedRooms: [],
      me: data.me,
      rejectedRooms: [],
      resourceGroups: [],
      ungroupedResources: [],
      waitingNanos: data.waitingNanos,
      contactAvatars: data.contactAvatars,
      timedOffers: data.timedOffers,
      newestNano: data.newestNano
    };

    ws.timedOffers.forEach((oneTimeOffer) => {
      oneTimeOffer.data = JSON.parse(<string>oneTimeOffer.data);
    });

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

    /**
     * collect all of the resource, decrypt them, then push its container
     */
    let allRes: Array<{
      container: any[];
      resources: Array<
        RawWorkspaceQueryRoomRecord | RawWorkspaceQueryDialogueRecord
      >;
    }> = [];

    // decrypt the group names
    data.resourceGroups.forEach((element) => {
      let decryptedGroup: WorkspaceQueryResourceGroupRecord = {
        closed: element.closed,
        id: element.id,
        name: undefined,
        ordering: element.ordering,
        resources: [],
        error: false,
      };
      ws.resourceGroups.push(decryptedGroup);

      // decrypt the group
      promises.push(
        ResourceGroupCrypto.decrypt(
          <Uint8Array>element.name,
          this.authService.getSelfAccountKeyring()
        )
          .then((decryptedName) => {
            decryptedGroup.name = <string>decryptedName;
          })
          .catch((error) => {
            decryptedGroup.error = true;
            decryptedGroup.errorMessage = error.message;
            console.warn("can not decrypt room", error.message, error.err);
          })
      );

      allRes.push({
        container: decryptedGroup.resources,
        resources: element.resources,
      });
    });

    allRes.push({
      container: ws.ungroupedResources,
      resources: data.ungroupedResources,
    });

    // now decrypt all the room/dialogues which coming from the query
    // note: granted/rejected rooms are not in the query only their ids
    // so need to query them one by one
    allRes.forEach((contResPair) => {
      contResPair.resources.forEach((resource) => {
        ((resource) => {
          if (resource.type == ResourceType.ROOM) {
            let roomResource = <RawWorkspaceQueryRoomRecord>resource;

            promises.push(
              this.roomService
                .decryptRoomRecord(roomResource)
                .then((decryptedRoom) => {
                  contResPair.container.push(decryptedRoom);
                })
            );
          } else {
            let dialogueResource = <RawWorkspaceQueryDialogueRecord>resource;

            let decryptedDialogue: WorkspaceQueryDialogueRecord = Object.assign(
              dialogueResource,
              { account: null, error: false }
            );

            contResPair.container.push(decryptedDialogue);
            promises.push(
              this.accountService
                .getAccount(dialogueResource.id)
                .then((account) => {
                  decryptedDialogue.account = account;
                  return;
                })
                .catch((err) => {
                  console.warn("can not find user", err);
                  decryptedDialogue.error = true;
                  decryptedDialogue.errorMessage = marker("Can not find user");
                  return;
                })
            );
          }
        })(resource);
      });
    });

    let plusKnownRooms = [...data.grantedRoomIds];

    // some rejected rooms are not granted
    for (let i = 0; i < data.rejectedRoomIds.length; i++) {
      if (!data.grantedRoomIds.includes(data.rejectedRoomIds[i])) {
        plusKnownRooms.push(data.rejectedRoomIds[i]);
      }
    }

    plusKnownRooms.forEach((roomId) => {
      promises.push(
        this.roomService
          .getData(roomId)
          .then((roomData) => {
            if (data.rejectedRoomIds.includes(roomData.id)) {
              ws.rejectedRooms.push(roomData);
            } else {
              ws.grantedRooms.push(roomData);
            }
            return;
          })
          .catch((err) => {
            // if you do not have any permission for this room
            // but it is in the rejected rooms, so maybe it is exist
            // we will load a broken record to the rejected list
            console.warn("could not reach the room", roomId);

            ws.rejectedRooms.push({
              data: null,
              id: roomId,
              nanoSessions: {},
              ownerAccountId: null,
              pinnedKey: null,
              topMessageId: null,
              seenMessageId: null,
              type: ResourceType.ROOM,
              ordering: null,
              error: true,
              errorMessage: marker("Not available"),
              public: false,
            });
            return;
          })
      );
    });

    return Promise.all(promises).then(() => {
      console.log("decrypted ws", JSON.parse(JSON.stringify(ws)));
      return ws;
    });
  }

  /**
   * Subscribe to the workspace changes
   * @param cb
   * @returns
   */
  private subscribeToWorkspaceChanges(
    cb: (res: WorkspaceSubscriptionRecord) => void
  ) {
    this.serverRestApiService
      .subscribe({
        query: workspaceSubscription,
        variables: {
          prepared: true,
        },
        decrypt: this.decryptWorkspaceChangeData.bind(this),
      })
      .subscribe({
        next: cb,
        error: (err) => {
          console.log("subscription service ERR", err);
          this.initWorkspace();
        },
      });
  }

  private decryptWorkspaceChangeData(
    data: RawWorkspaceSubscriptionRecord
  ): Promise<WorkspaceSubscriptionRecord> {
    console.log("inc raw event", JSON.parse(JSON.stringify(data)));
    let p: Promise<void>[] = [];

    let ws: WorkspaceSubscriptionRecord = {};

    if (data[SubscriptionServiceEvent.RESOURCE_GROUP_EVENT]) {
      let resourceGroupEvent =
        data[SubscriptionServiceEvent.RESOURCE_GROUP_EVENT];

      ws[SubscriptionServiceEvent.RESOURCE_GROUP_EVENT] = Object.assign(
        data[SubscriptionServiceEvent.RESOURCE_GROUP_EVENT],
        {
          error: false,
        }
      );

      if (resourceGroupEvent.ordering) {
        ws[SubscriptionServiceEvent.RESOURCE_GROUP_EVENT].ordering =
          resourceGroupEvent.ordering;
      }

      if (resourceGroupEvent.name) {
        p.push(
          ResourceGroupCrypto.decrypt(
            <Uint8Array>resourceGroupEvent.name,
            this.authService.getSelfAccountKeyring()
          )
            .then((decryptedName) => {
              ws[SubscriptionServiceEvent.RESOURCE_GROUP_EVENT].name = <string>(
                decryptedName
              );
            })
            .catch((error) => {
              ws[SubscriptionServiceEvent.RESOURCE_GROUP_EVENT].error = true;
              ws[SubscriptionServiceEvent.RESOURCE_GROUP_EVENT].errorMessage =
                error.message;
              console.error("can not decrypt room", error.message, error.err);
            })
        );
      }
    }

    if (data[SubscriptionServiceEvent.ROOM_MESSAGE_EVENT]) {
      let messageEvent = data[SubscriptionServiceEvent.ROOM_MESSAGE_EVENT];

      ws[SubscriptionServiceEvent.ROOM_MESSAGE_EVENT] = Object.assign(
        data[SubscriptionServiceEvent.ROOM_MESSAGE_EVENT],
        { error: false }
      );

      if (messageEvent.content) {
        // if already opened, you can use keyring, else just skip
        p.push(
          this.roomService
            .getKeyring(messageEvent.id)
            .then((room_kr) => {
              return ROOM_CRYPTO_CONTEXT.load(
                new RoomConfigLoadArgs(
                  <Uint8Array>messageEvent.content,
                  room_kr,
                  getTimeFromId(messageEvent.messageId)
                )
              )
                .then((loadedData) => {
                  ws[SubscriptionServiceEvent.ROOM_MESSAGE_EVENT].content =
                    msgpack.decode(loadedData)[1];
                  return;
                })
                .catch(() => {
                  ws[SubscriptionServiceEvent.ROOM_MESSAGE_EVENT].error = true;
                  ws[SubscriptionServiceEvent.ROOM_MESSAGE_EVENT].errorMessage =
                    marker("Error: Can not decrypt message");
                  return;
                });
            })
            .catch(() => {
              ws[SubscriptionServiceEvent.ROOM_MESSAGE_EVENT].error = true;
              ws[SubscriptionServiceEvent.ROOM_MESSAGE_EVENT].errorMessage =
                marker("Error: Can not load room keyring");
              return;
            })
        );
      }
    }
    if (data[SubscriptionServiceEvent.DIALOGUE_MESSAGE_EVENT]) {
      let messageEvent = data[SubscriptionServiceEvent.DIALOGUE_MESSAGE_EVENT];
      ws[SubscriptionServiceEvent.DIALOGUE_MESSAGE_EVENT] = {
        id: messageEvent.id,
        messageId: messageEvent.messageId,
        posterId: messageEvent.posterId,
        cmd: messageEvent.cmd,
        error: false,
      };

      ws[SubscriptionServiceEvent.DIALOGUE_MESSAGE_EVENT] = Object.assign(
        data[SubscriptionServiceEvent.DIALOGUE_MESSAGE_EVENT],
        { error: false }
      );

      if (messageEvent.content) {
        // if already opened, you can use keyring, else just skip

        p.push(
          this.dialogueService
            .getKeyring(messageEvent.id)
            .then((peer_kr) => {
              return DIALOGUE_CRYPTO_CONTEXT.load(
                new DialogueConfigLoadArgs(
                  <Uint8Array>messageEvent.content,
                  this.authService.getSelfAccountKeyring(),
                  peer_kr,
                  getTimeFromId(messageEvent.messageId)
                )
              )
                .then((loadedData) => {
                  ws[SubscriptionServiceEvent.DIALOGUE_MESSAGE_EVENT].content =
                    msgpack.decode(loadedData)[1];
                  return;
                })
                .catch(() => {
                  ws[SubscriptionServiceEvent.DIALOGUE_MESSAGE_EVENT].content =
                    marker("Error: Can not decrypt message");
                  return;
                });
            })
            .catch(() => {
              ws[SubscriptionServiceEvent.DIALOGUE_MESSAGE_EVENT].content =
                marker("Error: Can not load peer keyring");
              return;
            })
        );
      }
    }

    if (data[SubscriptionServiceEvent.ROOM_EVENT]) {
      let messageEvent = data[SubscriptionServiceEvent.ROOM_EVENT];

      ws[SubscriptionServiceEvent.ROOM_EVENT] = Object.assign(
        data[SubscriptionServiceEvent.ROOM_EVENT],
        { error: false, public: false }
      );

      if (messageEvent.data) {
        p.push(
          this.roomService
            .getData(messageEvent.id)
            .then((roomData) => {
              return this.roomService.getKeyring(roomData.id);
            })
            .then((room_kr) => {
              return RoomCrypto.decrypt(
                <Uint8Array>messageEvent.data,
                messageEvent.id,
                room_kr
              )
                .then((loadedData) => {
                  if (typeof loadedData === "object" && loadedData !== null) {
                    ws[SubscriptionServiceEvent.ROOM_EVENT].data = loadedData;
                  } else {
                    ws[SubscriptionServiceEvent.ROOM_EVENT].error = true;
                    ws[SubscriptionServiceEvent.ROOM_EVENT].errorMessage =
                      marker("Dataset Error");
                  }

                  return;
                })
                .catch((err) => {
                  console.error(
                    "can not decrypt room data in subscription",
                    err.message
                  );
                  ws[SubscriptionServiceEvent.ROOM_EVENT].error = true;
                  ws[SubscriptionServiceEvent.ROOM_EVENT].errorMessage =
                    marker("Decryption Error");

                  return;
                });
            })
        );
      }
    }

    if (data[SubscriptionServiceEvent.DIALOGUE_EVENT]) {
      ws[SubscriptionServiceEvent.DIALOGUE_EVENT] =
        data[SubscriptionServiceEvent.DIALOGUE_EVENT];

      let dialogueEvent = data[SubscriptionServiceEvent.DIALOGUE_EVENT];

      // cache the user if it is needed, cache service will store the dialogueData
      p.push(this.accountService.getAccount(dialogueEvent.id).then());
    }

    // copy the event which does not need any changes
    if (data[SubscriptionServiceEvent.ACCOUNT_EVENT]) {
      ws[SubscriptionServiceEvent.ACCOUNT_EVENT] =
        data[SubscriptionServiceEvent.ACCOUNT_EVENT];
    }

    if (data[SubscriptionServiceEvent.ACCOUNT_CONTACT_EVENT]) {
      ws[SubscriptionServiceEvent.ACCOUNT_CONTACT_EVENT] =
        data[SubscriptionServiceEvent.ACCOUNT_CONTACT_EVENT];
    }

    if (data[SubscriptionServiceEvent.ACCOUNT_TRUST_EVENT]) {
      ws[SubscriptionServiceEvent.ACCOUNT_TRUST_EVENT] =
        data[SubscriptionServiceEvent.ACCOUNT_TRUST_EVENT];
    }

    if (data[SubscriptionServiceEvent.DIALOGUE_EVENT]) {
      ws[SubscriptionServiceEvent.DIALOGUE_EVENT] =
        data[SubscriptionServiceEvent.DIALOGUE_EVENT];
    }

    if (data[SubscriptionServiceEvent.ROOM_PERMISSION_EVENT]) {
      ws[SubscriptionServiceEvent.ROOM_PERMISSION_EVENT] =
        data[SubscriptionServiceEvent.ROOM_PERMISSION_EVENT];
    }

    if (data[SubscriptionServiceEvent.ROOM_ACCOUNT_PERMISSION_EVENT]) {
      ws[SubscriptionServiceEvent.ROOM_ACCOUNT_PERMISSION_EVENT] =
        data[SubscriptionServiceEvent.ROOM_ACCOUNT_PERMISSION_EVENT];
    }

    if (data[SubscriptionServiceEvent.NANO_WAITING_EVENT]) {
      ws[SubscriptionServiceEvent.NANO_WAITING_EVENT] =
        data[SubscriptionServiceEvent.NANO_WAITING_EVENT];
    }

    if (data[SubscriptionServiceEvent.ACCOUNT_OFFER_EVENT]) {
      let offer = data[SubscriptionServiceEvent.ACCOUNT_OFFER_EVENT];

      if (offer.data) {
        offer.data = JSON.parse(<string>offer.data);
      }

      ws[SubscriptionServiceEvent.ACCOUNT_OFFER_EVENT] = offer;
    }

    return Promise.all(p).then(() => {
      console.log("inc event", JSON.parse(JSON.stringify(ws)));
      return ws;
    });
  }

  // subscribe/unsubscribe any changes -----------------------------------------------------
  // resource group
  /*public subscribeOnCreateResourceGroup(
    cb: (event: WorkspaceSubscriptionResourceGroupEventRecord) => any
  ) {
    this.subscribe(
      SubscriptionServiceEvent.RESOURCE_GROUP_EVENT,
      SubscriptionServiceEventType.CREATE,
      cb
    );
  }

  public subscribeOnModifyResourceGroup(
    cb: (event: WorkspaceSubscriptionResourceGroupEventRecord) => any
  ) {
    this.subscribe(
      SubscriptionServiceEvent.RESOURCE_GROUP_EVENT,
      SubscriptionServiceEventType.MODIFY,
      cb
    );
  }

  public subscribeOnDeleteResourceGroup(
    cb: (event: WorkspaceSubscriptionResourceGroupEventRecord) => any
  ) {
    this.subscribe(
      SubscriptionServiceEvent.RESOURCE_GROUP_EVENT,
      SubscriptionServiceEventType.DELETE,
      cb
    );
  }

  public unsubscribeOnCreateResourceGroup(
    cb: (event: WorkspaceSubscriptionResourceGroupEventRecord) => any
  ) {
    this.unsubscribe(
      SubscriptionServiceEvent.RESOURCE_GROUP_EVENT,
      SubscriptionServiceEventType.CREATE,
      cb
    );
  }

  public unsubscribeOnModifyResourceGroup(
    cb: (event: WorkspaceSubscriptionResourceGroupEventRecord) => any
  ) {
    this.unsubscribe(
      SubscriptionServiceEvent.RESOURCE_GROUP_EVENT,
      SubscriptionServiceEventType.MODIFY,
      cb
    );
  }

  public unsubscribeOnDeleteResourceGroup(
    cb: (event: WorkspaceSubscriptionResourceGroupEventRecord) => any
  ) {
    this.unsubscribe(
      SubscriptionServiceEvent.RESOURCE_GROUP_EVENT,
      SubscriptionServiceEventType.DELETE,
      cb
    );
  }*/
}
