import { Observable } from "rxjs";

import { ProfileOrganizationResponse } from "../../admin-console/models/response/profile-organization.response";
import { ProfileProviderOrganizationResponse } from "../../admin-console/models/response/profile-provider-organization.response";
import { ProfileProviderResponse } from "../../admin-console/models/response/profile-provider.response";
import { KdfConfig } from "../../auth/models/domain/kdf-config";
import { OrganizationId, ProviderId, UserId } from "../../types/guid";
import { UserKey, MasterKey, OrgKey, ProviderKey, PinKey, CipherKey } from "../../types/key";
import { KeySuffixOptions, KdfType, HashPurpose } from "../enums";
import { EncArrayBuffer } from "../models/domain/enc-array-buffer";
import { EncString } from "../models/domain/enc-string";
import { SymmetricCryptoKey } from "../models/domain/symmetric-crypto-key";

export abstract class CryptoService {
  abstract activeUserKey$: Observable<UserKey>;

  /**
   * Returns the an observable key for the given user id.
   *
   * @note this observable represents only user keys stored in memory. A null value does not indicate that we cannot load a user key from storage.
   * @param userId The desired user
   */
  abstract getInMemoryUserKeyFor$(userId: UserId): Observable<UserKey>;
  /**
   * Sets the provided user key and stores
   * any other necessary versions (such as auto, biometrics,
   * or pin)
   *
   * @throws when key is null. Lock the account to clear a key
   * @param key The user key to set
   * @param userId The desired user
   */
  abstract setUserKey(key: UserKey, userId?: string): Promise<void>;
  /**
   * Gets the user key from memory and sets it again,
   * kicking off a refresh of any additional keys
   * (such as auto, biometrics, or pin)
   */
  abstract refreshAdditionalKeys(): Promise<void>;
  /**
   * Observable value that returns whether or not the currently active user has ever had auser key,
   * i.e. has ever been unlocked/decrypted. This is key for differentiating between TDE locked and standard locked states.
   */
  abstract everHadUserKey$: Observable<boolean>;
  /**
   * Retrieves the user key
   * @param userId The desired user
   * @returns The user key
   */
  abstract getUserKey(userId?: string): Promise<UserKey>;

  /**
   * Checks if the user is using an old encryption scheme that used the master key
   * for encryption of data instead of the user key.
   */
  abstract isLegacyUser(masterKey?: MasterKey, userId?: string): Promise<boolean>;
  /**
   * Use for encryption/decryption of data in order to support legacy
   * encryption models. It will return the user key if available,
   * if not it will return the master key.
   * @param userId The desired user
   */
  abstract getUserKeyWithLegacySupport(userId?: string): Promise<UserKey>;
  /**
   * Retrieves the user key from storage
   * @param keySuffix The desired version of the user's key to retrieve
   * @param userId The desired user
   * @returns The user key
   */
  abstract getUserKeyFromStorage(keySuffix: KeySuffixOptions, userId?: string): Promise<UserKey>;

