import type { IDCrypto } from '../types/iddwn-crypto.js';

import { InvalidAccessError, NotSupportedError } from './errors.js';

export abstract class CryptoAlgorithm {

  /**
   * Name of the algorithm
   */
  public abstract readonly name: string;

  /**
   * Indicates which cryptographic operations are permissible to be used with this algorithm.
   */
  public abstract readonly keyUsages: IDCrypto.KeyUsage[] | IDCrypto.KeyPairUsage;

  public checkAlgorithmName(options: {
    algorithmName: string
  }): void {
    const { algorithmName } = options;
    if (algorithmName === undefined) {
      throw new TypeError(`Required parameter missing: 'algorithmName'`);
    }
    if (algorithmName !== this.name) {
      throw new NotSupportedError(`Algorithm not supported: '${algorithmName}'`);
    }
  }

  public checkCryptoKey(options: {
    key: IDCrypto.CryptoKey
  }): void {
    const { key } = options;
    if (!('algorithm' in key && 'extractable' in key && 'type' in key && 'usages' in key)) {
      throw new TypeError('Object is not a CryptoKey');
    }
  }

  public checkKeyAlgorithm(options: {
    keyAlgorithmName: string
  }): void {
    const { keyAlgorithmName } = options;
    if (keyAlgorithmName === undefined) {
      throw new TypeError(`Required parameter missing: 'keyAlgorithmName'`);
    }
    if (keyAlgorithmName && keyAlgorithmName !== this.name) {
      throw new InvalidAccessError(`Algorithm '${this.name}' does not match the provided '${keyAlgorithmName}' key.`);
    }
  }

  public checkKeyType(options: {
    keyType: IDCrypto.KeyType,
    allowedKeyType: IDCrypto.KeyType
  }): void {
    const { keyType, allowedKeyType } = options;
    if (keyType === undefined || allowedKeyType === undefined) {
      throw new TypeError(`One or more required parameters missing: 'keyType, allowedKeyType'`);
    }
    if (keyType && keyType !== allowedKeyType) {
      throw new InvalidAccessError(`Requested operation is not valid for the provided '${keyType}' key.`);
    }
  }

  public checkKeyUsages(options: {
    keyUsages: IDCrypto.KeyUsage[],
    allowedKeyUsages: IDCrypto.KeyUsage[] | IDCrypto.KeyPairUsage
  }): void {
    const { keyUsages, allowedKeyUsages } = options;
    if (!(keyUsages && keyUsages.length > 0)) {
      throw new TypeError(`Required parameter missing or empty: 'keyUsages'`);
    }
    const allowedUsages = (Array.isArray(allowedKeyUsages)) ? allowedKeyUsages : [...allowedKeyUsages.privateKey, ...allowedKeyUsages.publicKey];
    if (!keyUsages.every(usage => allowedUsages.includes(usage))) {
      throw new InvalidAccessError(`Requested operation(s) '${keyUsages.join(', ')}' is not valid for the provided key.`);
    }
  }

  /**
   * Creates an instance of the class on which it is called.
   *
   * This is a generic factory method that creates an instance of any
   * crypto algorithm that extends this abstract class.
   *
   * @template T The type of the instance to be created.
   * @returns An instance of the class it is called on.
   * @throws {TypeError} If the class it is called on cannot be constructed.
   */
  static create<T extends CryptoAlgorithm>(this: new () => T): T {
    return new this();
  }

  public abstract decrypt(options: {
    algorithm: IDCrypto.AlgorithmIdentifier | IDCrypto.AesCtrOptions | IDCrypto.AesGcmOptions,
    key: IDCrypto.CryptoKey,
    data: Uint8Array
  }): Promise<Uint8Array>;

  public abstract deriveBits(options: {
    algorithm: IDCrypto.AlgorithmIdentifier | IDCrypto.EcdhDeriveKeyOptions,
    baseKey: IDCrypto.CryptoKey,
    length: number | null
  }): Promise<Uint8Array>;

  public abstract encrypt(options: {
    algorithm: IDCrypto.AlgorithmIdentifier | IDCrypto.AesCtrOptions | IDCrypto.AesGcmOptions,
    key: IDCrypto.CryptoKey,
    data: Uint8Array
  }): Promise<Uint8Array>;

  public abstract generateKey(options: {
    algorithm: Partial<IDCrypto.GenerateKeyOptions>,
    extractable: boolean,
    keyUsages: IDCrypto.KeyUsage[],
  }): Promise<IDCrypto.CryptoKey | IDCrypto.CryptoKeyPair>;

  public abstract sign(options: {
    algorithm: IDCrypto.AlgorithmIdentifier | IDCrypto.EcdsaOptions | IDCrypto.EdDsaOptions,
    key: IDCrypto.CryptoKey,
    data: Uint8Array
  }): Promise<Uint8Array>;

  public abstract verify(options: {
    algorithm: IDCrypto.AlgorithmIdentifier | IDCrypto.EcdsaOptions | IDCrypto.EdDsaOptions,
    key: IDCrypto.CryptoKey,
    signature: Uint8Array,
    data: Uint8Array
  }): Promise<boolean>;
}