import { concatByteArray } from '../utility';
import { AbstractKeyring, MissingKey } from './base';
import { base64 } from 'rfc4648';

/**
 * The account keyring has a trust state that must be loaded, validated checked before using them.
 * It declares the key version that are used for public-key cryptography.
 */
export abstract class AbstractAccountKeyring extends AbstractKeyring {
  public static TRUSTED_NOT = 0; // verification failed, keyring *is* compromised
  public static TRUSTED_UNKNOWN = 1; // not trusted
  public static TRUSTED_FULLY = 2; // trusted and fingerprint is of the most preferred key
  public static TRUSTED_STALE = 3; // trusted, but fingerprint is of an older key (no newer is available for the account)
  public static TRUSTED_REQUIRES_UPGRADE = 4; // trusted, but fingerprint should be upgraded

  public id;
  public _public_keys;
  public trusted;

  // safe_radix32.encode_fw(0)
  public static ANONYMOUS_ID = '2222222222222';

  // This is reserved for development of the trust-version handling of the crypto-configs.
  public static DEV_RESERVED = 'x';

  // real key versions
  public static CURVE25519 = '1';
  public static ED25519 = '2';

  // in order of preference (from best to oldest)
  public static TRUST_KEY_VERSIONS = [AbstractAccountKeyring.ED25519];

  public static VALID_PUBLIC_KEY_LENGTH_MAP = {
    [AbstractAccountKeyring.CURVE25519]: 32,
    [AbstractAccountKeyring.ED25519]: 32,
  };
  public static ECDH_KEY_VERSIONS = (() => {
    const tmp = [];
    for (let k in AbstractAccountKeyring.VALID_PUBLIC_KEY_LENGTH_MAP) {
      if (AbstractAccountKeyring.TRUST_KEY_VERSIONS.indexOf(k) === -1) {
        tmp.push(k);
      }
    }
    tmp.sort(function (a, b) {
      return a.attr < b.attr ? 1 : -1;
    });
    return tmp;
  })();

  //public static abstract load: Function

  constructor(
    account_id: string,
    public_keys: Object,
    trusted: number = AbstractAccountKeyring.TRUSTED_UNKNOWN
  ) {
    super();
    this.id = new TextEncoder().encode(account_id);
    AbstractAccountKeyring.check_key_lengths(
      public_keys,
      AbstractAccountKeyring.VALID_PUBLIC_KEY_LENGTH_MAP
    );
    this._public_keys = public_keys;
    this.trusted = trusted;
  }

  public get_public_key(version: string): Uint8Array {
    let key = this._public_keys[version];
    if (key) return key;
    else {
      throw new MissingKey(version + '');
    }
  }

  /**
   * The actual order is not relevant, the goal is for it to be deterministic.
   */
  public ordered_ids(other: AbstractAccountKeyring): Uint8Array {
    if (new TextDecoder().decode(this.id) < new TextDecoder().decode(other.id)) {
      return concatByteArray(this.id, other.id);
    }
    return concatByteArray(other.id, this.id);
  }
}

/**
 * Add TOFU (trust-on-first-use) identity generation and storage to the peer keyring.
 */
export abstract class AbstractPeerAccountKeyring extends AbstractAccountKeyring {
  public tofu_fingerprints;

  constructor(
    account_id: string,
    public_keys: Object,
    trusted: number = AbstractAccountKeyring.TRUSTED_UNKNOWN
  ) {
    super(account_id, public_keys, trusted);
    this.tofu_fingerprints = null;
  }

  /**
     * Get the simple fingerprints of the trust keys of the keyring. These are stored on the client side
        and compared automatically when re-loading the same keyring for consistency.
     */
  public create_tofu_fingerprints(): Promise<Uint8Array> {
    this.tofu_fingerprints = {};

    let _create = (index) => {
      let key_version = AbstractAccountKeyring.TRUST_KEY_VERSIONS[index];
      let key;

      try {
        key = this.get_public_key(key_version);
      } catch (e) {
        if (e instanceof MissingKey) {
          return _create(index + 1);
        }
      }

      return new Promise((resolve, reject) => {
        crypto.subtle
          .digest('SHA-256', key)
          .then((digest) => {
            this.tofu_fingerprints[key_version] = new Uint8Array(digest.slice(0, 16));
            resolve(this.tofu_fingerprints[key_version]); // TODO upgrade typescript, now digest return with PromiseLike
          })
          .catch(reject);
      });
    };

    return _create(0);
  }
}

/**
 * The self account can have the same public cryptographic items as a peer and their secret counterparts.
    Additionally secret symmetric keys could be here. The an account's own keyring is always trusted (see notes
    at the implementation of load()).
 */
export abstract class AbstractSelfAccountKeyring extends AbstractAccountKeyring {
  public _secret_keys;

  public static MASTER = '3';

  public static VALID_SECRET_KEY_LENGTH_MAP = Object.assign(
    {},
    AbstractAccountKeyring.VALID_PUBLIC_KEY_LENGTH_MAP,
    { [AbstractSelfAccountKeyring.MASTER]: 32 }
  );

  //public static abstract load: Function;

  constructor(account_id: string, public_keys: Object, secret_keys: Object) {
    super(account_id, public_keys, AbstractSelfAccountKeyring.TRUSTED_FULLY);
    AbstractSelfAccountKeyring.check_key_lengths(
      secret_keys,
      AbstractSelfAccountKeyring.VALID_SECRET_KEY_LENGTH_MAP
    );
    this._secret_keys = secret_keys;
  }

  public static load: Function;

  public update(public_keys?: Object, secret_keys?: Object) {
    if (public_keys) {
      AbstractSelfAccountKeyring.check_key_lengths(
        public_keys,
        AbstractSelfAccountKeyring.VALID_PUBLIC_KEY_LENGTH_MAP
      );
      this._public_keys = Object.assign(this._public_keys, public_keys);
    }
    if (secret_keys) {
      AbstractSelfAccountKeyring.check_key_lengths(
        secret_keys,
        AbstractSelfAccountKeyring.VALID_SECRET_KEY_LENGTH_MAP
      );
      this._secret_keys = Object.assign(this._secret_keys, secret_keys);
    }
  }

  public get_secret_key(version: string): Uint8Array {
    let key = this._secret_keys[version];
    if (key) {
      return key;
    } else {
      throw new MissingKey(version + '');
    }
  }
}
