import { Component, Inject, LOCALE_ID, OnInit } from "@angular/core";
import { MatDialog } from "@angular/material/dialog";
import { marker } from "@biesbjerg/ngx-translate-extract-marker";
import { GroupConfigBlockChain } from "src/app/shared/crypto/group_config/chain";
import { AuthService } from "src/app/shared/server-services/auth.service";
import { NanoService } from "src/app/shared/nano/nano.service";
import {
  NanoClientRecord,
  RemoteConfigRecord,
  DriveRoomRecord,
  DriveRecord,
} from "src/app/shared/server-services/query-records/nano-records";
import {
  waitingNanosQuery,
  getNanoSlotsQuery,
  roomConfigQuery,
  getVersionQuery,
} from "src/app/shared/server-services/querys";
import { RoomService } from "src/app/shared/server-services/room.service";
import { ServerError } from "src/app/shared/server-services/server-errors";
import { DialogService } from "src/app/shared/services/dialog.service";
import { ServerRestApiService } from "src/app/shared/services/server-rest-api.service";
import { TitleService } from "src/app/shared/services/title.service";
import { AdminPasswordDialogComponent } from "./admin-password-dialog/admin-password-dialog.component";
import { AttachRoomDialogComponent } from "./attach-room-dialog/attach-room-dialog.component";
import { CreateDriveDialogComponent } from "./create-drive-dialog/create-drive-dialog.component";
import { RoomData } from "src/app/shared/server-services/room.service";
import { SubscriptionService } from "src/app/shared/server-services/subscription.service";
import {
  SubscriptionServiceEvent,
  SubscriptionServiceEventType,
} from "src/app/shared/server-services/subscription-event";
import { sideMenuActiveObserver } from "../header/header.component";
import { EnvironmentService } from "src/environments/environment.service";
import { TranslateService } from "@ngx-translate/core";
import { formatDate } from "@angular/common";
import { of } from "rxjs";
import { delay } from "rxjs/operators";
import { VersionService } from "src/app/shared/services/version.service";
import { ClipboardService } from "src/app/shared/services/clipboard.service";

@Component({
  selector: "app-nano-manager",
  templateUrl: "./nano-manager.component.html",
  styleUrls: ["./nano-manager.component.scss"],
})
export class NanoManagerComponent implements OnInit {
  nanoClients: Array<NanoClientRecord> = new Array();
  filteredNanoClients: Array<NanoClientRecord> = new Array();
  public waitingNanoClients: number;
  nanoClientFilter: string;
  selectedNanoClient: NanoClientRecord;
  public websiteDownloadPage: string =
    this.environmentService.url_landing + "/download";

  sidebarReady: Boolean = false;
  showSideBar: Boolean = true;
  hasLicense: Boolean = undefined;
  outOfDateNano: Boolean = false;

  newNanoName: string;

  adminPasswords: { [key: number]: string } = {};

  constructor(
    private serverRestApi: ServerRestApiService,
    private nanoService: NanoService,
    private authService: AuthService,
    private roomService: RoomService,
    private restApiService: ServerRestApiService,
    private dialog: MatDialog,
    private dialogService: DialogService,
    private subscriptionService: SubscriptionService,
    private titleService: TitleService,
    private environmentService: EnvironmentService,
    private translate: TranslateService,
    private versionService: VersionService,
    private clipboardService: ClipboardService,
    @Inject(LOCALE_ID) private locale: string
  ) {
    this.titleService.setTabTitle(marker("Nano Manager"));
  }

  ngOnInit() {
    sideMenuActiveObserver.next(false);

    this.subscriptionService.subscribe(
      SubscriptionServiceEvent.NANO_WAITING_EVENT,
      SubscriptionServiceEventType.CREATE,
      this.handleNewWaitingNano.bind(this)
    );
    this.subscriptionService.subscribe(
      SubscriptionServiceEvent.NANO_WAITING_EVENT,
      SubscriptionServiceEventType.DELETE,
      this.handleLeftWaitingNano.bind(this)
    );

    this.getNanoClients();
    this.countWaitingNanos();
  }

