import { Injectable } from "@angular/core";
import {
  createNewIdQuery,
  createRoomConfigBlockQuery,
  createRoomQuery,
  deleteRoomQuery,
  editRoomAccountPermissionQuery,
  editRoomDataQuery,
  editRoomPermissionQuery,
  editRoomPlacementQuery,
  getAllRoomQuery,
  getRoomDataQuery,
  joinRoomQuery,
  leaveRoomQuery,
  roomConfigQuery,
  roomMemberQuery,
  roomMyPermissionsQuery,
  roomPermissionsQuery,
} from "../server-services/querys";
import { ServerRestApiService } from "../services/server-rest-api.service";
import { AccountService } from "./account.service";
import { AuthService } from "./auth.service";
import { RoomKeyring } from "../crypto/keyring/room";
import { GroupConfigBlockChain } from "../crypto/group_config/chain";
import {
  GroupConfigBlockFlags,
  GroupConfigBlockMemberFlags,
} from "../crypto/group_config/block";
import { marker } from "@biesbjerg/ngx-translate-extract-marker";
import { RoomMember } from "src/app/components/resource-page/room-details/room-details.component";
import {
  FlagParser,
  RoomPermissions,
} from "../crypto/group_config/chain-flag-parser";
import {
  RawRoomRecord,
  RoomRecord,
  roomConfigRecord,
  RoomPermissionRecord,
  RoomMyPermissionRecord,
  RoomMemberRecord,
} from "./query-records/room-records";
import { RawWorkspaceQueryRoomRecord } from "./query-records/workspace-records";
import { RoomCrypto } from "../crypto/top/room";
import { ID } from "./query-records/common-records";
import { CacheService } from "../services/cache.service";

export interface RoomData {
  name: string;
  avatar: ArrayBuffer;
}

@Injectable({
  providedIn: "root",
})
export class RoomService {
  /**
   * Cache for keyrings
   */
  private roomKeyrings = {};

  constructor(
    private serverRestApiService: ServerRestApiService,
    private accountService: AccountService,
    private authService: AuthService,
    private cacheService: CacheService
  ) {
    authService.onLogout(() => {
      this.roomKeyrings = {};
    });
  }

  public getKeyring(resourceId): Promise<RoomKeyring> {
    if (this.roomKeyrings.hasOwnProperty(resourceId)) {
      if (!this.roomKeyrings[resourceId]) {
        return Promise.reject(
          "Could not generate keyring for this room: " + resourceId
        );
      } else {
        return Promise.resolve(this.roomKeyrings[resourceId]);
      }
    }
    return this.getData(resourceId).then((roomData) => {
      return this.makeKeyring(roomData);
    });
  }

  public makeKeyring(
    roomData: { public } & RawRoomRecord
  ): Promise<RoomKeyring> {
    if (!this.roomKeyrings.hasOwnProperty(roomData.ownerAccountId)) {
      if (roomData.public) {
        if (roomData.pinnedKey) {
          return RoomKeyring.loadRaw(roomData.id, roomData.pinnedKey).then(
            (room_kr) => {
              this.roomKeyrings[roomData.id] = room_kr;
              return room_kr;
            }
          );
        } else {
          console.error("did not get pinned key", roomData.pinnedKey);
          return Promise.reject("need pinned key for operation");
        }
      } else {
        return this.accountService
          .getPeerKeyring(roomData.ownerAccountId)
          .then((peer_kr) => {
            return RoomKeyring.load(
              this.authService.getSelfAccountKeyring(),
              peer_kr,
              roomData.id,
              roomData.savedKey,
              roomData.pinnedKey
            )
              .then((room_kr) => {
                // save keyring for future request
                this.roomKeyrings[roomData.id] = room_kr;

                return room_kr;
              })
              .catch((err) => {
                console.warn("can not load roomkeyring", err.message);
                this.roomKeyrings[roomData.id] = null;
                return Promise.reject("can not load roomkeyring");
              });
          });
      }
    } else {
      if (this.roomKeyrings[roomData.ownerAccountId]) {
        return Promise.resolve(this.getKeyring(roomData.id));
      } else {
        return Promise.reject("can not load roomkeyring");
      }
    }
  }

