import { Injectable } from '@angular/core';
import { hobaParseChallengeHeader } from '@src/assets/js/hoba.js';
import { signChallenge } from '@src/assets/js/hoba-pgp.js';
import { environment } from '@src/environments/environment';
import { LoggingService } from '@app/_services/logging.service';
import { MutableKeyStore, MutablePgpKeyStore } from '@app/_pgp';
import { ErrorDialogService } from '@app/_services/error-dialog.service';
import { HttpClient } from '@angular/common/http';
import { HttpError } from '@app/_helpers/global-error-handler';

@Injectable({
  providedIn: 'root',
})
export class AuthService {
  sessionToken: any;
  sessionLoginCount: number = 0;
  mutableKeyStore: MutableKeyStore;

  constructor(
    private httpClient: HttpClient,
    private loggingService: LoggingService,
    private errorDialogService: ErrorDialogService
  ) {
    this.mutableKeyStore = new MutablePgpKeyStore();
  }

  async init(): Promise<void> {
    await this.mutableKeyStore.loadKeyring();
    // TODO setting these together should be atomic
    if (sessionStorage.getItem(btoa('CICADA_SESSION_TOKEN'))) {
      this.sessionToken = sessionStorage.getItem(btoa('CICADA_SESSION_TOKEN'));
    }
    if (localStorage.getItem(btoa('CICADA_PRIVATE_KEY'))) {
      await this.mutableKeyStore.importPrivateKey(localStorage.getItem(btoa('CICADA_PRIVATE_KEY')));
    }
  }

  setState(s): void {
    document.getElementById('state').innerHTML = s;
  }

  getWithToken(): void {
    const xhr: XMLHttpRequest = new XMLHttpRequest();
    xhr.responseType = 'text';
    xhr.open('GET', environment.cicMetaUrl + window.location.search.substring(1));
    xhr.setRequestHeader('Authorization', 'Bearer ' + this.sessionToken);
    xhr.setRequestHeader('Content-Type', 'application/json');
    xhr.setRequestHeader('x-cic-automerge', 'none');
    xhr.addEventListener('load', (e) => {
      if (xhr.status === 401) {
        throw new Error('login rejected');
      }
      this.sessionLoginCount++;
      this.setState('Click button to log in');
      return;
    });
    xhr.send();
  }

  // TODO rename to send signed challenge and set session. Also separate these responsibilities
  sendResponse(hobaResponseEncoded: any): Promise<boolean> {
    return new Promise((resolve, reject) => {
      const xhr: XMLHttpRequest = new XMLHttpRequest();
      xhr.responseType = 'text';
      xhr.open('GET', environment.cicMetaUrl + window.location.search.substring(1));
      xhr.setRequestHeader('Authorization', 'HOBA ' + hobaResponseEncoded);
      xhr.setRequestHeader('Content-Type', 'application/json');
      xhr.setRequestHeader('x-cic-automerge', 'none');
      xhr.addEventListener('load', (e) => {
        if (xhr.status !== 200) {
          const error = new HttpError(xhr.statusText, xhr.status);
          return reject(error);
        }
        this.sessionToken = xhr.getResponseHeader('Token');
        sessionStorage.setItem(btoa('CICADA_SESSION_TOKEN'), this.sessionToken);
        this.sessionLoginCount++;
        this.setState('Click button to log in');
        return resolve(true);
      });
      xhr.send();
    });
  }

  getChallenge(): void {
    const xhr: XMLHttpRequest = new XMLHttpRequest();
    xhr.responseType = 'arraybuffer';
    xhr.open('GET', environment.cicMetaUrl + window.location.search.substring(1));
    xhr.onload = async (e) => {
      if (xhr.status === 401) {
        const authHeader = xhr.getResponseHeader('WWW-Authenticate');
        const o = hobaParseChallengeHeader(authHeader);
        this.loginResponse(o);
      }
    };
    xhr.send();
  }

  login(): boolean {
    if (this.sessionToken !== undefined) {
      try {
        this.getWithToken();
        return true;
      } catch (e) {
        this.loggingService.sendErrorLevelMessage('Login token failed', this, { error: e });
      }
    } else {
      try {
        this.getChallenge();
      } catch (e) {
        this.loggingService.sendErrorLevelMessage('Login challenge failed', this, { error: e });
      }
    }
    return false;
  }

  async loginResponse(o: { challenge: string; realm: any }): Promise<any> {
    return new Promise(async (resolve, reject) => {
      try {
        const r = await signChallenge(
          o.challenge,
          o.realm,
          environment.cicMetaUrl,
          this.mutableKeyStore
        );
        const sessionTokenResult: boolean = await this.sendResponse(r);
      } catch (error) {
        if (error instanceof HttpError) {
          if (error.status === 403) {
            this.errorDialogService.openDialog({
              message: 'You are not authorized to use this system',
            });
          }
          if (error.status === 401) {
            this.errorDialogService.openDialog({
              message:
                'Unable to authenticate with the service. ' +
                'Please speak with the staff at Grassroots ' +
                'Economics for requesting access ' +
                'staff@grassrootseconomics.net.',
            });
          }
        }
        // TODO define this error
        this.errorDialogService.openDialog({ message: 'Incorrect key passphrase.' });
        resolve(false);
      }
    });
  }

  loginView(): void {
    document.getElementById('one').style.display = 'none';
    document.getElementById('two').style.display = 'block';
    this.setState('Click button to log in with PGP key ' + this.mutableKeyStore.getPrivateKeyId());
  }

  async setKey(privateKeyArmored): Promise<boolean> {
    try {
      const isValidKeyCheck = await this.mutableKeyStore.isValidKey(privateKeyArmored);
      if (!isValidKeyCheck) {
        throw Error('The private key is invalid');
      }
      // TODO leaving this out for now.
      // const isEncryptedKeyCheck = await this.mutableKeyStore.isEncryptedPrivateKey(privateKeyArmored);
      // if (!isEncryptedKeyCheck) {
      //   throw Error('The private key doesn\'t have a password!');
      // }
      const key = await this.mutableKeyStore.importPrivateKey(privateKeyArmored);
      localStorage.setItem(btoa('CICADA_PRIVATE_KEY'), privateKeyArmored);
    } catch (err) {
      this.loggingService.sendErrorLevelMessage(
        `Failed to set key: ${err.message || err.statusText}`,
        this,
        { error: err }
      );
      this.errorDialogService.openDialog({
        message: `Failed to set key: ${err.message || err.statusText}`,
      });
      return false;
    }
    this.loginView();
    return true;
  }

  logout(): void {
    sessionStorage.removeItem(btoa('CICADA_SESSION_TOKEN'));
    this.sessionToken = undefined;
    window.location.reload();
  }

  getTrustedUsers(): any {
    const trustedUsers: Array<any> = [];
    this.mutableKeyStore.getPublicKeys().forEach((key) => trustedUsers.push(key.users[0].userId));
    return trustedUsers;
  }

  async getPublicKeys(): Promise<any> {
    return await fetch(environment.publicKeysUrl).then((res) => {
      if (!res.ok) {
        // TODO does angular recommend an error interface?
        throw Error(`${res.statusText} - ${res.status}`);
      }
      return res.text();
    });
  }

  getPrivateKey(): any {
    return this.mutableKeyStore.getPrivateKey();
  }
}
