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

import { isBytesKeyPair } from '../utils.js';
import { Ed25519 } from '../crypto-primitives/index.js';
import { CryptoKey, BaseEdDsaAlgorithm } from '../algorithms-api/index.js';

export class EdDsaAlgorithm extends BaseEdDsaAlgorithm {
  public readonly namedCurves = ['Ed25519', 'Ed448'];

  public async generateKey(options: {
    algorithm: IDCrypto.EdDsaGenerateKeyOptions,
    extractable: boolean,
    keyUsages: IDCrypto.KeyUsage[]
  }): Promise<IDCrypto.CryptoKeyPair> {
    const { algorithm, extractable, keyUsages } = options;

    this.checkGenerateKey({ algorithm, keyUsages });

    let keyPair: BytesKeyPair | undefined;
    let cryptoKeyPair: IDCrypto.CryptoKeyPair;

    switch (algorithm.namedCurve) {

      case 'Ed25519': {
        keyPair = await Ed25519.generateKeyPair();
        break;
      }
      // Default case not needed because checkGenerateKey() already validates the specified namedCurve is supported.
    }

    if (!isBytesKeyPair(keyPair)) {
      throw new Error('Operation failed to generate key pair.');
    }

    cryptoKeyPair = {
      privateKey : new CryptoKey(algorithm, extractable, keyPair.privateKey, 'private', this.keyUsages.privateKey),
      publicKey  : new CryptoKey(algorithm, true, keyPair.publicKey, 'public', this.keyUsages.publicKey)
    };

    return cryptoKeyPair;
  }

  public async sign(options: {
    algorithm: IDCrypto.EdDsaOptions,
    key: IDCrypto.CryptoKey,
    data: Uint8Array
  }): Promise<Uint8Array> {
    const { algorithm, key, data } = options;

    this.checkAlgorithmOptions({ algorithm });
    // The key's algorithm must match the algorithm implementation processing the operation.
    this.checkKeyAlgorithm({ keyAlgorithmName: key.algorithm.name });
    // The key must be a private key.
    this.checkKeyType({ keyType: key.type, allowedKeyType: 'private' });
    // The key must be allowed to be used for sign operations.
    this.checkKeyUsages({ keyUsages: ['sign'], allowedKeyUsages: key.usages });

    let signature: Uint8Array;

    const keyAlgorithm = key.algorithm as IDCrypto.EdDsaGenerateKeyOptions; // Type guard.

    switch (keyAlgorithm.namedCurve) {

      case 'Ed25519': {
        signature = await Ed25519.sign({ key: key.material, data });
        break;
      }

      default:
        throw new TypeError(`Out of range: '${keyAlgorithm.namedCurve}'. Must be one of '${this.namedCurves.join(', ')}'`);
    }

    return signature;
  }

  public async verify(options: {
    algorithm: IDCrypto.EdDsaOptions;
    key: IDCrypto.CryptoKey;
    signature: Uint8Array;
    data: Uint8Array;
  }): Promise<boolean> {
    const { algorithm, key, signature, data } = options;

    this.checkAlgorithmOptions({ algorithm });
    // The key's algorithm must match the algorithm implementation processing the operation.
    this.checkKeyAlgorithm({ keyAlgorithmName: key.algorithm.name });
    // The key must be a public key.
    this.checkKeyType({ keyType: key.type, allowedKeyType: 'public' });
    // The key must be allowed to be used for verify operations.
    this.checkKeyUsages({ keyUsages: ['verify'], allowedKeyUsages: key.usages });

    let isValid: boolean;

    const keyAlgorithm = key.algorithm as IDCrypto.EdDsaGenerateKeyOptions; // Type guard.

    switch (keyAlgorithm.namedCurve) {

      case 'Ed25519': {
        isValid = await Ed25519.verify({ key: key.material, signature, data });
        break;
      }

      default:
        throw new TypeError(`Out of range: '${keyAlgorithm.namedCurve}'. Must be one of '${this.namedCurves.join(', ')}'`);
    }

    return isValid;
  }
}