  /**
   * Note: If you get data from workspace you will get the data from the cache. So you
   * have a good chance, this decrypter wont get called.
   */
  private decryptData(
    data: RawRoomRecord,
    variables,
    extra
  ): Promise<RoomRecord> {
    return this.decryptRoomRecord(data, extra["pinnedKey"]);
  }

  public decryptRoomRecord(
    roomRecord: RawRoomRecord | RawWorkspaceQueryRoomRecord,
    pinnedKey?: Uint8Array
  ) {
    let publicRoom = false;

    if (!roomRecord.pinnedKey && pinnedKey) {
      roomRecord.pinnedKey = pinnedKey;
      publicRoom = true;
    }

    let result: RoomRecord = Object.assign(roomRecord, {
      public: publicRoom,
      error: true,
      errorMessage: "",
    });

    if (
      roomRecord.inaccessible === true ||
      (!roomRecord.savedKey && !roomRecord.pinnedKey)
    ) {
      result.errorMessage = marker("Not available"); // TODO
      return Promise.resolve(result);
    } else {
      if (!roomRecord.data) {
        result.errorMessage = marker("Dataset Error");
        return Promise.resolve(result);
      } else {
        return this.makeKeyring(result)
          .then((room_kr) => {
            return RoomCrypto.decrypt(
              <Uint8Array>roomRecord.data,
              roomRecord.id,
              room_kr
            );
          })
          .then((loadedData) => {
            if (typeof loadedData === "object" && loadedData !== null) {
              result.data = <RoomData>loadedData;
              result.error = false;
              return result;
            } else {
              result.errorMessage = marker("Dataset Error");
              return result;
            }
          })
          .catch((err) => {
            result.errorMessage = marker("Decryption error");
            return result;
          });
      }
    }
  }

  private encryptData(data, extra): Promise<Object> {
    return this.getData(data["roomId"])
      .then((roomData) => {
        return this.makeKeyring(roomData);
      })
      .then((room_kr) => {
        return RoomCrypto.encrypt(data["data"], data["roomId"], room_kr);
      })
      .then((dump) => {
        data["data"] = dump;
        return data;
      });
  }

