import { SelfAccountKeyring } from '../keyring/account_self';
import { AbstractPrivateKeyring } from '../keyring/private';
import {
  AuthValueError,
  auth_key_hash_function,
  CLIENT_SALT_INPUT_LENGTH,
  generate_client_salt_input,
  server_salt,
} from './common';
import { create_account_login_private_keyring } from './login';
import { MASTER_KEY_LENGTH } from '../keyring/private';
import { PRIVATE_KEY_CRYPTO_CONTEXT } from '../context/private_key/__init__';
import { PrivateKeyConfigDumpArgs, PrivateKeyConfigLoadArgs } from '../context/private_key/args';
import { base64 } from 'rfc4648';
import { ACCOUNT_SIGNATURE_CRYPTO_CONTEXT } from '../context/account_signature/__init__';
import { AccountSignatureConfigDumpArgs } from '../context/account_signature/args';
import { Ed25519DS } from '../base/ds/ec';
import { concatByteArray } from '../utility';
import { PeerAccountKeyring } from '../keyring/account_peer';
import { REGISTER_CHALLENGE_CRYPTO_CONTEXT } from '../context/register_challenge/__init__';
import { RegisterChallengeConfigLoadArgs } from '../context/register_challenge/args';

/**
 * The first step for the client is to initiate the registration. They generate and encrypt their master key
    by their private keyring. It derives the encryption key and an authentication key from their secret password
    and client-salt.
    They will receive this after proving ownership of the associated email and the knowledge of the authentication
    key in the following step.

    NOTE: The master_key and client_salt_input are only passable for testing and using the password change procedure.
        They must be generated newly when registering a new account. The client_salt_input must be newly generated
        for an attempt at password change as well.
    NOTE: The enforce_normalized_password is for testing.
 */
export function account_register_prepare(
  private_kr_cls: typeof AbstractPrivateKeyring,
  password: string,
  master_key?: Uint8Array,
  client_salt_input?: Uint8Array,
  enforce_normalized_password?: boolean
): Promise<[Uint8Array, Uint8Array, Uint8Array]> {
  // Absolute minimum rules of a valid password: not too short and not too simple.
  // These can only be enforced by the client, for the benefit of the user.

  if (!client_salt_input) {
    client_salt_input = generate_client_salt_input();
  }
  if (
    !(client_salt_input instanceof Uint8Array) ||
    client_salt_input.byteLength !== CLIENT_SALT_INPUT_LENGTH
  ) {
    return Promise.reject(new AuthValueError('Client-salt-input is invalid')); // need to return with an error inside a promise, or you cant catch it
  }

  // For register, changing the password or recovery enforce that the used password is normalized.
  if (
    enforce_normalized_password !== false &&
    password !== private_kr_cls.normalize_password(password)
  ) {
    return Promise.reject(new AuthValueError('Password is not normalized')); // need to return with an error inside a promise, or you cant catch it
  }

  // In addition to the reference, encoded password is used not only for Secure Remote Password Protocol but also as
  // the cipher key for encoding client secrets which encrypt user data. We use KDFs that are designed for low entropy
  // inputs, so their output is viable for encrypting additional secrets to be stored on the server which cannot be
  // decrypted without knowing the password.
  // A diversion from the reference is done for user convenience and the possibility of seamless account-recovery,
  // which is that the email is not part of the hashing. If it was, the account's email would not be change-able by
  // support when a user loses access to it. (or rather the private-keyring values would break)
  return server_salt(client_salt_input).then((_server_salt) => {
    return create_account_login_private_keyring(private_kr_cls, password, _server_salt).then(
      (private_kr) => {
        if (!master_key) {
          master_key = SelfAccountKeyring.generate_master_key(SelfAccountKeyring.MASTER);
        }

        if (!(master_key instanceof Uint8Array) || master_key.byteLength != MASTER_KEY_LENGTH) {
          throw new AuthValueError('Master-key is invalid');
        }

        let obj = {};
        obj[SelfAccountKeyring.MASTER] = master_key;

        return PRIVATE_KEY_CRYPTO_CONTEXT.dump(
          new PrivateKeyConfigDumpArgs(
            new SelfAccountKeyring('N/A', {}, obj),
            private_kr,
            SelfAccountKeyring.MASTER
          )
        ).then((encrypted_master_key) => {
          return PRIVATE_KEY_CRYPTO_CONTEXT.load(
            new PrivateKeyConfigLoadArgs(
              encrypted_master_key,
              private_kr,
              SelfAccountKeyring.MASTER
            )
          ).then((_private_key_crypto_context) => {
            if (base64.stringify(master_key) != base64.stringify(_private_key_crypto_context)) {
              throw new AuthValueError('Master-key encryption is not consistent');
            }
            return auth_key_hash_function(private_kr.auth_key).then((hashed_authentication_key) => {
              return [client_salt_input, encrypted_master_key, hashed_authentication_key];
            });
          });
        });
      }
    );
  });
}