  /**
   * Determines whether the user key is available for the given user.
   * @param userId The desired user. If not provided, the active user will be used. If no active user exists, the method will return false.
   * @returns True if the user key is available
   */
  abstract hasUserKey(userId?: UserId): Promise<boolean>;
  /**
   * Determines whether the user key is available for the given user in memory.
   * @param userId The desired user. If not provided, the active user will be used. If no active user exists, the method will return false.
   * @returns True if the user key is available
   */
  abstract hasUserKeyInMemory(userId?: string): Promise<boolean>;
  /**
   * @param keySuffix The desired version of the user's key to check
   * @param userId The desired user
   * @returns True if the provided version of the user key is stored
   */
  abstract hasUserKeyStored(keySuffix: KeySuffixOptions, userId?: string): Promise<boolean>;
  /**
   * Generates a new user key
   * @param masterKey The user's master key
   * @returns A new user key and the master key protected version of it
   */
  abstract makeUserKey(key: MasterKey): Promise<[UserKey, EncString]>;
  /**
   * Clears the user's stored version of the user key
   * @param keySuffix The desired version of the key to clear
   * @param userId The desired user
   */
  abstract clearStoredUserKey(keySuffix: KeySuffixOptions, userId?: string): Promise<void>;
  /**
   * Stores the master key encrypted user key
   * @param userKeyMasterKey The master key encrypted user key to set
   * @param userId The desired user
   */
  abstract setMasterKeyEncryptedUserKey(UserKeyMasterKey: string, userId?: string): Promise<void>;
  /**
   * @param password The user's master password that will be used to derive a master key if one isn't found
   * @param userId The desired user
   */
  abstract getOrDeriveMasterKey(password: string, userId?: string): Promise<MasterKey>;
  /**
   * Generates a master key from the provided password
   * @param password The user's master password
   * @param email The user's email
   * @param kdf The user's selected key derivation function to use
   * @param KdfConfig The user's key derivation function configuration
   * @returns A master key derived from the provided password
   */
  abstract makeMasterKey(
    password: string,
    email: string,
    kdf: KdfType,
    KdfConfig: KdfConfig,
  ): Promise<MasterKey>;
  /**
   * Encrypts the existing (or provided) user key with the
   * provided master key
   * @param masterKey The user's master key
   * @param userKey The user key
   * @returns The user key and the master key protected version of it
   */
  abstract encryptUserKeyWithMasterKey(
    masterKey: MasterKey,
    userKey?: UserKey,
  ): Promise<[UserKey, EncString]>;
  /**
   * Decrypts the user key with the provided master key
   * @param masterKey The user's master key
   * @param userKey The user's encrypted symmetric key
   * @param userId The desired user
   * @returns The user key
   */
  abstract decryptUserKeyWithMasterKey(
    masterKey: MasterKey,
    userKey?: EncString,
    userId?: string,
  ): Promise<UserKey>;
  /**
   * Creates a master password hash from the user's master password. Can
   * be used for local authentication or for server authentication depending
   * on the hashPurpose provided.
   * @param password The user's master password
   * @param key The user's master key
   * @param hashPurpose The iterations to use for the hash
   * @returns The user's master password hash
   */
  abstract hashMasterKey(
    password: string,
    key: MasterKey,
    hashPurpose?: HashPurpose,
  ): Promise<string>;
  /**
   * Compares the provided master password to the stored password hash and server password hash.
   * Updates the stored hash if outdated.
   * @param masterPassword The user's master password
   * @param key The user's master key
   * @returns True if the provided master password matches either the stored
   * key hash or the server key hash
   */
  abstract compareAndUpdateKeyHash(masterPassword: string, masterKey: MasterKey): Promise<boolean>;
  /**
   * Stores the encrypted organization keys and clears any decrypted
   * organization keys currently in memory
   * @param orgs The organizations to set keys for
   * @param providerOrgs The provider organizations to set keys for
   */
  abstract setOrgKeys(
    orgs: ProfileOrganizationResponse[],
    providerOrgs: ProfileProviderOrganizationResponse[],
  ): Promise<void>;
  abstract activeUserOrgKeys$: Observable<Record<OrganizationId, OrgKey>>;
  /**
   * Returns the organization's symmetric key
   * @deprecated Use the observable activeUserOrgKeys$ and `map` to the desired orgKey instead
   * @param orgId The desired organization
   * @returns The organization's symmetric key
   */
  abstract getOrgKey(orgId: string): Promise<OrgKey>;
  /**
   * @deprecated Use the observable activeUserOrgKeys$ instead
   * @returns A record of the organization Ids to their symmetric keys
   */
  abstract getOrgKeys(): Promise<Record<string, SymmetricCryptoKey>>;
  /**
   * Uses the org key to derive a new symmetric key for encrypting data
   * @param orgKey The organization's symmetric key
   */
  abstract makeDataEncKey<T extends UserKey | OrgKey>(
    key: T,
  ): Promise<[SymmetricCryptoKey, EncString]>;
  /**
   * Stores the encrypted provider keys and clears any decrypted
   * provider keys currently in memory
   * @param providers The providers to set keys for
   */
  abstract activeUserProviderKeys$: Observable<Record<ProviderId, ProviderKey>>;
  abstract setProviderKeys(orgs: ProfileProviderResponse[]): Promise<void>;
  /**
   * @param providerId The desired provider
   * @returns The provider's symmetric key
   */
  abstract getProviderKey(providerId: string): Promise<ProviderKey>;
  /**
   * @returns A record of the provider Ids to their symmetric keys
   */
  abstract getProviderKeys(): Promise<Record<ProviderId, ProviderKey>>;
  /**
   * Returns the public key from memory. If not available, extracts it
   * from the private key and stores it in memory
   * @returns The user's public key
   */
  abstract getPublicKey(): Promise<Uint8Array>;
  /**
   * Creates a new organization key and encrypts it with the user's public key.
   * This method can also return Provider keys for creating new Provider users.
   * @returns The new encrypted org key and the decrypted key itself
   */
  abstract makeOrgKey<T extends OrgKey | ProviderKey>(): Promise<[EncString, T]>;
  /**
   * Sets the the user's encrypted private key in storage and
   * clears the decrypted private key from memory
   * Note: does not clear the private key if null is provided
   * @param encPrivateKey An encrypted private key
   */
  abstract setPrivateKey(encPrivateKey: string): Promise<void>;
  /**
   * Returns the private key from memory. If not available, decrypts it
   * from storage and stores it in memory
   * @returns The user's private key
   */
  abstract getPrivateKey(): Promise<Uint8Array>;
  /**
   * Generates a fingerprint phrase for the user based on their public key
   * @param fingerprintMaterial Fingerprint material
   * @param publicKey The user's public key
   * @returns The user's fingerprint phrase
   */
  abstract getFingerprint(fingerprintMaterial: string, publicKey?: Uint8Array): Promise<string[]>;
  /**
   * Generates a new keypair
   * @param key A key to encrypt the private key with. If not provided,
   * defaults to the user key
   * @returns A new keypair: [publicKey in Base64, encrypted privateKey]
   */
  abstract makeKeyPair(key?: SymmetricCryptoKey): Promise<[string, EncString]>;
  /**
   * @param pin The user's pin
   * @param salt The user's salt
   * @param kdf The user's kdf
   * @param kdfConfig The user's kdf config
   * @returns A key derived from the user's pin
   */
  abstract makePinKey(
    pin: string,
    salt: string,
    kdf: KdfType,
    kdfConfig: KdfConfig,
  ): Promise<PinKey>;
  /**
   * Clears the user's pin keys from storage
   * Note: This will remove the stored pin and as a result,
   * disable pin protection for the user
   * @param userId The desired user
   */
  abstract clearPinKeys(userId?: string): Promise<void>;
  /**
   * Decrypts the user key with their pin
   * @param pin The user's PIN
   * @param salt The user's salt
   * @param kdf The user's KDF
   * @param kdfConfig The user's KDF config
   * @param pinProtectedUserKey The user's PIN protected symmetric key, if not provided
   * it will be retrieved from storage
   * @returns The decrypted user key
   */
  abstract decryptUserKeyWithPin(
    pin: string,
    salt: string,
    kdf: KdfType,
    kdfConfig: KdfConfig,
    protectedKeyCs?: EncString,
  ): Promise<UserKey>;
  /**
   * Creates a new Pin key that encrypts the user key instead of the
   * master key. Clears the old Pin key from state.
   * @param masterPasswordOnRestart True if Master Password on Restart is enabled
   * @param pin User's PIN
   * @param email User's email
   * @param kdf User's KdfType
   * @param kdfConfig User's KdfConfig
   * @param oldPinKey The old Pin key from state (retrieved from different
   * places depending on if Master Password on Restart was enabled)
   * @returns The user key
   */
  abstract decryptAndMigrateOldPinKey(
    masterPasswordOnRestart: boolean,
    pin: string,
    email: string,
    kdf: KdfType,
    kdfConfig: KdfConfig,
    oldPinKey: EncString,
  ): Promise<UserKey>;
  /**
   * Replaces old master auto keys with new user auto keys
   */
  abstract migrateAutoKeyIfNeeded(userId?: string): Promise<void>;
  /**
   * @param keyMaterial The key material to derive the send key from
   * @returns A new send key
   */
  abstract makeSendKey(keyMaterial: Uint8Array): Promise<SymmetricCryptoKey>;
  /**
   * Clears all of the user's keys from storage
   * @param userId The user's Id
   */
  abstract clearKeys(userId?: string): Promise<any>;
  /**
   * RSA encrypts a value.
   * @param data The data to encrypt
   * @param publicKey The public key to use for encryption, if not provided, the user's public key will be used
   * @returns The encrypted data
   */
  abstract rsaEncrypt(data: Uint8Array, publicKey?: Uint8Array): Promise<EncString>;
  /**
   * Decrypts a value using RSA.
   * @param encValue The encrypted value to decrypt
   * @param privateKeyValue The private key to use for decryption
   * @returns The decrypted value
   */
  abstract rsaDecrypt(encValue: string, privateKeyValue?: Uint8Array): Promise<Uint8Array>;
  abstract randomNumber(min: number, max: number): Promise<number>;
  /**
   * Generates a new cipher key
   * @returns A new cipher key
   */
  abstract makeCipherKey(): Promise<CipherKey>;

