import { AbstractSelfAccountKeyring } from './account_base';
import { AbstractPrivateKeyring } from './private';
import { PRIVATE_KEY_CRYPTO_CONTEXT } from '../context/private_key/__init__';
import { PrivateKeyConfigDumpArgs, PrivateKeyConfigLoadArgs } from '../context/private_key/args';
import { Curve25519KeyGenerator, Ed25519KeyGenerator } from '../base/key/ec';
import { ACCOUNT_ROOT_KEY_CRYPTO_CONTEXT } from '../context/account_key/root/__init__';
import {
  AccountRootKeyConfigDumpArgs,
  AccountRootKeyConfigLoadArgs,
} from '../context/account_key/root/args';
import { RandomKeyGenerator256 } from '../base/key/random';
import msgpack from 'msgpack-lite';
import { LOCAL_STORAGE_CRYPTO_CONTEXT } from '../context/local_storage/__init__';
import {
  LocalStorageConfigDumpArgs,
  LocalStorageConfigLoadArgs,
} from '../context/local_storage/args';

/**
 * This is a separate module to resolve a cyclic import.
 */
export class SelfAccountKeyring extends AbstractSelfAccountKeyring {
  /**
     * Loading the owner's account keyring is bootstrapped with the private-keyring. Following that the specific
        keys are all loaded with their own context, limiting the server's ability to be malicious. The keys are
        stored with authenticated encryption so they can be trusted if the loading succeeds.
     */
  public static load(
    private_kr: AbstractPrivateKeyring,
    account_id: string,
    master_key: Uint8Array,
    encrypting_secret_ec: Uint8Array,
    signing_secret_ec: Uint8Array
  ): any | Promise<Uint8Array> {
    return PRIVATE_KEY_CRYPTO_CONTEXT.load(
      new PrivateKeyConfigLoadArgs(master_key, private_kr, this.MASTER)
    ).then((master_key) => {
      let secretObj = {};
      secretObj[SelfAccountKeyring.MASTER] = master_key;

      let self_kr = new SelfAccountKeyring(account_id, {}, secretObj);

      return ACCOUNT_ROOT_KEY_CRYPTO_CONTEXT.load(
        new AccountRootKeyConfigLoadArgs(
          encrypting_secret_ec,
          self_kr,
          SelfAccountKeyring.CURVE25519
        )
      ).then((account_room_key_crypto_context) => {
        // load secret keys and regenerate their public parts
        let curve25519keyPair = Curve25519KeyGenerator.regenerate_key_pair(
          account_room_key_crypto_context
        );

        return ACCOUNT_ROOT_KEY_CRYPTO_CONTEXT.load(
          new AccountRootKeyConfigLoadArgs(signing_secret_ec, self_kr, SelfAccountKeyring.ED25519)
        ).then((account_root_key_crypto_context) => {
          let ed25519keyPair = Ed25519KeyGenerator.regenerate_key_pair(
            account_root_key_crypto_context
          );

          let secretObj = {};
          secretObj[SelfAccountKeyring.CURVE25519] = curve25519keyPair.secretKey;
          secretObj[SelfAccountKeyring.ED25519] = ed25519keyPair.secretKey;

          let publicObj = {};
          publicObj[SelfAccountKeyring.CURVE25519] = curve25519keyPair.publicKey;
          publicObj[SelfAccountKeyring.ED25519] = ed25519keyPair.publicKey;

          self_kr.update(publicObj, secretObj);

          return self_kr;
        });
      });
    });
  }

  /**
     * Only returns the secret keys in encrypted form, even if public keys are loaded.
        NOTE: When changing a password only the 'master' key(s) must be saved/updated.
        NOTE: When creating the account, the public keys need to be signed with ACCOUNT_SIGNATURE_CRYPTO_CONTEXT!
     */
  public dump(private_kr?: AbstractPrivateKeyring): Promise<Object> {
    let res = {};

    return ACCOUNT_ROOT_KEY_CRYPTO_CONTEXT.dump(
      new AccountRootKeyConfigDumpArgs(this, SelfAccountKeyring.CURVE25519)
    ).then((enc) => {
      return ACCOUNT_ROOT_KEY_CRYPTO_CONTEXT.dump(
        new AccountRootKeyConfigDumpArgs(this, SelfAccountKeyring.ED25519)
      ).then((sign) => {
        res[SelfAccountKeyring.CURVE25519] = enc;
        res[SelfAccountKeyring.ED25519] = sign;

        if (!private_kr) {
          return res;
        } else {
          return PRIVATE_KEY_CRYPTO_CONTEXT.dump(
            new PrivateKeyConfigDumpArgs(this, private_kr, SelfAccountKeyring.MASTER)
          ).then((master) => {
            res[SelfAccountKeyring.MASTER] = master;
            return res;
          });
        }
      });
    });
  }

