import { Component, Inject, LOCALE_ID, OnDestroy, 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 {
  NanoSlotRecord,
  RemoteConfigRecord,
  DriveRoomRecord,
  DriveRecord,
} from 'src/app/shared/server-services/query-records/nano-records';
import {
  waitingNanosQuery,
  getNanoSlotsQuery,
  roomConfigQuery,
  getNanoVersionQuery,
} 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 { SubscriptionService } from 'src/app/shared/server-services/subscription.service';
import {
  SubscriptionServiceEvent,
  SubscriptionServiceEventType,
} from 'src/app/shared/server-services/subscription-event';
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';
import { SnackBarService } from 'src/app/shared/services/snackbar.service';
import { RoomData } from 'src/app/shared/server-services/query-records/room-records';
import { sideMenuActiveObserver } from 'src/app/shared/shared-observers';
import { AppStorage } from 'src/app/shared/app-storage';
import { AdvancedSettingsEnum } from '../advanced-settings/advanced-settings.component';
import { NANO_VERSION_FOR_FEATURE_REQUIRE_EXPLICIT_PEER_TRUST } from 'src/app/shared/drive-version';

@Component({
  selector: 'app-nano-manager',
  templateUrl: './nano-manager.component.html',
  styleUrls: ['./nano-manager.component.scss'],
})
export class NanoManagerComponent implements OnInit, OnDestroy {
  nanoSlots: Array<NanoSlotRecord> = new Array();
  filteredNanoSlots: Array<NanoSlotRecord> = new Array();
  public waitingNanoSlots: number;
  nanoSlotFilter: string;
  selectedNanoSlot: NanoSlotRecord;
  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 } = {};

  public isOnlyTrustedUsersFeatureAvailable: boolean = false;

  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,
    private snackbarService: SnackBarService,
    @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.getNanoSlots();
    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.waitingNanoSlots = Object.keys(response).length;
      });
  }

  private async getNanoSlots() {
    this.sidebarReady = false;
    this.nanoSlots = <Array<NanoSlotRecord>>(
      await this.serverRestApi.query({ query: getNanoSlotsQuery })
    );

    this.nanoSlots
      .filter((ns) => ns.expiration == 946688461)
      .forEach((nc) => (nc.isEtalonSlot = true));
    this.nanoSlots = this.nanoSlots.filter(
      (nc) => new Date(nc.expiration * 1000) > new Date() || nc.isEtalonSlot
    );

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

    var activeNanoSlots = this.nanoSlots.filter(
      (nanoSlot) => nanoSlot.lastActive && this.wasActiveWithinMinutes(5, nanoSlot)
    );

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

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

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

          var unresponsiveNanoSlot = this.nanoSlots.find((nc) => nc.id == nanoSlot.id);
          unresponsiveNanoSlot.occupiedSlot = true;

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

          counter++;

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

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

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

  private sortNanoSlots() {
    this.nanoSlots = this.nanoSlots.sort((a, b) => {
      if (a.isEtalonSlot) return -1;

      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.filteredNanoSlots = this.nanoSlots.filter(
      (nanoClient) =>
        !this.nanoSlotFilter ||
        this.nanoSlotFilter.length == 0 ||
        nanoClient.detail?.name.toLowerCase().indexOf(this.nanoSlotFilter.toLowerCase()) > -1
    );
  }

  public selectNanoSlot(nanoSlot: NanoSlotRecord, force: boolean = null) {
    if (nanoSlot == this.selectedNanoSlot && !force) {
      this.showSideBar = false;
      return;
    }

    this.isOnlyTrustedUsersFeatureAvailable =
      AppStorage.getItem(AdvancedSettingsEnum.EXP_IDENTITY_SIGNATURE) == '1' &&
      nanoSlot.detail.version >= NANO_VERSION_FOR_FEATURE_REQUIRE_EXPLICIT_PEER_TRUST;

    var roomCounter = 0;

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

        this.selectedNanoSlot.detail.remoteConfig.drives.forEach((drive) => {
          drive.roomDetails = new Array<DriveRoomRecord>();
          drive.rooms?.forEach((room) => {
            this.roomService
              .getRoom(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.sortNanoSlotRooms();
              })
              .catch((err) => {
                console.error('Error during room details query', room, err);
                drive.roomDetails.push({
                  id: room,
                  name: null,
                  isValid: false,
                  avatar: null,
                });
                roomCounter++;
              });
          });
        });

        this.sortNanoSlotDrives();

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

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

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

  private sortNanoSlotRooms() {
    this.selectedNanoSlot.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: {
        nanoSlot: this.selectedNanoSlot,
        adminPw: this.adminPasswords[this.selectedNanoSlot.id],
      },
      position: { top: '20px' },
    });

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

      this.nanoService.adminCreateDrive(
        this.selectedNanoSlot.id,
        resDrivePath,
        this.adminPasswords[this.selectedNanoSlot.id],
        (res) => {
          this.selectNanoSlot(this.selectedNanoSlot, true);
          this.snackbarService.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.selectedNanoSlot.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.selectedNanoSlot.id])
        .then((attachResponse) => {
          this.snackbarService.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.selectedNanoSlot.id, () =>
              this.attachRoomToDrive(
                drive.path,
                resRoom.id,
                this.adminPasswords[this.selectedNanoSlot.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.selectedNanoSlot.id,
          drive.path,
          this.adminPasswords[this.selectedNanoSlot.id],
          (res) => {
            this.snackbarService.showSnackbar(marker('Drive deleted successfully!'));
            this.selectNanoSlot(this.selectedNanoSlot, true);
          },
          (err) => {
            console.log('err', err);

            if (err.error == ServerError.NANO_REQUEST_ERROR_ADMIN_PASSWORD)
              this.openProvideAdminPasswordDialog(this.selectedNanoSlot.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.selectedNanoSlot.id,
          drive.path,
          room.id,
          this.adminPasswords[this.selectedNanoSlot.id],
          (res) => {
            this.snackbarService.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.selectedNanoSlot.id, () =>
                this.removeRoomFromDrive(drive, room)
              );
          }
        );
      });
  }

  public attachRoomToDrive(
    drivePath: string,
    roomId: string,
    adminPassword: string
  ): Promise<Object> {
    return this.roomService
      .getRoomBlockChain(roomId, this.authService.getSelfAccountKeyring())
      .then((chain) => {
        return new Promise((resolve, reject) => {
          this.nanoService.adminAttachDrive(
            this.selectedNanoSlot.id,
            drivePath,
            roomId,
            chain.head_names,
            adminPassword,
            (res) => {
              resolve(res);
            },
            reject
          );
        });
      });
  }

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

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

  public setRequireExplicitPeerTrust(checked: boolean) {
    this.nanoService.adminEditRequireExplicitPeerTrust(
      this.selectedNanoSlot.id,
      checked,
      this.adminPasswords[this.selectedNanoSlot.id],
      (res) => {
        this.selectNanoSlot(this.selectedNanoSlot, true);
        this.snackbarService.showSnackbar(marker('Require explicit peer trust change successful!'));
      },
      (err) => {
        console.log('err', err);

        if (err.error == ServerError.NANO_REQUEST_ERROR_ADMIN_PASSWORD)
          this.openProvideAdminPasswordDialog(this.selectedNanoSlot.id, () =>
            this.setRequireExplicitPeerTrust(checked)
          );
      }
    );
  }

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

        if (err.error == ServerError.NANO_REQUEST_ERROR_ADMIN_PASSWORD)
          this.openProvideAdminPasswordDialog(this.selectedNanoSlot.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.selectedNanoSlot.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.snackbarService.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.snackbarService.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: NanoSlotRecord): boolean {
    var currentDate = new Date();
    var currentDateMinutesAgo = new Date(
      currentDate.setMinutes(currentDate.getMinutes() - minutes)
    );
    return new Date(nanoClient.lastActive * 1000) > currentDateMinutesAgo;
  }

  public getLastActiveDate(nanoClient: NanoSlotRecord): 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: NanoSlotRecord): 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: NanoSlotRecord): boolean {
    return this.versionService.isValidVersion(nanoClient.detail.version);
  }
}