/**
 * This is the base payload to be submitted to the server for starting the registration process.

    These may be submitted to the "register-prepare" endpoint with the "invite_token" additional argument.
 */
export function create_account_register_prepare_request(
  email: string,
  client_salt_input: Uint8Array,
  encrypted_master_key: Uint8Array,
  hashed_authentication_key: Uint8Array
) {
  return {
    email,
    client_salt_input: base64.stringify(client_salt_input),
    encrypted_master_key: base64.stringify(encrypted_master_key),
    hashed_authentication_key: base64.stringify(hashed_authentication_key),
  };
}

/**
 * The client shall generate a new keyring from the supplied secrets.
    This step is after the user has verified the email and performed a login-challenge against the register-data.
    The private keyring that is created for the login-challenge must be used here as well. After this step it is
    no longer needed ans must be deleted.
 */
export function account_register_keyring(
  private_kr: AbstractPrivateKeyring,
  account_id: string,
  encrypted_master_key: Uint8Array
): Promise<SelfAccountKeyring> {
  return PRIVATE_KEY_CRYPTO_CONTEXT.load(
    new PrivateKeyConfigLoadArgs(encrypted_master_key, private_kr, SelfAccountKeyring.MASTER)
  ).then((master_key) => {
    return SelfAccountKeyring.generate(account_id, master_key);
  });
}

/**
 * Prepare the new keyring for storage:
        - encrypt the secret keys
        - sign the public keys to create a trust root key

    These may be submitted to the "register-challenge" endpoint with the "register_token"
    and "authentication_key" additional arguments.
 */
export type create_account_register_keyring_request_result = {
  email: string;
  encrypting_secret_ec: string;
  encrypting_public_ec: string;
  signing_secret_ec: string;
  signing_public_ec: string;
  encrypting_public_ec_signature: string;
};
export function create_account_register_keyring_request(
  email: string,
  self_kr: SelfAccountKeyring
): Promise<create_account_register_keyring_request_result> {
  return self_kr.dump().then((encrypted_secret_keys) => {
    // sign the public encryption keys for authenticity
    return ACCOUNT_SIGNATURE_CRYPTO_CONTEXT.dump(
      new AccountSignatureConfigDumpArgs(
        self_kr.get_public_key(SelfAccountKeyring.CURVE25519),
        self_kr
      )
    ).then((enc_pub_ec_sig) => {
      return {
        email,
        encrypting_secret_ec: base64.stringify(
          encrypted_secret_keys[SelfAccountKeyring.CURVE25519]
        ),
        encrypting_public_ec: base64.stringify(
          self_kr.get_public_key(SelfAccountKeyring.CURVE25519)
        ),
        signing_secret_ec: base64.stringify(encrypted_secret_keys[SelfAccountKeyring.ED25519]),
        signing_public_ec: base64.stringify(self_kr.get_public_key(SelfAccountKeyring.ED25519)),
        encrypting_public_ec_signature: base64.stringify(enc_pub_ec_sig),
      };
    });
  });
}

/**
 * The confirmation of the register procedure requires a proper challenge value signed by the account.

    These may be submitted to the "register-confirm" endpoint.
 */
export type create_account_register_confirm_request_result = {
  challenge_data: string;
  challenge_signature: string;
};

export function create_account_register_confirm_request(
  self_kr: SelfAccountKeyring,
  encrypted_challenge: Uint8Array,
  challenge_25519_pk: Uint8Array = undefined, // for legacy api compatibility
  challenge_pks: Object = undefined // prefer using this to challenge_25519_pk
): Promise<create_account_register_confirm_request_result> {
  let peer_kr;
  if (challenge_pks) {
    peer_kr = PeerAccountKeyring.load_anonymous_keys(challenge_pks);
  } else {
    peer_kr = PeerAccountKeyring.load_anonymous(challenge_25519_pk);
  }
  return REGISTER_CHALLENGE_CRYPTO_CONTEXT.load(
    new RegisterChallengeConfigLoadArgs(encrypted_challenge, self_kr, peer_kr)
  ).then((challenge) => {
    return Ed25519DS.sign(
      challenge,
      concatByteArray(
        self_kr.get_secret_key(SelfAccountKeyring.ED25519),
        self_kr.get_public_key(SelfAccountKeyring.ED25519)
      )
    ).then((challenge_signature) => {
      return {
        challenge_data: new TextDecoder().decode(challenge), // base64 encoded already (hmac signed data-token)
        challenge_signature: base64.stringify(challenge_signature),
      };
    });
  });
}