  ngOnDestroy() {
    this.subscriptionService.unsubscribe(
      SubscriptionServiceEvent.NANO_WAITING_EVENT,
      SubscriptionServiceEventType.CREATE,
      this.handleNewWaitingNano
    );
    this.subscriptionService.unsubscribe(
      SubscriptionServiceEvent.NANO_WAITING_EVENT,
      SubscriptionServiceEventType.DELETE,
      this.handleLeftWaitingNano
    );
  }

  /**
   *
   * @param record Object { id: "24LFFGzv22222", waiterId: "12", cmd: "C" }
   */
  private handleNewWaitingNano(record) {
    this.countWaitingNanos();
  }

  /**
   *
   * @param record Object { id: "24LFFGzv22222", waiterId: "12", cmd: "D" }
   */
  private handleLeftWaitingNano(record) {
    this.countWaitingNanos();
  }

  private countWaitingNanos() {
    this.serverRestApi
      .query({
        query: waitingNanosQuery,
      })
      .then((response: { [key: string]: number }) => {
        this.waitingNanoClients = Object.keys(response).length;
      });
  }

  private async getNanoClients() {
    this.sidebarReady = false;
    this.nanoClients = <Array<NanoClientRecord>>(
      await this.serverRestApi.query({ query: getNanoSlotsQuery })
    );

    this.nanoClients = this.nanoClients.filter(
      (nc) => new Date(nc.expiration * 1000) > new Date()
    );

    this.hasLicense = this.nanoClients.length > 0;

    var activeNanoClients = this.nanoClients.filter(
      (nanoClient) =>
        nanoClient.lastActive && this.wasActiveWithinMinutes(5, nanoClient)
    );

    var counter = 0;
    for (let i = 0; i < activeNanoClients.length; i++) {
      const nanoClient = activeNanoClients[i];

      this.nanoService.adminGetInfo(
        nanoClient.id,
        (res) => {
          nanoClient.detail = res;
          counter++;

          if (counter == activeNanoClients.length) this.setSideBarReady();
          if (!this.versionService.isValidVersion(nanoClient.detail.version)) {
            this.outOfDateNano = true;
          }
        },
        (err) => {
          console.error("Get Nano Info error for slot " + nanoClient.id, err);

          var unresponsiveNanoClient = this.nanoClients.find(
            (nc) => nc.id == nanoClient.id
          );
          unresponsiveNanoClient.occupiedSlot = true;

          of(unresponsiveNanoClient)
            .pipe(
              delay(
                this.getWhenSlotAvailableInSeconds(unresponsiveNanoClient) *
                  1000
              )
            )
            .subscribe((unresponsiveNanoClient) => {
              unresponsiveNanoClient.occupiedSlot = false;
            });

          counter++;

          if (counter == activeNanoClients.length) this.setSideBarReady();
        }
      );
    }

    if (activeNanoClients.length == 0) this.setSideBarReady();
  }

  private setSideBarReady() {
    this.sortNanoClients();
    this.filterInput();
    this.sidebarReady = true;
  }

  private sortNanoClients() {
    this.nanoClients = this.nanoClients.sort((a, b) => {
      if (a.lastActive === b.lastActive) {
        return 0;
      }

      if (a.lastActive && b.lastActive) {
        if (a.lastActive > b.lastActive) return -1;
        if (a.lastActive < b.lastActive) return 1;
      }

      if (a.lastActive) {
        return -1;
      }

      if (b.lastActive) {
        return 1;
      }
    });
  }

  public filterInput() {
    this.filteredNanoClients = this.nanoClients.filter(
      (nanoClient) =>
        !this.nanoClientFilter ||
        this.nanoClientFilter.length == 0 ||
        nanoClient.detail?.name
          .toLowerCase()
          .indexOf(this.nanoClientFilter.toLowerCase()) > -1
    );
  }

