import { Injectable } from '@angular/core';
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 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 {
  RawWorkspaceRecord,
  RawWorkspaceSubscriptionRecord,
  WorkspaceQueryRecord,
  WorkspaceQueryResourceGroupRecord,
  WorkspaceSubscriptionDialogueMessageEventRecord,
  WorkspaceSubscriptionRecord,
  WorkspaceSubscriptionRoomEventRecord,
  WorkspaceSubscriptionRoomMessageEventRecord,
} 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 { CacheService } from '../services/cache/cache.service';
import { DialogService } from '../services/dialog.service';
import { TimedOfferData } from './query-records/timed-offer-records';

/**
 * 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: [] },
    [SubscriptionServiceEvent.DIALOGUE_TYPING_EVENT]: { C: [], M: [], D: [] },
    [SubscriptionServiceEvent.ROOM_TYPING_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
  ) {
    let callAuthInits = () => {
      this.isWorkspaceLoaded = true;
      let cbCopy = [...this.initCallbacks];
      cbCopy.forEach((cb) => {
        cb();
      });
    };

    let setupSubscription = () => {
      if (!this.authService.isAnonym()) {
        console.log('sub: 1 - user state; subscription setup started');
        this.initWorkspace().then(() => {
          console.log('sub: 2 - subscription service OK');
          callAuthInits();
        });
      } else {
        console.log('sub: 1 - anonym state; no subscription needed.');
        callAuthInits();
      }
    };

    this.authService.onSelfAccountKeyringLoaded(() => {
      setupSubscription();
    });
  }

  private initWorkspace() {
    this.isWorkspaceLoaded = false;
    this.cacheService.resetCache();
    // only the logined user has workspace
    // query (account, resources) to prepare the cache
    let p: Promise<void>;

    return 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'];
              let cbCopy = [...this.subsciptions[prop][cmd]];
              cbCopy.forEach((cb) => {
                cb(res[prop]);
              });
            }
          }
        });

        return;
      })
      .catch((err) => {
        // on password change
        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();
            });
        } else {
          console.error('ws query err', err);
        }*/
      });
  }

  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)));

    // result
    let ws: WorkspaceQueryRecord = {
      grantedRooms: data.grantedRoomIds.filter((id) => {
        return !data.rejectedRoomIds.includes(id);
      }),
      me: data.me,
      rejectedRooms: data.rejectedRoomIds,
      resourceGroups: [],
      ungroupedResources: [],
      waitingNanos: data.waitingNanos,
      contacts: data.contacts,
      timedOffers: [],
      newestNano: data.newestNano,
      newestApp: data.newestApp,
    };

    data.timedOffers.forEach((oneTimeOffer) => {
      ws.timedOffers.push({
        data: JSON.parse(<string>oneTimeOffer.data),
        expiration: oneTimeOffer.expiration,
        id: oneTimeOffer.id,
      });
    });

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

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

      element.resources.forEach((resource) => {
        decryptedGroup.resources.push(resource);
      });

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

    data.ungroupedResources.forEach((resource) => {
      ws.ungroupedResources.push(resource);
    });

    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] = {
        cmd: data[SubscriptionServiceEvent.RESOURCE_GROUP_EVENT].cmd,
        decryptionError: false,
        id: data[SubscriptionServiceEvent.RESOURCE_GROUP_EVENT].id,
        closed: data[SubscriptionServiceEvent.RESOURCE_GROUP_EVENT].closed,
      };

      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].decryptionError = true;
              ws[SubscriptionServiceEvent.RESOURCE_GROUP_EVENT].decryptionErrorMessage =
                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];

      let eventCopy = {};
      for (let prop in messageEvent) {
        if (prop != 'content') {
          // copy everything, except content, which will be decrypted later
          eventCopy[prop] = messageEvent[prop];
        }
      }

      ws[SubscriptionServiceEvent.ROOM_MESSAGE_EVENT] = <
        WorkspaceSubscriptionRoomMessageEventRecord
      >eventCopy;

      if (messageEvent.content) {
        ws[SubscriptionServiceEvent.ROOM_MESSAGE_EVENT].decryptionError = false;
        // 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].decryptionError = true;
                  ws[SubscriptionServiceEvent.ROOM_MESSAGE_EVENT].decryptionErrorMessage = marker(
                    'Error: Can not decrypt message'
                  );
                  return;
                });
            })
            .catch(() => {
              ws[SubscriptionServiceEvent.ROOM_MESSAGE_EVENT].decryptionError = true;
              ws[SubscriptionServiceEvent.ROOM_MESSAGE_EVENT].decryptionErrorMessage = marker(
                'Error: Can not load room keyring'
              );
              return;
            })
        );
      }
    }
    if (data[SubscriptionServiceEvent.DIALOGUE_MESSAGE_EVENT]) {
      let messageEvent = data[SubscriptionServiceEvent.DIALOGUE_MESSAGE_EVENT];

      let eventCopy = {};
      for (let prop in messageEvent) {
        if (prop != 'content') {
          // copy everything, except content, which will be decrypted later
          eventCopy[prop] = messageEvent[prop];
        }
      }

      ws[SubscriptionServiceEvent.DIALOGUE_MESSAGE_EVENT] = <
        WorkspaceSubscriptionDialogueMessageEventRecord
      >eventCopy;

      if (messageEvent.content) {
        ws[SubscriptionServiceEvent.DIALOGUE_MESSAGE_EVENT].decryptionError = false;
        // 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].decryptionError = true;
                  ws[SubscriptionServiceEvent.DIALOGUE_MESSAGE_EVENT].content = marker(
                    'Error: Can not decrypt message'
                  );
                  return;
                });
            })
            .catch(() => {
              ws[SubscriptionServiceEvent.DIALOGUE_MESSAGE_EVENT].decryptionError = true;
              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];

      let eventCopy = {};
      for (let prop in messageEvent) {
        if (prop != 'data') {
          // copy everything, except data, which will be decrypted later
          eventCopy[prop] = messageEvent[prop];
        }
      }

      ws[SubscriptionServiceEvent.ROOM_EVENT] = <WorkspaceSubscriptionRoomEventRecord>eventCopy;

      if (messageEvent.data) {
        p.push(
          this.roomService
            .getRoom(messageEvent.id)
            .then((roomData) => {
              return this.roomService.getKeyring(roomData.id);
            })
            .then((room_kr) => {
              return this.roomService.getRoomRecord(messageEvent.id).then((roomRecord) => {
                return this.roomService
                  .decryptRoomData(roomRecord, <Uint8Array>messageEvent.data)
                  .then((roomData) => {
                    ws[SubscriptionServiceEvent.ROOM_EVENT].data = roomData;
                    return;
                  });
              });
              /*
              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];

      // update the trust field
      p.push(
        this.accountService
          .getPeerKeyring(data[SubscriptionServiceEvent.ACCOUNT_TRUST_EVENT].peerId)
          .then((peer_kr) => {
            return peer_kr.update(
              data[SubscriptionServiceEvent.ACCOUNT_TRUST_EVENT].trust,
              this.authService.getSelfAccountKeyring()
            );
          })
      );
    }

    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.DIALOGUE_TYPING_EVENT]) {
      ws[SubscriptionServiceEvent.DIALOGUE_TYPING_EVENT] =
        data[SubscriptionServiceEvent.DIALOGUE_TYPING_EVENT];
    }

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

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

      ws[SubscriptionServiceEvent.ACCOUNT_OFFER_EVENT] = {
        cmd: offer.cmd,
        data: offer.data ? <TimedOfferData>JSON.parse(<string>offer.data) : undefined,
        expiration: offer.expiration,
        id: offer.id,
      };
    }

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