import { AES256GCMKeyDerivationStrategy } from './strategies/aes256gcm.js';
import { DerivationNonce, IDerivedKey, IKey, IKeyDerivationStrategy } from './strategies/interfaces.js';
import { P256KeyDerivationStrategy } from './strategies/p256ECDSA.js';
import { Secp256k1KeyDerivationStrategy } from './strategies/secp256k1.js';
import { IDerivationService } from './DerivationService.js';
// eslint-disable-next-line
// @ts-ignore
import { deserialiseKeyInformation, utils } from '../../index.js';
import { KeyBuilder } from './KeyBuilder.js';
import { MB_DERIVING_KEY, MB_SIGNING_KEY } from '../types.js';
import * as jose from 'jose';

export enum SupportedKeyDerivationTypes {
  P256_ECDH = 'P256_ECDH',
  P256_ECDSA = 'P256_ECDSA',
  SECP256K1 = 'SECP256K1',
  AES256GCM = 'AES256GCM',
}

interface Key {
  key: IKey | IDerivedKey;
  name: string;
}

type StrategiesMap = {
  [key in SupportedKeyDerivationTypes]: IKeyDerivationStrategy;
};

/**
 * Key Management SDK Module.
 *
 * This module is responsible for creating, importing and searching for a users keys.
 *
 * ## Importing Example:
 *  ```js
 *  const cryptoKey = window.crypto.subtle.generateKey(
 *    {
 *      name: "ECDH",
 *      namedCurve: "P-256"
 *    },
 *    true,
 *    ["deriveKey", "deriveBits"]
 *  );
 *
 *  const myCryptoKeyBuffer = await window.crypto.subtle.exportKey("raw", cryptoKey);
 *  const importedKey = await client.keys.import(
 *    "Example Key",
 *    myCryptoKeyBuffer,
 *    SupportedKeyDerivationTypes.P256_ECDH
 *  );
 *  ```
 *
 * ## Creation Keybuilder Example
 * - This method of key creation is useful for when you want to use MintBlue's sensible defaults.
 * - You may not know which curve you specifically need but know the use case for the key such as encryption or signing.
 *
 * ```js
 *  await await client.keys
 *    .keyBuilder()
 *    .setAsymmetric()
 *    .setSignable()
 *    .setName("Example P256 ECDSA")
 *    .build()
 * ```
 * More documentation is available on the KeyBuilder class itself.
 *
 * ## Creation Create function Example:
 * ```js
 *  let createdKey = await client.keys.create("Example P256 ECDSA", SupportedKeyDerivationTypes.P256_ECDSA)
 * ```
 *
 * ## Searching Example:
 * ```js
 *  let myBitcoinKey = await client.keys.findKeyByName("My Bitcoin Key")
 * ```
 */

export const SECP256K1_STRATEGY = new Secp256k1KeyDerivationStrategy();
export const P256_ECDH_STRATEGY = new P256KeyDerivationStrategy('ECDH');
export const P256_ECDSA_STRATEGY = new P256KeyDerivationStrategy('ECDSA');
export const AES256GCM_STRATEGY = new AES256GCMKeyDerivationStrategy();

export class KeysSDKModule {
  private strategies: StrategiesMap = {
    [SupportedKeyDerivationTypes.SECP256K1]: SECP256K1_STRATEGY,

    [SupportedKeyDerivationTypes.P256_ECDH]: P256_ECDH_STRATEGY,
    [SupportedKeyDerivationTypes.P256_ECDSA]: P256_ECDSA_STRATEGY,

    [SupportedKeyDerivationTypes.AES256GCM]: AES256GCM_STRATEGY,
  };
  private keysMap: Map<string, Key> = new Map();
  private mapOutdated: boolean = true;

  constructor(private derivationService: IDerivationService) {}

  /**
   * Creates a new Key off of the user's Master Key
   */
  public async create(name: string, strategy: IKeyDerivationStrategy): Promise<IKey> {
    if (!name || !strategy) {
      throw new Error('create() requires a name and a strategy');
    }

    try {
      const salt = utils.generateSaltToDeriveKey(32);
      const derivationKey = await strategy.create(this.derivationService.getMasterKey(), salt);

      await this.derivationService.saveKey(derivationKey, name, false);

      return derivationKey;
    } catch (e) {
      throw e;
    } finally {
      this.mapOutdated = true;
    }
  }

  /**
   * A stateful key creation entity.
   *
   * This method of key creation is useful for when you want to use MintBlue's sensible defaults.
   * You may not know which curve you specifically need but know the use case for the key such as encryption or signing.
   */
  public keyBuilder(): KeyBuilder {
    return new KeyBuilder(this);
  }

  /**
   * Import any of the default keys into the key derivation service (as a raw
   * key)
   *  NOTE: Only used during initializing of the SDK when any of the default
   *  keys need to be migrated
   */
  public async import(name: string, raw: ArrayBuffer, curve: SupportedKeyDerivationTypes): Promise<IKey> {
    try {
      const key = await this.strategies[curve].fromRaw(new Uint8Array(raw));
      await this.derivationService.saveKey(key, name, true);
      return key;
    } catch (e) {
      throw e;
    } finally {
      this.mapOutdated = true;
    }
  }

