import { Inject, Injectable, Optional } from "@angular/core";
import { base64 } from "rfc4648";
import { GlobalCrypto } from "../global-crypto";

export type LocalStorageData = {
  client_session_key: Uint8Array;
  master_key: Uint8Array;
  encrypting_secret_rsa: Uint8Array;
  encrypting_secret_ec: Uint8Array;
  signing_secret_ec: Uint8Array;
};

export interface LocalStorageInterface {
  setItem(key, value);
  getItem(key);
  clear();
}

@Injectable({
  providedIn: "root",
})
export class LocalStorageService {
  /**
   * It can mocked by tests this way, just replace it with a compatible mock object
   */
  private localStorage;

  /**
   *
   * @param alternativeLocalStorage - optionally - You can set custom localstorage, for example alternative local db,
   * or mock for tests. Just be sure you implement all the function in the LocalStorageInterface. The default is the window.localStorage
   */
  constructor(
    @Inject("propVal")
    @Optional()
    alternativeLocalStorage: LocalStorageInterface
  ) {
    if (!alternativeLocalStorage) {
      this.localStorage = window.localStorage;
    } else {
      this.localStorage = alternativeLocalStorage;
    }
  }

  /**
   * Save everything what we need to re-create the logined state.
   * Used AES_SIV_N96_128_256 for encrypting data. The key is a combined key: server-generated-key + client-generated-key
   * Uint8Array datas are stringified with base64 package
   * @param serverSessionKey - first half part of the encrypt/decrypt key,
   * you can get it during the login ('session_encryption_key')
   * @param storageData - all data, what we need to recreate the "logined" state
   */
  public save(
    serverSessionKey: Uint8Array,
    storageData: LocalStorageData
  ): any {
    return GlobalCrypto.HKDF_SHA256_256(
      new Uint8Array([...serverSessionKey, ...storageData.client_session_key]),
      new Uint8Array()
    ).then(async (combinedSessionKey) => {
      // encrypt everything with combinedKey except the client_session_key
      for (let key in storageData) {
        if (key == "client_session_key") continue;

        await GlobalCrypto.AES_SIV_N96_128_256_encrypt(
          storageData[key],
          combinedSessionKey
        ).then((encryptedData) => {
          this.localStorage.setItem(key, base64.stringify(encryptedData));
        });
      }

      // and finally save the clientSessionKey as raw
      this.localStorage.setItem(
        "client_session_key",
        base64.stringify(storageData["client_session_key"])
      );
      return;
    });
  }

  /**
   * Load everything what we need to re-create the logined state.
   * @param serverSessionKey - If you have a valid `k` and `s` cookie,
   * you can get it from the gql server: `query key { sessionEncryptionKey }`
   * @result decrypted LocalStorageData in a Promise, if there is a saved login state in
   * the localstorage, null otherwise
   */
  public load(serverSessionKey: Uint8Array): any {
    let storageData: LocalStorageData = {
      client_session_key: null,
      master_key: null,
      encrypting_secret_rsa: null,
      encrypting_secret_ec: null,
      signing_secret_ec: null,
    };

    let clientsk = this.localStorage.getItem("client_session_key");

    if (clientsk === null) return null;
    storageData.client_session_key = base64.parse(clientsk);

    return GlobalCrypto.HKDF_SHA256_256(
      new Uint8Array([...serverSessionKey, ...storageData.client_session_key]),
      new Uint8Array()
    ).then(async (combinedSessionKey) => {
      storageData.client_session_key = base64.parse(
        this.localStorage.getItem("client_session_key")
      );

      if (storageData.client_session_key === null) return null;

      // decrypt everything except the client_session_key what is already in raw format
      for (let key in storageData) {
        if (key == "client_session_key") continue;

        let cipher = this.localStorage.getItem(key);

        if (cipher === null) throw key + " is missing from localStorage";
        cipher = base64.parse(cipher);

        storageData[key] = await GlobalCrypto.AES_SIV_N96_128_256_decrypt(
          cipher,
          combinedSessionKey
        );
      }

      return storageData;
    });
  }

  public reset() {
    this.localStorage.clear();
  }
}