  public selectNanoClient(nanoClient: NanoClientRecord, force: boolean = null) {
    if (nanoClient == this.selectedNanoClient && !force) {
      this.showSideBar = false;
      return;
    }

    var roomCounter = 0;

    this.nanoService.adminGetRemoteConfig(
      nanoClient.id,
      this.adminPasswords[nanoClient.id],
      (res: RemoteConfigRecord) => {
        nanoClient.detail.remoteConfig = res;
        this.selectedNanoClient = nanoClient;
        this.newNanoName = nanoClient.detail.name;

        this.selectedNanoClient.detail.remoteConfig.drives.forEach((drive) => {
          drive.roomDetails = new Array<DriveRoomRecord>();
          drive.rooms?.forEach((room) => {
            this.roomService
              .getData(room)
              .then((res) => {
                console.log("getData", res);
                drive.roomDetails.push({
                  id: room,
                  name: (<RoomData>res.data)?.name,
                  isValid: true,
                  avatar: (<RoomData>res.data)?.avatar,
                });
                roomCounter++;

                if (roomCounter == drive.rooms.length)
                  this.sortNanoClientRooms();
              })
              .catch((err) => {
                console.error("Error during room details query", room, err);
                drive.roomDetails.push({
                  id: room,
                  name: null,
                  isValid: false,
                  avatar: null,
                });
                roomCounter++;
              });
          });
        });

        this.sortNanoClientDrives();

        this.showSideBar = false;
      },
      (err) => {
        console.error(
          `Error during remote config query for Nano Client: ${nanoClient.id}`,
          err
        );

        if (err.error == ServerError.NANO_REQUEST_ERROR_ADMIN_PASSWORD)
          this.openProvideAdminPasswordDialog(nanoClient.id, () =>
            this.selectNanoClient(nanoClient, true)
          );
        else if (
          err.error == ServerError.NANO_REQUEST_ERROR_ADMIN_PASSWORD_MISSING
        )
          this.openFirstTimeAdminPasswordDialog(nanoClient.id);
      }
    );
  }

  private sortNanoClientDrives() {
    this.selectedNanoClient.detail.remoteConfig.drives =
      this.selectedNanoClient.detail.remoteConfig.drives.sort((a, b) =>
        this.getTopLevelFolderName(a.path).toUpperCase() >
        this.getTopLevelFolderName(b.path).toUpperCase()
          ? 1
          : -1
      );
  }

  private sortNanoClientRooms() {
    this.selectedNanoClient.detail.remoteConfig.drives.forEach((drive) => {
      drive.roomDetails = drive.roomDetails.sort((a, b) =>
        a.name.toUpperCase() > b.name.toUpperCase() ? 1 : -1
      );
    });
  }

  public openNewDriveDialog() {
    var ref = this.dialog.open(CreateDriveDialogComponent, {
      data: {
        nanoClient: this.selectedNanoClient,
        adminPw: this.adminPasswords[this.selectedNanoClient.id],
      },
      position: { top: "20px" },
    });

    ref.afterClosed().subscribe((resDrivePath: string) => {
      if (!resDrivePath) return;

      this.nanoService.adminCreateDrive(
        this.selectedNanoClient.id,
        resDrivePath,
        this.adminPasswords[this.selectedNanoClient.id],
        (res) => {
          this.selectNanoClient(this.selectedNanoClient, true);
          this.dialogService.showSnackbar(
            marker("Drive created successfully!")
          );
        },
        (err) => {
          console.log("err", err);

          if (err.error == ServerError.NANO_REQUEST_ERROR_ADMIN_PASSWORD) {
          }
          switch (err.error) {
            case ServerError.NANO_REQUEST_ERROR_ADMIN_PASSWORD:
              this.openProvideAdminPasswordDialog(
                this.selectedNanoClient.id,
                () => this.openNewDriveDialog()
              );
              break;
            case ServerError.NANO_REQUEST_ERROR_HANDLER_DENIED:
              this.dialogService
                .openAlertDialog(
                  marker("Invalid folder"),
                  marker(
                    "The Nano Client's security settings disallow the creation of the drive from the selected folder. Please choose a different folder."
                  )
                )
                .subscribe(() => this.openNewDriveDialog());
              break;
          }
        }
      );
    });
  }

  public openAttachRoomDialog(drive: DriveRecord) {
    var ref = this.dialog.open(AttachRoomDialogComponent, { data: drive });

    ref.afterClosed().subscribe((resRoom: DriveRoomRecord) => {
      if (!resRoom) return;
      this.attachRoomToDrive(
        drive.path,
        resRoom.id,
        this.adminPasswords[this.selectedNanoClient.id]
      )
        .then((attachResponse) => {
          this.dialogService.showSnackbar(
            marker("Room added to drive successfully!")
          );
          drive.roomDetails.push(resRoom);
        })
        .catch((err) => {
          console.error("Room failed to attach", err);
          if (err.error == ServerError.NANO_REQUEST_ERROR_ADMIN_PASSWORD)
            this.openProvideAdminPasswordDialog(
              this.selectedNanoClient.id,
              () =>
                this.attachRoomToDrive(
                  drive.path,
                  resRoom.id,
                  this.adminPasswords[this.selectedNanoClient.id]
                )
            );
        });
    });
  }