  /**
   * Fetches all keys from the Key Derivation service and returns them as a Map indexed by thumbprint
   */
  public async getKeys(): Promise<Map<string, Key>> {
    try {
      const masterKey = this.derivationService.getMasterKey();
      const derivationMap = await this.derivationService.getKeys();

      // Recursive function to resolve keys back to the root
      const resolveKey = async (fingerprint: string): Promise<Key | undefined> => {
        if (this.keysMap.has(fingerprint)) {
          return this.keysMap.get(fingerprint);
        }

        const derivation = derivationMap.get(fingerprint);
        if (!derivation) {
          console.error('Missing key derivation for fingerprint:', fingerprint);
          return undefined;
        }

        const plaintext = await utils.decryptFromB64String(derivation.path, this.derivationService.getMasterKey());
        const jsonString = Buffer.from(plaintext).toString('utf-8');
        const keyInfo = deserialiseKeyInformation(jsonString);

        const strategy = this.strategies[keyInfo.curve];

        if (keyInfo.key.type === 'raw') {
          const rawKey = keyInfo.key.rawKeyHex;

          if (!rawKey) {
            throw new Error('Cannot import a raw key without raw key material!');
          }

          const decodedKey = new Uint8Array(utils.hex.hexToBuf(rawKey));
          const rootKey = await strategy.fromRaw(decodedKey);

          const key: Key = {
            key: rootKey,
            name: derivation.name,
          };
          this.keysMap.set(fingerprint, key);
          return key;
        } else if (keyInfo.key.type === 'derived') {
          if (keyInfo.key.from !== 'master') {
            throw new Error('We dont yet support deriving keys from non-master parents');
          }

          if (!keyInfo.key.nonce) {
            throw new Error('Cannot derive a child key without a corresponding nonce!');
          }

          const childKey = await strategy.hydrate(masterKey, DerivationNonce.fromString(keyInfo.key.nonce));

          console.assert(
            utils.hex.bufToHex(await childKey.getFingerprint()) === fingerprint,
            "Child key fingerprint doesn't match expected fingerprint.",
          );

          const key: Key = {
            key: childKey,
            name: derivation.name,
          };
          this.keysMap.set(fingerprint, key); // Assuming derived keys are handled similarly
          return key;
        }

        return undefined;
      };

      // Resolve all keys in the derivation map
      for (const fingerprint of derivationMap.keys()) {
        await resolveKey(fingerprint);
      }

      return this.keysMap;
    } catch (error) {
      console.error('Failed to get keys', { error });
      throw error;
    } finally {
      this.mapOutdated = false;
    }
  }

  /**
   * Find a key by its name
   */
  public async findKeyByName(name: string): Promise<IKey | IDerivedKey | undefined> {
    try {
      if (this.mapOutdated) {
        await this.getKeys();
      }

      for (const key of this.keysMap.values()) {
        if (key.name === name) {
          return key.key;
        }
      }
    } catch (error) {
      console.warn('Failed to find key by name, returning undefined', {
        error,
      });
    }
    return undefined;
  }

  /**
   * Returns the default keys used by mintblue
   */
  async getDefaultKeys() {
    return await Promise.all([
      this.findKeyByName(MB_DERIVING_KEY),
      this.findKeyByName(MB_SIGNING_KEY),
      this.findKeyByName('peppol'),
      this.findKeyByName('kvk'),
      this.findKeyByName('bitcoin'),
    ]);
  }

  async verifyKeyMigrationNeeded(): Promise<boolean> {
    const [mb_deriving_key, mb_signing_key, peppol_key, kvk_key, bitcoin_key] = await this.getDefaultKeys();
    // If all of them exist, continue
    const hasAllNecessaryKeys = mb_deriving_key && mb_signing_key && peppol_key && kvk_key && bitcoin_key;
    return !hasAllNecessaryKeys;
  }

  async asJoseGeneralDecryptGetKey(): Promise<utils.jose.GeneralDecryptGetKey> {
    const currentKeys = [...this.keysMap.values()];
    const jwkKeys: jose.JWK[] = [];
    for (const jwkKey of currentKeys) {
      const key = await jwkKey.key.toJWK({ includePrivate: true });
      jwkKeys.push(key);
    }

    const jwsKeystoreFn: jose.GeneralDecryptGetKey = async (
      _protectedHeader: utils.jose.JWEHeaderParameters,
      token: utils.jose.FlattenedJWE,
    ) => {
      if (!token || !token.header || !token.header.kid) {
        throw new Error('Unable to match key because token.header.kid');
      }
      const kid = token.header.kid;
      const jwkKey = jwkKeys.find((key) => key.kid === kid)!;

      const cryptoKey = await jose.importJWK(jwkKey, 'ECDH-ES');
      return cryptoKey;
    };
    return jwsKeystoreFn;
  }
}
