import {
  AbstractAccountKeyring,
  AbstractPeerAccountKeyring,
  AbstractSelfAccountKeyring,
} from './account_base';
import { ACCOUNT_SIGNATURE_CRYPTO_CONTEXT } from '../context/account_signature/__init__';
import { AccountSignatureConfigLoadArgs } from '../context/account_signature/args';
import { ACCOUNT_TRUST_CRYPTO_CONTEXT } from '../context/account_trust/__init__';
import { AccountTrustConfigLoadArgs } from '../context/account_trust/args';
import { TrustIsStale, TrustNeedsUpgrade, TrustError } from '../context/account_trust/common';
import { UnknownCryptoConfigVersion } from '../context/base';
import { base64 } from 'rfc4648';

/**
 * A peer account's cryptographic primitives are their public keys for encryption and signature verification.
 */
export class PeerAccountKeyring extends AbstractPeerAccountKeyring {
  /**
     * Loading a peer account's keyring requires checking the signatures of all keys by the trust root and
        verifying the trust-fingerprint of the root if present.
        After new keys and signing roots have been introduced the old ones can become optional.
        The caller and all codes must check the trusted state before using the keyring.

     */
  public static async load(
    account_id: string,
    curve25519_public: Uint8Array,
    curve25519_public_sig: Uint8Array,
    ed25519_public: Uint8Array,
    trust_fingerprint?: Uint8Array,
    self_kr?: AbstractSelfAccountKeyring,
    stored_tofu_fingerprints?: Object
  ): Promise<PeerAccountKeyring> {
    let obj = {};
    obj[PeerAccountKeyring.CURVE25519] = curve25519_public;
    obj[PeerAccountKeyring.ED25519] = ed25519_public;

    let peer_kr = new PeerAccountKeyring(account_id, obj);

    // Signatures of all present keys are required regardless of trust.
    await ACCOUNT_SIGNATURE_CRYPTO_CONTEXT.load(
      new AccountSignatureConfigLoadArgs(curve25519_public_sig, peer_kr, curve25519_public)
    )
    // Check the fingerprint of the root signing key.
    if (trust_fingerprint) {
      // If there is an explicit identity-trust fingerprint, the TOFU fingerprints are irrelevant.
      if (!self_kr) {
        throw 'Self keyring is required for checking trust fingerprint';
      }
      await peer_kr.update(trust_fingerprint, self_kr);

      return peer_kr;
    } else {
      // Generate the TOFU fingerprints for validation and storage.

      await peer_kr.create_tofu_fingerprints();
      if (stored_tofu_fingerprints) {
        for (let key_version in PeerAccountKeyring.TRUST_KEY_VERSIONS) {
          if (
            !stored_tofu_fingerprints.hasOwnProperty(key_version) ||
            !peer_kr.tofu_fingerprints.includes(key_version)
          ) {
            continue; // we can only check these if both exist
          }
          // NOTE: Naive comparison should not pose much threat here. Loading a keyring is initiated by the
          // client and and attacker cannot measure when it "fails". (especially because it is cached)
          if (
            base64.stringify(stored_tofu_fingerprints[key_version]) !=
            base64.stringify(peer_kr.tofu_fingerprints[key_version])
          ) {
            peer_kr.trusted = PeerAccountKeyring.TRUSTED_NOT;
            break;
          }
        }
      }

      return peer_kr;
    }
  }

  // Updating the trust state after loading shall only be done when a user explicitly verifies a peer.
  public async update(trust_fingerprint: Uint8Array, self_kr: AbstractSelfAccountKeyring) {
    try {
      await ACCOUNT_TRUST_CRYPTO_CONTEXT.load(
        new AccountTrustConfigLoadArgs(trust_fingerprint, self_kr, this)
      );

      this.trusted = AbstractAccountKeyring.TRUSTED_FULLY;
    } catch (e) {
      if (e instanceof TrustIsStale) {
        this.trusted = AbstractAccountKeyring.TRUSTED_STALE;
      } else if (e instanceof TrustNeedsUpgrade) {
        // The upgrade may need to be done manually if the fingerprint is for an ancient trust root.
        this.trusted = AbstractAccountKeyring.TRUSTED_REQUIRES_UPGRADE;
      } else /*if (e instanceof TrustError || e instanceof UnknownCryptoConfigVersion)*/ {
        // When the verification fails in an "expected" way the keyring can be constructed and examined
        // to see what the problem is, but must not be used!
        this.trusted = AbstractAccountKeyring.TRUSTED_NOT;
      }
    }
  }

  /**
   * For legacy api compatibility. Prefer .load_anonymous_keys()
   */
  public static load_anonymous(curve25519_public: Uint8Array) {
    return this.load_anonymous_keys({ [PeerAccountKeyring.CURVE25519]: curve25519_public });
  }

  /**
     * Loading an anonymous peer account's keyring has no checks for authenticity because the peer is unknown.
        The caller code must keep in mind that a keyring is for an anonymous peer and work accordingly.
     */
  public static load_anonymous_keys(public_keys: Object) {
    const anon_keys = {};
    // Only load keys that are known to be for ECDH purpose. Trust-keys are not usable for anonymous and
    // newer (not yet implemented) or obsolete ECDH keys should not be used.
    for (const [key_version, key] of Object.entries(public_keys)) {
      if (AbstractAccountKeyring.ECDH_KEY_VERSIONS.indexOf(key_version) === -1) {
        continue;
      }
      anon_keys[key_version] = key;
    }
    return new PeerAccountKeyring(AbstractAccountKeyring.ANONYMOUS_ID, anon_keys);
  }
}
