import { getCryptoLib } from './cryptoUtils';

type NodeCryptoCreateCipher = typeof import('crypto').createCipheriv;
type NodeCryptoCreateDecipher = typeof import('crypto').createDecipheriv;

export type CipherAlgorithm = 'aes-256-cbc' | 'aes-128-cbc';

export interface AesCipher {
  encrypt(algorithm: CipherAlgorithm, key: Buffer, iv: Buffer, data: Buffer): Promise<Buffer>;

  decrypt(algorithm: CipherAlgorithm, key: Buffer, iv: Buffer, data: Buffer): Promise<Buffer>;
}

export class NodeCryptoAesCipher implements AesCipher {
  createCipher: NodeCryptoCreateCipher;

  createDecipher: NodeCryptoCreateDecipher;

  constructor(createCipher: NodeCryptoCreateCipher, createDecipher: NodeCryptoCreateDecipher) {
    this.createCipher = createCipher;
    this.createDecipher = createDecipher;
  }

  async encrypt(
    algorithm: CipherAlgorithm,
    key: Buffer,
    iv: Buffer,
    data: Buffer
  ): Promise<Buffer> {
    if (algorithm !== 'aes-128-cbc' && algorithm !== 'aes-256-cbc') {
      throw new Error(`Unsupported cipher algorithm "${algorithm}"`);
    }
    const cipher = this.createCipher(algorithm, key, iv);
    const result = Buffer.concat([cipher.update(data), cipher.final()]);
    return Promise.resolve(result);
  }

  async decrypt(
    algorithm: CipherAlgorithm,
    key: Buffer,
    iv: Buffer,
    data: Buffer
  ): Promise<Buffer> {
    if (algorithm !== 'aes-128-cbc' && algorithm !== 'aes-256-cbc') {
      throw new Error(`Unsupported cipher algorithm "${algorithm}"`);
    }
    const cipher = this.createDecipher(algorithm, key, iv);
    const result = Buffer.concat([cipher.update(data), cipher.final()]);
    return Promise.resolve(result);
  }
}

export class WebCryptoAesCipher implements AesCipher {
  subtleCrypto: SubtleCrypto;

  constructor(subtleCrypto: SubtleCrypto) {
    this.subtleCrypto = subtleCrypto;
  }

  async encrypt(
    algorithm: CipherAlgorithm,
    key: Buffer,
    iv: Buffer,
    data: Buffer
  ): Promise<Buffer> {
    let algo: string;
    let length: number;
    if (algorithm === 'aes-128-cbc') {
      algo = 'AES-CBC';
      length = 128;
    } else if (algorithm === 'aes-256-cbc') {
      algo = 'AES-CBC';
      length = 256;
    } else {
      throw new Error(`Unsupported cipher algorithm "${algorithm}"`);
    }
    const cryptoKey = await this.subtleCrypto.importKey('raw', key, { name: algo, length }, false, [
      'encrypt',
    ]);
    const result = await this.subtleCrypto.encrypt({ name: algo, iv }, cryptoKey, data);
    return Buffer.from(result);
  }

  async decrypt(
    algorithm: CipherAlgorithm,
    key: Buffer,
    iv: Buffer,
    data: Buffer
  ): Promise<Buffer> {
    let algo: string;
    let length: number;
    if (algorithm === 'aes-128-cbc') {
      algo = 'AES-CBC';
      length = 128;
    } else if (algorithm === 'aes-256-cbc') {
      algo = 'AES-CBC';
      length = 256;
    } else {
      throw new Error(`Unsupported cipher algorithm "${algorithm}"`);
    }
    const cryptoKey = await this.subtleCrypto.importKey('raw', key, { name: algo, length }, false, [
      'decrypt',
    ]);
    const result = await this.subtleCrypto.decrypt({ name: algo, iv }, cryptoKey, data);
    return Buffer.from(result);
  }
}

export async function createCipher(): Promise<AesCipher> {
  const cryptoLib = await getCryptoLib();
  if (cryptoLib.name === 'subtleCrypto') {
    return new WebCryptoAesCipher(cryptoLib.lib);
  } else {
    return new NodeCryptoAesCipher(cryptoLib.lib.createCipheriv, cryptoLib.lib.createDecipheriv);
  }
}
