import * as utils from '../../utils.js';
import {
  DerivationNonce,
  IAsymmetricKey,
  IDerivedKey,
  IKeyDerivationStrategy,
  IPublicKey,
  KeyInformation,
} from './interfaces.js';
import { SupportedKeyDerivationTypes } from '../module.js';
import { JWK } from 'jose';
import { base64, crypto } from '../../utils.js';
import el from 'elliptic';

export class P256KeyDerivationStrategy implements IKeyDerivationStrategy {
  constructor(private algorithmType: 'ECDH' | 'ECDSA') {}

  hydrate(masterKey: CryptoKey, nonce: DerivationNonce): Promise<IDerivedKey> {
    return this.create(masterKey, nonce.salt);
  }

  async fromRaw(rawKey: Uint8Array): Promise<P256Key> {
    const ec = new el.ec((el.curves as any)['p256']);
    const keyfrompriv = ec.keyFromPrivate(rawKey);
    const pubKey = new Uint8Array(keyfrompriv.getPublic(false, 'array')).slice(1);
    const xs = pubKey.slice(0, 32);
    const ys = pubKey.slice(32);

    const jwk = {
      kty: 'EC',
      crv: 'P-256',
      d: base64.bufToUrlBase64(rawKey),
      x: base64.bufToUrlBase64(xs),
      y: base64.bufToUrlBase64(ys),
    };

    const keyUsage: KeyUsage[] = this.algorithmType === 'ECDH' ? ['deriveBits'] : ['sign'];

    const kid = await utils.jose.calculateJwkThumbprint(jwk, 'sha256');
    const cryptoKey = await crypto.subtle.importKey(
      'jwk',
      jwk,
      { name: this.algorithmType, namedCurve: 'P-256' },
      true,
      keyUsage,
    );

    return new P256Key(
      cryptoKey,
      {
        ...jwk,
        kid,
      },
      rawKey,
    );
  }

  async create(masterKey: CryptoKey, salt: string): Promise<P256Key> {
    const rawMasterKey = await crypto.subtle.exportKey('raw', masterKey);
    const { secret, xs, ys } = await utils.generateP256Key(rawMasterKey, salt);

    const jwk = {
      kty: 'EC',
      crv: 'P-256',
      d: base64.bufToUrlBase64(secret),
      x: base64.bufToUrlBase64(xs),
      y: base64.bufToUrlBase64(ys),
    };

    const kid = await utils.jose.calculateJwkThumbprint(jwk, 'sha256');

    const keyUsage: KeyUsage[] = this.algorithmType === 'ECDH' ? ['deriveBits'] : ['sign'];

    const cryptoKey = await crypto.subtle.importKey(
      'jwk',
      jwk,
      { name: this.algorithmType, namedCurve: 'P-256' },
      true,
      keyUsage,
    );

    return new P256Key(
      cryptoKey,
      {
        ...jwk,
        kid,
      },
      secret,
      new DerivationNonce(salt, 0),
    );
  }
}

export class P256PublicKey implements IPublicKey {
  constructor(private cryptoKey: CryptoKey) {}

  async getPublicKeyHex(): Promise<string> {
    return utils.hex.bufToHex(await this.getPublicKeyBytes());
  }

  async getPublicKeyBytes(): Promise<Uint8Array> {
    const extractedKeyBytes = await crypto.subtle.exportKey('raw', this.cryptoKey);
    return new Uint8Array(extractedKeyBytes);
  }

  verify(signature: Uint8Array, message: Uint8Array): Promise<boolean> {
    return crypto.subtle.verify(
      {
        hash: 'SHA-256',
        name: 'ECDSA',
      } as EcdsaParams,
      this.cryptoKey,
      signature,
      message,
    );
  }
}

export class P256Key implements IAsymmetricKey, IDerivedKey {
  constructor(
    private key: CryptoKey,
    private jwk: JWK & Required<Pick<JWK, 'kid'>>,
    private secret?: Uint8Array,
    private derivation_path?: DerivationNonce,
  ) {}

  async getPublicKeyBytes(): Promise<Uint8Array> {
    const pubKey = await this.getPublicKey();
    return await pubKey.getPublicKeyBytes();
  }

  async getPrivateKeyBytes(): Promise<Uint8Array> {
    const exportedRaw = await crypto.subtle.exportKey('raw', this.key);

    return new Uint8Array(exportedRaw);
  }

  async toJWK(): Promise<JWK & Required<Pick<JWK, 'kid'>>> {
    return this.jwk;
  }

  async toCryptoKey(): Promise<CryptoKey> {
    return this.key;
  }

  async exportKeyInformation(): Promise<KeyInformation> {
    return {
      version: 1,
      curve: SupportedKeyDerivationTypes.P256_ECDSA,
      key: this.derivation_path
        ? {
            type: 'derived',
            from: 'master',
            nonce: this.derivation_path.toString(),
          }
        : {
            type: 'raw',
            rawKeyHex: utils.hex.bufToHex(this.secret!),
          },
      publicKey: utils.hex.bufToHex(await this.getPublicKeyBytes()),
      fingerprint: utils.hex.bufToHex(await this.getFingerprint()),
    };
  }

  async sign(message: Uint8Array): Promise<Uint8Array> {
    const signature = await crypto.subtle.sign(
      {
        hash: 'SHA-256',
        name: 'ECDSA',
      } as EcdsaParams,
      this.key,
      message,
    );

    return new Uint8Array(signature);
  }

  async verify(signature: Uint8Array, message: Uint8Array): Promise<boolean> {
    const pubKey = await this.getPublicKey();
    return Promise.resolve(pubKey.verify(signature, message));
  }

  getPath(): DerivationNonce {
    return this.derivation_path!;
  }

  async getFingerprint(): Promise<Uint8Array> {
    const pubKey = await this.getPublicKey();

    return utils.sha256(await pubKey.getPublicKeyBytes());
  }

  async getPublicKey(): Promise<P256PublicKey> {
    const jwkPrivate = Object.assign({}, this.jwk);

    // Delete secret P256 data, leaving only public information
    delete jwkPrivate.d;
    jwkPrivate.key_ops = ['verify'];

    const publicKey = await crypto.subtle.importKey('jwk', jwkPrivate, { name: 'ECDSA', namedCurve: 'P-256' }, true, [
      'verify',
    ]);

    return Promise.resolve(new P256PublicKey(publicKey));
  }
}