  /**
   * Initialize all necessary crypto keys needed for a new account.
   * Warning! This completely replaces any existing keys!
   * @returns The user's newly created  public key, private key, and encrypted private key
   */
  abstract initAccount(): Promise<{
    userKey: UserKey;
    publicKey: string;
    privateKey: EncString;
  }>;

  /**
   * Validate that the KDF config follows the requirements for the given KDF type.
   *
   * @remarks
   * Should always be called before updating a users KDF config.
   */
  abstract validateKdfConfig(kdf: KdfType, kdfConfig: KdfConfig): void;

  /**
   * @deprecated Left for migration purposes. Use decryptUserKeyWithPin instead.
   */
  abstract decryptMasterKeyWithPin(
    pin: string,
    salt: string,
    kdf: KdfType,
    kdfConfig: KdfConfig,
    protectedKeyCs?: EncString,
  ): Promise<MasterKey>;
  /**
   * Previously, the master key was used for any additional key like the biometrics or pin key.
   * We have switched to using the user key for these purposes. This method is for clearing the state
   * of the older keys on logout or post migration.
   * @param keySuffix The desired type of key to clear
   * @param userId The desired user
   */
  abstract clearDeprecatedKeys(keySuffix: KeySuffixOptions, userId?: string): Promise<void>;
  /**
   * @deprecated July 25 2022: Get the key you need from CryptoService (getKeyForUserEncryption or getOrgKey)
   * and then call encryptService.encrypt
   */
  abstract encrypt(plainValue: string | Uint8Array, key?: SymmetricCryptoKey): Promise<EncString>;
  /**
   * @deprecated July 25 2022: Get the key you need from CryptoService (getKeyForUserEncryption or getOrgKey)
   * and then call encryptService.encryptToBytes
   */
  abstract encryptToBytes(
    plainValue: Uint8Array,
    key?: SymmetricCryptoKey,
  ): Promise<EncArrayBuffer>;
  /**
   * @deprecated July 25 2022: Get the key you need from CryptoService (getKeyForUserEncryption or getOrgKey)
   * and then call encryptService.decryptToBytes
   */
  abstract decryptToBytes(encString: EncString, key?: SymmetricCryptoKey): Promise<Uint8Array>;
  /**
   * @deprecated July 25 2022: Get the key you need from CryptoService (getKeyForUserEncryption or getOrgKey)
   * and then call encryptService.decryptToUtf8
   */
  abstract decryptToUtf8(encString: EncString, key?: SymmetricCryptoKey): Promise<string>;
  /**
   * @deprecated July 25 2022: Get the key you need from CryptoService (getKeyForUserEncryption or getOrgKey)
   * and then call encryptService.decryptToBytes
   */
  abstract decryptFromBytes(
    encBuffer: EncArrayBuffer,
    key: SymmetricCryptoKey,
  ): Promise<Uint8Array>;
}