  // 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<RoomRecord> {
    return this.serverRestApiService
      .query({
        query: getRoomDataQuery,
        variables: {
          id,
        },
        decrypt: (data: RawRoomRecord, variables, extra) => {
          if (pinnedKey) {
            extra["pinnedKey"] = pinnedKey;
          }
          
          return this.decryptData(data, variables, extra);
        },
      }).then((roomData: RoomRecord)=>{
        
        // this is an advanced edge-case handling
        // room loaded into the cache because it is in your workspace
        // but unavailable (maybe you've been kicked), so you do not have pinned key
        // but the room is public and you have a fragment pinnedKey
        // the workspace decryption run earlier than the routerHandler init
        // normally it is handled in the resource-page component, but now
        // it is already loaded into the cache
        if (!roomData.pinnedKey && pinnedKey){
          // redecrypt the data with your pinnedKey
          return this.decryptData(roomData, {id}, {pinnedKey}).then((roomData)=>{
            console.log('redecrypt room', roomData)
            this.cacheService.set(getRoomDataQuery, {id}, roomData)
            return roomData;
          })
        }
        else{
          return roomData;
        }
      })
      .catch((err) => {
        return Promise.reject({
          message: "can not get room data from server",
          err,
          decrypt: false,
          fetch: false,
        });
      });
  }

  public editData(id, name?: string, avatar?: ArrayBuffer) {
    return this.serverRestApiService.mutate({
      query: editRoomDataQuery,
      encrypt: this.encryptData.bind(this),
      variables: {
        roomId: id,
        data: {
          name,
          avatar,
        },
      },
    });
  }

  public editPermission(id, msgBrdReadOnly: boolean) {
    return this.serverRestApiService.mutate({
      query: editRoomPermissionQuery,
      variables: {
        roomId: id,
        attribute: "msgBrdReadOnly",
        value: msgBrdReadOnly,
      },
    });
  }

  public getAllRoom(): Promise<RoomRecord[]> {
    return this.serverRestApiService.query({
      query: getAllRoomQuery,
    });
  }

  public getMyRooms(): Promise<RoomRecord[]> {
    return this.getAllRoom().then((rooms) => {
      return this.accountService.getMe().then((me) => {
        let results = [];
        for (let i = 0; i < rooms.length; i++) {
          if (rooms[i].ownerAccountId == me.id) {
            results.push(rooms[i]);
          }
        }
        return results;
      });
    });
  }

  public getRoomConfig(resourceId): Promise<FlagParser> {
    return this.getData(resourceId).then((roomData) => {
      return this.accountService
        .getPeerKeyring(roomData.ownerAccountId)
        .then((peerKr) => {
          return this.serverRestApiService
            .query({
              query: roomConfigQuery,
              variables: { id: resourceId },
            })
            .then((res: roomConfigRecord[]) => {
              let blocks = [];
              console.log("conf res", res);
              res.forEach((element) => {
                blocks.push([element.block, element.signature]);
              });

              return GroupConfigBlockChain.load(
                resourceId,
                blocks,
                peerKr
              ).then((chain) => {
                return new FlagParser(chain);

                //chain.new_block()

                // new_block viszsaad: raw_block, signature, member_deltas, group_deltas
                // member_deltas ból kinyerem az id-ket. Csinálok objot:
                // {id: roomkeyring.dump(selfkeyring, peerKeyring(id)).pinnedKey}
                // createRoom(roomId, rawBlock, signature, generált obj)
              });
            });
        });
    });
  }

  public newRoom(name): Promise<boolean> {
    console.log("99 - Begin create room with name", name);

    return this.serverRestApiService
      .mutate({
        query: createNewIdQuery,
        variables: {
          type: "room-id",
        },
      })
      .then((id) => {
        console.log("99 - New room id is", id);

        let roomKr = RoomKeyring.generate(id);

        return GroupConfigBlockChain.load(
          id,
          [],
          this.authService.getSelfAccountKeyring()
        ).then((chain) => {
          return this.accountService.getMe().then((me) => {
            return chain
              .new_block(
                this.authService.getSelfAccountKeyring(),
                id,
                {
                  [me.id]: FlagParser.allFlag(),
                },
                0
              )
              .then(
                ([newBlock, newBlockSignature, memberDeltas, groupDeltas]) => {
                  let deltaKeys = {};
                  //console.log("memberDeltas", memberDeltas);

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

                  for (let key in memberDeltas) {
                    // its only me now
                    if (
                      (memberDeltas[key] &
                        GroupConfigBlockMemberFlags.ACTIVE) ==
                      1
                    ) {
                      promises.push(
                        this.accountService
                          .getPeerKeyring(key)
                          .then((peerKr) => {
                            return roomKr
                              .dump(
                                this.authService.getSelfAccountKeyring(),
                                peerKr
                              )
                              .then((dump) => {
                                deltaKeys[key] = dump[RoomKeyring.PINNED];
                                return;
                              });
                          })
                      );
                    }
                  }

                  return Promise.all(promises).then(() => {
                    return this.serverRestApiService
                      .mutate({
                        query: createRoomQuery,
                        variables: {
                          roomId: id,
                          newBlock,
                          newBlockSignature,
                          deltaKeys,
                        },
                      })
                      .then((res) => {
                        // the room is created, set the name too

                        console.log("99 - Room created", res, id, name);

                        return this.editData(id, name).then(() => {
                          //console.log("done");
                          return true;
                        });
                      });
                  });
                }
              );
          });
        });
      });
  }

  public deleteRoom(resourceId) {
    return this.serverRestApiService.mutate({
      query: deleteRoomQuery,
      variables: {
        roomId: resourceId,
      },
    });
  }

  private getChain(resourceId): Promise<GroupConfigBlockChain> {
    return this.serverRestApiService
      .query({
        query: roomConfigQuery,
        variables: { id: resourceId },
      })
      .then((res: roomConfigRecord[]) => {
        let blocks = [];

        res.forEach((element) => {
          blocks.push([element.block, element.signature]);
        });

        return GroupConfigBlockChain.load(
          resourceId,
          blocks,
          this.authService.getSelfAccountKeyring()
        );
      });
  }

  public saveConfig(
    resourceId,
    members: RoomMember[],
    roomPermissions: RoomPermissions
  ): Promise<any> {
    return this.serverRestApiService
      .mutate({
        query: createNewIdQuery,
      })
      .then((id) => {
        //console.log("newId", id);
        //chain.new_block()

        // new_block viszsaad: raw_block, signature, member_deltas, group_deltas
        // member_deltas ból kinyerem az id-ket. Csinálok objot:
        // {id: roomkeyring.dump(selfkeyring, peerKeyring(id)).pinnedKey}
        // createRoom(roomId, rawBlock, signature, generált obj)
        let permissions = {};
        members.forEach((m) => {
          if (m.hasChanged) {
            permissions[m.user.id] = FlagParser.parseToFlags(
              m.permissions,
              m.newPermissions
            );
          }
        });

        let roomFlag = 0;
        if (roomPermissions.allowAnonym === true) {
          roomFlag = GroupConfigBlockFlags.ALLOW_ANONYMOUS;
        } else if (roomPermissions.allowAnonym === false) {
          roomFlag = GroupConfigBlockFlags.ALLOW_ANONYMOUS_NEGATE;
        }

        if (
          Object.keys(permissions).length > 0 ||
          Object.keys(roomPermissions).length > 0
        ) {
          return this.getChain(resourceId).then((chain) => {
            return chain
              .new_block(
                this.authService.getSelfAccountKeyring(),
                id,
                permissions,
                roomFlag
              )
              .then(
                async ([
                  newBlock,
                  newBlockSignature,
                  memberDeltas,
                  groupDeltas,
                ]) => {
                  let deltaKeys = {};

                  let roomKr = await this.makeKeyring(
                    await this.getData(resourceId)
                  );

                  for (let key in memberDeltas) {
                    // only active users
                    if (
                      (memberDeltas[key] &
                        GroupConfigBlockMemberFlags.ACTIVE) ==
                      1
                    ) {
                      let dump = await roomKr
                        .dump(
                          this.authService.getSelfAccountKeyring(),
                          await this.accountService.getPeerKeyring(key)
                        )
                        .catch((err) => {
                          console.error("can not dump key", err);
                          throw err;
                        });

                      deltaKeys[key] = dump[RoomKeyring.PINNED];
                    }
                  }

                  //console.log("deltas", Object.assign({}, deltaKeys));
                  //return new Promise((resolve, reject)=>{
                    //setTimeout(()=>{
                      return this.serverRestApiService
                      .mutate({
                        query: createRoomConfigBlockQuery,
                        variables: {
                          roomId: resourceId,
                          newBlock,
                          newBlockSignature,
                          deltaKeys,
                        },
                      })
                      .then((res) => {
                        //console.log("res", res);
                        //resolve(true)
                        return true;
                      })//.catch(reject)
                    //}, 5000);
                  //})
                  
                }
              );
          });
        } else {
          //console.log("nothing to change in config block");
          return true;
        }
      });
  }

  public getRoomPermissions(resourceId): Promise<RoomPermissionRecord> {
    return this.serverRestApiService
      .query({
        query: roomPermissionsQuery,
        variables: { id: resourceId },
      })
      .then((res: RoomPermissionRecord) => {
        //console.log("res", res);
        return res;
      });
  }

  public getMyRoomPermissions(resourceId): Promise<RoomMyPermissionRecord> {
    return this.serverRestApiService
      .query({
        query: roomMyPermissionsQuery,
        variables: { id: resourceId },
      })
      .then((res: RoomMyPermissionRecord) => {
        console.log("myroom", res);
        return res;
      });
  }

  public joinRoom(roomId) {
    // put it into cache
    return this.getData(roomId).then(() => {
      return this.serverRestApiService.mutate({
        query: joinRoomQuery,
        variables: { roomId },
      });
    });
  }

  public leaveRoom(roomId) {
    return this.serverRestApiService.mutate({
      query: leaveRoomQuery,
      variables: { roomId },
    });
  }

  public editAccountPermissionModerator(resourceId, accountId, value: boolean) {
    return this.serverRestApiService.mutate({
      query: editRoomAccountPermissionQuery,
      variables: {
        roomId: resourceId,
        accountId,
        attribute: "msgBrdModerate",
        value,
      },
    });
  }

  public getRoomMembers(roomId): Promise<RoomMemberRecord> {
    return this.serverRestApiService.query({
      query: roomMemberQuery,
      variables: {
        id: roomId,
      },
    });
  }

  public editPlacement(id, groupId, ordering: number) {
    return this.serverRestApiService.mutate({
      query: editRoomPlacementQuery,
      variables: {
        roomId: id,
        ordering,
        groupId,
      },
    });
  }
}
