import { AbstractKeyring } from './base';

export const MASTER_KEY_LENGTH = 32;
export const AUTHENTICATION_KEY_LENGTH = 16;
export const PRIVATE_KR_OUTPUT_LENGTH = MASTER_KEY_LENGTH + AUTHENTICATION_KEY_LENGTH;

// We're using the unicode-space regexp definition from Python 3.9.
const UNICODE_SPACE_RE = new RegExp(
  '[\u0009\u000A\u000B\u000C\u000D\u001C\u001D\u001E\u001F\u0020\u0085\u00A0\u1680\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200A\u2028\u2029\u202F\u205F\u3000]',
  'gu'
);

/**
 * A user's private keys that are used for authenticating and encrypting the account's master key.
    The upgrade procedure and versioning for these is a little different. This keyring does not hold different
    versions of keys, but is itself a set of keys for a specific version. This implies that all account-private keys
    are always encrypted with the same private keyring, as well as the account-login authentication.
    The login procedure shall iterate through all of the private keyring implementations from most to least preferred.
    If not the most preferred keyring is successful at logging in, then (if upgrade graceful wait is done) update the
    stored keys to use the preferred keyring.
    NOTE: Dispose all of these keyring instances after use directly! Never store or transmit them!
 */
export abstract class AbstractPrivateKeyring extends AbstractKeyring {
  public auth_key;
  public enc_key;

  public static VERSION: string;

  //public static abstract load: Function;

  constructor(encryption_key, authentication_key) {
    super();
    this.auth_key = authentication_key;
    this.enc_key = encryption_key;
  }

  public static load: Function;

  public static normalize_password = (password: string): string => {
    /**
        A not enforcing implementation of OpaqueString normalization from RFC8265.
        The normalization described is simple to execute: replace unicode space to ASCII space and normalize to NFC
        then repeat to check idempotence of operations.
        The additional validation rules though... they are very convoluted to express and implement:
        The OpaqueString builds on the FreeformClass from RFC8264. Some libraries we found, include up to 160KB of
        unicode mapping data to express the ruleset. This size does not seem easy to be justified for avoiding
        ambiguous edge cases, that the validation is supposed to protect users from. Implementing or validating the
        ruleset is not trivial, and an erroneous implementation may cause more harm than good. The available libraries
        are lacking in support and code quality by testing at the time of writing this. The validation is only supposed
        to protect users who create passwords out of niche expressions of unicode codepoints. We could argue in favor
        of preventing or allowing those cases to be made, but we opt to not do validation enforcement for now.
        NOTE: Since password derivation is done in the client exclusively, in theory any binary data string could be
            used for the input because there is no way for the server to enforce any validation.
        */
    password = password.replace(UNICODE_SPACE_RE, ' ').normalize('NFC');
    if (password !== password.replace(UNICODE_SPACE_RE, ' ').normalize('NFC')) {
      // Ancient Unicode implementations had some exceptions to idempotency, but this should not happen:
      // http://www.unicode.org/faq/normalization.html#14
      throw 'password input is not idempotent';
    }
    return password;
  };

  public static password_forms = (password: string): Array<string> => {
    /**
        Default implementation is to have the normalized form only by the current implementation.
        Return value may have multiple items for backwards compatibility. Most preferred value shall be the first one.
        */
    return [AbstractPrivateKeyring.normalize_password(password)];
  };

  public static flattened_password_forms_with_cls = (
    cls_list: Array<typeof AbstractPrivateKeyring>,
    password: string
  ): Array<[typeof AbstractPrivateKeyring, string]> => {
    /**
        Flat list of the optimized password forms with the loading keyring class.
        This can be used to simply try each relevant configuration for login.
        */
    const r = [];
    for (const cls of cls_list) {
      for (const pw of cls.password_forms(password)) {
        r.push([cls, pw]);
      }
    }
    return r;
  };
}

/**
 * PBKDF2 is not bad, but a little old. This has good performance support on all platforms.
 */
export class PrivateKeyringV1 extends AbstractPrivateKeyring {
  public static VERSION: string = '1';

  public static load = (password: Uint8Array, salt: Uint8Array): Promise<PrivateKeyringV1> => {
    return new Promise((resolve, reject) => {
      // TODO update typescript and remove this. The older version return with PromiseLike
      crypto.subtle
        .importKey(
          // pbkdf2 key containing the pw
          'raw',
          password,
          { name: 'PBKDF2' },
          false,
          ['deriveKey']
        )
        .then((baseKey) => {
          crypto.subtle
            .deriveKey(
              // derive key from the pw
              {
                name: 'PBKDF2',
                salt,
                iterations: 150000,
                hash: 'SHA-256',
              },
              baseKey,
              {
                name: 'HMAC',
                hash: 'SHA-256',
                length: 384,
              },
              true,
              ['sign']
            )
            .then((aesKey) => {
              // export it in order to display it
              crypto.subtle.exportKey('raw', aesKey).then((keyBytes) => {
                resolve(
                  new PrivateKeyringV1(
                    new Uint8Array(keyBytes.slice(0, 32)),
                    new Uint8Array(keyBytes.slice(32))
                  )
                );
              }, reject);
            }, reject);
        }, reject);
    });
  };

  public static password_forms = (password: string): Array<string> => {
    /**
        Optimize authentication attempts by omitting duplicate password forms.
        */
    let norm_pwd;
    try {
      norm_pwd = AbstractPrivateKeyring.normalize_password(password);
    } catch {
      // NOTE: We don't expect normalize to fail for any password.
      return [password];
    }
    if (norm_pwd === password) {
      return [norm_pwd];
    }
    return [norm_pwd, password];
  };
}

/**
 * Argon2 is the best-in-class solution for password KDF.
   Unfortunately this is not supported universally on all platforms, or requires extension like WASM to run.
   NOTE: The password normalization was added to this class as an update. This was possible because until that point,
   it was not utilized in production and no authentication may be broken by the change.
 */
export class PrivateKeyringV2 extends AbstractPrivateKeyring {
  public static VERSION: string = '2';

  public static load(password: Uint8Array, salt: Uint8Array) {
    // TODONOW
    return Promise.reject('not implemented');
  }
}

// NOTE: For login, use AbstractPrivateKeyring.flattened_password_forms_with_cls(PRIVATE_KEYRING_CLASSES, <password>) to
// iterate through the valid authentication keyring configurations.
// from best to least preferred
export const PRIVATE_KEYRING_CLASSES = [PrivateKeyringV2, PrivateKeyringV1];
export let PRIVATE_KEYRING_VERSION_MAP = [];
PRIVATE_KEYRING_VERSION_MAP[PrivateKeyringV2.VERSION] = PrivateKeyringV2;
PRIVATE_KEYRING_VERSION_MAP[PrivateKeyringV1.VERSION] = PrivateKeyringV1;
