import { AbstractAccountKeyring, AbstractSelfAccountKeyring } from '../../keyring/account_base';
import { AbstractCryptoConfig } from '../base';
import { AccountMasterDeriveKeyV1 } from '../common';
import { AccountTrustConfigDumpArgs, AccountTrustConfigLoadArgs } from './args';
import { makeInfoByteArray, ValueError } from '../../utility';

import { base64 } from 'rfc4648';
import { MissingKey } from '../../keyring/base';

/**
 * Base class for trust-related errors.
    When this is being handled instead of a more specific exception, the trust must be treated as compromised!
 */
export class TrustError extends Error {}

/**
 * Some error happened, the peer must not be trusted!
 */
export class TrustCompromised extends TrustError {}

/**
 * Trust is not of preferred key, but still fine.
 */
export class TrustIsStale extends TrustError {}

/**
 * Raised when the current trust fingerprint is valid, but there is a newer trust root which should be stored as
    a fingerprint. The system can automatically upgrade the trust if the new trust root is signed with the old one
    and not only the other way around.
    NOTE: Even if auto-upgrade is possible after a while (when the old trust root is sufficiently obsolete) it will
    not be performed anymore and manual confirmation will be necessary.
 */
export class TrustNeedsUpgrade extends TrustError {}

/**
 * The trust of the account builds on chained signatures of root trusts (by upgrades) and signatures of other keys.
 */
export abstract class AbstractAccountTrustConfig extends AbstractCryptoConfig<
  AccountTrustConfigLoadArgs,
  AccountTrustConfigDumpArgs
> {
  public load(args: AccountTrustConfigLoadArgs): Promise<Uint8Array> {
    try {
      return this.derive_key(args.self_kr).then((derive_key) => {
        return this._decrypt(args.cipher, derive_key).then((fingerprint) => {
          let could_upgrade = false;

          let checkFingerprint = (index) => {
            if (index < AbstractAccountKeyring.TRUST_KEY_VERSIONS.length) {
              let key_version = AbstractAccountKeyring.TRUST_KEY_VERSIONS[index];

              let trust_root;
              try {
                trust_root = args.peer_kr.get_public_key(key_version);
              } catch (e) {
                // TODO MissingKey check
                if (e instanceof MissingKey) {
                  return checkFingerprint(index + 1);
                } else {
                  throw e;
                }
              }

              // NOTE: The naive comparison is not a problem here, because the authenticated decryption protects
              // us from guessing&timing attacks.
              return this._get_fingerprint(args.peer_kr.id, trust_root, args.self_kr).then((getFingerprint) => {
                if (base64.stringify(getFingerprint) == base64.stringify(fingerprint)) {
                  if (key_version == AbstractAccountKeyring.TRUST_KEY_VERSIONS[0]) {
                    return trust_root; // RETURN
                  }
                  if (could_upgrade) {
                    throw new TrustNeedsUpgrade();
                  }
                  throw new TrustIsStale();
                }

                could_upgrade = true;

                return checkFingerprint(index + 1);
              });
            } else {
              throw new TrustCompromised();
            }
          };

          // Check the fingerprint from highest to lowest trust root.
          return checkFingerprint(0);
        });
      });
    } catch (e) {
      if (e instanceof ValueError) {
        throw new TrustCompromised();
      }
      throw e;
    }
  }

  public dump(args: AccountTrustConfigDumpArgs): Promise<Uint8Array> {
    if (!AbstractAccountKeyring.TRUST_KEY_VERSIONS.includes(args.key_version)) {
      throw 'Invalid key-version for trust fingerprint';
    }
    return this.derive_key(args.self_kr).then((derive_key) => {
      return this._get_fingerprint(
        args.peer_kr.id,
        args.peer_kr.get_public_key(args.key_version),
        args.self_kr
      ).then((fingerprint) => {
        return this._encrypt(fingerprint, derive_key);
      });
    });
  }

  public derive_key(self_kr: AbstractSelfAccountKeyring): Promise<Uint8Array> {
    return AccountMasterDeriveKeyV1.derive_key(
      self_kr,
      makeInfoByteArray(['account-trust-', this.version])
    );
  }

  protected abstract _decrypt(cipher: Uint8Array, key: Uint8Array): Promise<Uint8Array>;

  protected abstract _encrypt(plain: Uint8Array, key: Uint8Array): Promise<Uint8Array>;

  /**
     * The fingerprint for a peer account is made from one of their trust roots and their account id. Including
        the peer account's id is necessary, because the Nano's permission blockchain references the peers by their
        id, so they must be linked to enforce consistent identity verification.
        The self keyring is provided here to fingerprint the peer-root-key in an exclusive and secret way. It
        normally would not be an issue because of the encryption of the fingerprint, but this will completely
        prevent a malicious server from doing chosen-ciphertext attacks against the trust-store's encryption.

        NOTE: The initial version of this did not incorporate the peer's id. The rationale of that was that even
        if a malicious server orchestrates a fake account masquerading with the same keys as a trusted one it
        cannot gain any information or access to data unless it knows the secret keys. Which is not incorrect,
        but it does not cover the case when there is a colluding malicious peer that has been trusted. Since the
        Nano's permission blockchain references the peers by their id, that identity representation must be linked
        with the cryptographic one to protect from identity forgery.
     */
  protected abstract _get_fingerprint(
    peer_id: Uint8Array,
    peer_root_key: Uint8Array,
    self_kr: AbstractSelfAccountKeyring
  ): Promise<Uint8Array>;
}