  /**
   * The master-key (any version) must be created in a preparation step at registration.
   */
  public static generate_master_key(version?: string): Uint8Array {
    if (!version || version == SelfAccountKeyring.MASTER) {
      return RandomKeyGenerator256.new_key();
    } else {
      throw 'unknown key';
    }
  }

  /**
     * At registration this is done at the challenge step, when the encrypted master key is returned
        from the server. The master-key parameter is optional to support other (mostly development) uses.
     */
  public static generate(account_id: string, master_key?: Uint8Array): SelfAccountKeyring {
    if (!master_key) {
      master_key = this.generate_master_key(SelfAccountKeyring.MASTER);
    } else if (master_key.byteLength != 32) {
      throw 'Invalid master key for current version';
    }

    let curve25519keyPair = Curve25519KeyGenerator.new_key();
    let ed25519keyPair = Ed25519KeyGenerator.new_key();

    let publicKeys = {};
    let secretKeys = {};

    publicKeys[SelfAccountKeyring.CURVE25519] = curve25519keyPair.publicKey;
    publicKeys[SelfAccountKeyring.ED25519] = ed25519keyPair.publicKey;

    secretKeys[SelfAccountKeyring.CURVE25519] = curve25519keyPair.secretKey;
    secretKeys[SelfAccountKeyring.ED25519] = ed25519keyPair.secretKey;
    secretKeys[SelfAccountKeyring.MASTER] = master_key;

    return new SelfAccountKeyring(account_id, publicKeys, secretKeys);
  }

  /**
   * Creating an anonymous keyring consists only of confidentiality public-key cryptographic keys.
   */
  public static generate_anonymous(): SelfAccountKeyring {
    let keys = Curve25519KeyGenerator.new_key();
    let secretObj = {};
    secretObj[SelfAccountKeyring.CURVE25519] = keys.secretKey;

    let publicObj = {};
    publicObj[SelfAccountKeyring.CURVE25519] = keys.publicKey;

    return new SelfAccountKeyring(SelfAccountKeyring.ANONYMOUS_ID, publicObj, secretObj);
  }

  /**
     * Serialize and encrypt the keyring for local storage.
        NOTE: The key should be derived from a locally-stored and a server-stored input at least.
              For extra security the user could also provide some password for the protection of their session data.
              This is only beneficial when an attacker acquires their device while the app is not loaded.
     */
  public dump_local_storage(
    server_key: Uint8Array,
    client_key: Uint8Array,
    salt: Uint8Array
  ): Promise<Uint8Array> {
    let secretObj = {};
    secretObj[SelfAccountKeyring.CURVE25519] = this._secret_keys[SelfAccountKeyring.CURVE25519];
    secretObj[SelfAccountKeyring.ED25519] = new Uint8Array(
      this._secret_keys[SelfAccountKeyring.ED25519]
    );
    secretObj[SelfAccountKeyring.MASTER] = this._secret_keys[SelfAccountKeyring.MASTER];

    return LOCAL_STORAGE_CRYPTO_CONTEXT.dump(
      new LocalStorageConfigDumpArgs(
        msgpack.encode({
          id: new TextDecoder().decode(this.id),
          secret_keys: secretObj,
        }),
        server_key,
        client_key,
        salt
      )
    );
  }

  /**
   * Decrypt and load the keyring from local storage.
   */
  public static load_local_storage(
    cipher: Uint8Array,
    server_key: Uint8Array,
    client_key: Uint8Array,
    salt: Uint8Array
  ): Promise<SelfAccountKeyring> {
    if (!salt) {
      salt = new Uint8Array([]);
    }
    return LOCAL_STORAGE_CRYPTO_CONTEXT.load(
      new LocalStorageConfigLoadArgs(cipher, server_key, client_key, salt)
    ).then((data) => {
      data = msgpack.decode(data);

      let curveKeys = Curve25519KeyGenerator.regenerate_key_pair(
        data['secret_keys'][SelfAccountKeyring.CURVE25519]
      );
      let edKeys = Ed25519KeyGenerator.regenerate_key_pair(
        data['secret_keys'][SelfAccountKeyring.ED25519]
      );

      let publicObj = {};
      publicObj[SelfAccountKeyring.CURVE25519] = curveKeys.publicKey;
      publicObj[SelfAccountKeyring.ED25519] = edKeys.publicKey;

      let secretObj = {};
      secretObj[SelfAccountKeyring.CURVE25519] = curveKeys.secretKey;
      secretObj[SelfAccountKeyring.ED25519] = edKeys.secretKey;
      secretObj[SelfAccountKeyring.MASTER] = data['secret_keys'][SelfAccountKeyring.MASTER];

      return new SelfAccountKeyring(data['id'], publicObj, secretObj);
    });
  }
}