  public deleteDrive(drive: DriveRecord) {
    this.dialogService
      .openConfirmDialog(
        marker("Delete drive"),
        marker("Are you sure you want to delete this drive?")
      )
      .subscribe((closedWithOK: boolean) => {
        if (!closedWithOK) return;

        this.nanoService.adminDeleteDrive(
          this.selectedNanoClient.id,
          drive.path,
          this.adminPasswords[this.selectedNanoClient.id],
          (res) => {
            this.dialogService.showSnackbar(
              marker("Drive deleted successfully!")
            );
            this.selectNanoClient(this.selectedNanoClient, true);
          },
          (err) => {
            console.log("err", err);

            if (err.error == ServerError.NANO_REQUEST_ERROR_ADMIN_PASSWORD)
              this.openProvideAdminPasswordDialog(
                this.selectedNanoClient.id,
                () => this.deleteDrive(drive)
              );
          }
        );
      });
  }

  public removeRoomFromDrive(drive: DriveRecord, room: DriveRoomRecord) {
    this.dialogService
      .openConfirmDialog(
        marker("Remove room"),
        marker("Are you sure you want to remove this room?")
      )
      .subscribe((closedWithOK: boolean) => {
        if (!closedWithOK) return;

        this.nanoService.adminDetachDrive(
          this.selectedNanoClient.id,
          drive.path,
          room.id,
          this.adminPasswords[this.selectedNanoClient.id],
          (res) => {
            this.dialogService.showSnackbar(
              marker("Room removed from drive successfully!")
            );

            drive.roomDetails = drive.roomDetails.filter(
              (roomDetail) => roomDetail.id != room.id
            );
          },
          (err) => {
            console.error("err", err);

            if (err.error == ServerError.NANO_REQUEST_ERROR_ADMIN_PASSWORD)
              this.openProvideAdminPasswordDialog(
                this.selectedNanoClient.id,
                () => this.removeRoomFromDrive(drive, room)
              );
          }
        );
      });
  }

  public attachRoomToDrive(
    drivePath: string,
    roomId: string,
    adminPassword: string
  ): Promise<Object> {
    return new Promise((resolve, reject) => {
      this.restApiService
        .query({ query: roomConfigQuery, variables: { id: roomId } })
        .then((res) => {
          let blocks = [];
          res.forEach((element) => {
            blocks.push([element["block"], element["signature"]]);
          });

          GroupConfigBlockChain.load(
            roomId,
            blocks,
            this.authService.getSelfAccountKeyring()
          ).then((chain) => {
            this.nanoService.adminAttachDrive(
              this.selectedNanoClient.id,
              drivePath,
              roomId,
              chain.head_names,
              adminPassword,
              (res) => {
                resolve(res);
              },
              reject
            );
          });
        });
    });
  }

  public setAnonymous(anonymousDenied: boolean) {
    this.nanoService.adminEditDenyAnonymous(
      this.selectedNanoClient.id,
      anonymousDenied,
      this.adminPasswords[this.selectedNanoClient.id],
      (res) => {
        this.selectNanoClient(this.selectedNanoClient, true);
        this.dialogService.showSnackbar(
          marker("Anonymous access change successful!")
        );
      },
      (err) => {
        console.log("err", err);

        if (err.error == ServerError.NANO_REQUEST_ERROR_ADMIN_PASSWORD)
          this.openProvideAdminPasswordDialog(this.selectedNanoClient.id, () =>
            this.setAnonymous(anonymousDenied)
          );
      }
    );
  }

  public editName() {
    this.nanoService.adminEditNameRequest(
      this.selectedNanoClient.id,
      this.newNanoName,
      this.adminPasswords[this.selectedNanoClient.id],
      (res) => {
        this.selectedNanoClient.detail.name = this.newNanoName;
        this.dialogService.showSnackbar(
          marker("Nano client name change successful!")
        );
      },
      (err) => {
        console.log("err", err);

        if (err.error == ServerError.NANO_REQUEST_ERROR_ADMIN_PASSWORD)
          this.openProvideAdminPasswordDialog(this.selectedNanoClient.id, () =>
            this.editName()
          );
      }
    );
  }

