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, rejectBody } from '@app/_helpers/global-error-handler';

@Injectable({
  providedIn: 'root',
})
export class AuthService {
  sessionToken: any;
  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(): Promise<boolean> {
    return new Promise((resolve, reject) => {
      const headers = {
        Authorization: 'Bearer ' + this.sessionToken,
        'Content-Type': 'application/json;charset=utf-8',
        'x-cic-automerge': 'none',
      };
      const options = {
        headers,
      };
      fetch(environment.cicMetaUrl, options).then((response) => {
        if (response.status === 401) {
          return reject(rejectBody(response));
        }
        return resolve(true);
      });
    });
  }

  // TODO rename to send signed challenge and set session. Also separate these responsibilities
  sendResponse(hobaResponseEncoded: any): Promise<boolean> {
    return new Promise((resolve, reject) => {
      const headers = {
        Authorization: 'HOBA ' + hobaResponseEncoded,
        'Content-Type': 'application/json;charset=utf-8',
        'x-cic-automerge': 'none',
      };
      const options = {
        headers,
      };
      fetch(environment.cicMetaUrl, options).then((response) => {
        if (response.status === 401) {
          return reject(rejectBody(response));
        }
        this.sessionToken = response.headers.get('Token');
        sessionStorage.setItem(btoa('CICADA_SESSION_TOKEN'), this.sessionToken);
        this.setState('Click button to log in');
        return resolve(true);
      });
    });
  }

  getChallenge(): Promise<any> {
    return new Promise((resolve, reject) => {
      fetch(environment.cicMetaUrl).then(async (response) => {
        if (response.status === 401) {
          const authHeader: string = response.headers.get('WWW-Authenticate');
          return resolve(hobaParseChallengeHeader(authHeader));
        }
        if (!response.ok) {
          return reject(rejectBody(response));
        }
      });
    });
  }

  async login(): Promise<boolean> {
    if (this.sessionToken !== undefined) {
      try {
        const response: boolean = await this.getWithToken();
        return response === true;
      } catch (e) {
        this.loggingService.sendErrorLevelMessage('Login token failed', this, { error: e });
      }
    } else {
      try {
        const o = await this.getChallenge();
        const response: boolean = await this.loginResponse(o);
        return response === true;
      } 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 response: boolean = await this.sendResponse(r);
        resolve(response);
      } catch (error) {
        if (error instanceof HttpError) {
          if (error.status === 403) {
            this.errorDialogService.openDialog({
              message: 'You are not authorized to use this system',
            });
          } else 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.',
            });
          }
        } else {
          // 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'));
    localStorage.removeItem(btoa('CICADA_PRIVATE_KEY'));
    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 new Promise((resolve, reject) => {
      fetch(environment.publicKeysUrl).then((res) => {
        if (!res.ok) {
          // TODO does angular recommend an error interface?
          return reject(rejectBody(res));
        }
        return resolve(res.text());
      });
    });
  }

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

  getPrivateKeyInfo(): any {
    return this.getPrivateKey().users[0].userId;
  }
}