  public openChangeAdminPasswordDialog() {
    var ref = this.dialog.open(AdminPasswordDialogComponent, {
      data: { adminPasswordMode: 1 },
    });

    ref.afterClosed().subscribe((res: any) => {
      if (!res) return;

      if (res.oldPassword)
        this.changeAdminPassword(
          res.password,
          res.oldPassword,
          this.selectedNanoClient.id
        );
    });
  }

  public openProvideAdminPasswordDialog(
    nanoClientId: number,
    onPasswordProvided: () => void
  ) {
    var ref = this.dialog.open(AdminPasswordDialogComponent, {
      data: { adminPasswordMode: 0 },
    });

    ref.afterClosed().subscribe((res: any) => {
      if (!res) return;

      this.adminPasswords[nanoClientId] = res.password;
      onPasswordProvided();
    });
  }

  public openFirstTimeAdminPasswordDialog(nanoClientId: number) {
    var ref = this.dialog.open(AdminPasswordDialogComponent, {
      data: { adminPasswordMode: 2 },
    });

    ref.afterClosed().subscribe((res: any) => {
      if (!res) return;

      this.adminPasswords[nanoClientId] = res.password;

      this.changeAdminPassword(res.password, res.oldPassword, nanoClientId);
    });
  }

  public changeAdminPassword(
    password: string,
    oldPassword: string,
    nanoClientId: number
  ) {
    this.nanoService.adminChangePasswordRequest(
      nanoClientId,
      oldPassword,
      password,
      (res) => {
        this.dialogService.showSnackbar(
          marker("Change admin password successful!")
        );
      },
      (err) => {
        if (err.error == ServerError.NANO_REQUEST_ERROR_ADMIN_PASSWORD)
          this.dialogService
            .openAlertDialog(
              marker("Change admin password"),
              marker("The given old password was incorrect.")
            )
            .subscribe(() => this.openChangeAdminPasswordDialog());
        else
          this.dialogService.openAlertDialog(
            marker("Change admin password"),
            marker("Change admin password error.")
          );
      }
    );
  }

  copyMessage(val: string) {
    this.clipboardService
      .copy(val)
      .then(() => {
        this.dialogService.showSnackbar(marker("Drive path copied!"));
      })
      .catch((err) => {
        console.error("clipboard err", err);
        this.dialogService.openAlertDialog(
          marker("Copy Error"),
          marker("Could not copy to the clipboard")
        );
      });
  }

  public getTopLevelFolderName(path: string): string {
    if (path.endsWith(":/") || path.endsWith(":\\")) return path;

    return path.split("/").pop().split("\\").pop();
  }

  private wasActiveWithinMinutes(
    minutes: number,
    nanoClient: NanoClientRecord
  ): boolean {
    var currentDate = new Date();
    var currentDateMinutesAgo = new Date(
      currentDate.setMinutes(currentDate.getMinutes() - minutes)
    );
    return new Date(nanoClient.lastActive * 1000) > currentDateMinutesAgo;
  }

  public getLastActiveDate(nanoClient: NanoClientRecord): string {
    var availableInSeconds = this.getWhenSlotAvailableInSeconds(nanoClient);
    var translatedText = this.translate.instant(marker("Last active: "));
    var date = new Date(availableInSeconds * 1000).toISOString().substr(14, 5);
    var translatedSuffixText =
      this.translate.instant(marker(" (available in")) + ` ${date})`;
    return (
      translatedText +
      formatDate(nanoClient.lastActive * 1000, "medium", this.locale) +
      translatedSuffixText
    );
  }

  private getWhenSlotAvailableInSeconds(nanoClient: NanoClientRecord): number {
    var dateWhenFree = new Date(nanoClient.lastActive * 1000 + 1000 * 300);
    var currentDate = new Date();
    var secondsUntilActive = Math.floor(
      (dateWhenFree.getTime() - currentDate.getTime()) / 1000
    );

    return secondsUntilActive;
  }

  public isNanoUpToDate(nanoClient: NanoClientRecord): boolean {
    return this.versionService.isValidVersion(nanoClient.detail.version);
  }
}
