/**
 * Copyright (c) HashiCorp, Inc.
 * SPDX-License-Identifier: MPL-2.0
 */

import Component from '@glimmer/component';
import { action } from '@ember/object';
import { inject as service } from '@ember/service';
import { tracked } from '@glimmer/tracking';
import { KEY_TYPES } from '../../models/keymgmt/key';
import { task } from 'ember-concurrency';
import { waitFor } from '@ember/test-waiters';

/**
 * @module KeymgmtDistribute
 * KeymgmtDistribute components are used to provide a form to distribute Keymgmt keys to a provider.
 *
 * @example
 * ```js
 * <KeymgmtDistribute @backend="keymgmt" @key="my-key" @provider="my-kms" />
 * ```
 * @param {string} backend - name of backend, which will be the basis of other store queries
 * @param {string} [key] - key is the name of the existing key which is being distributed. Will hide the key field in UI
 * @param {string} [provider] - provider is the name of the existing provider which is being distributed to. Will hide the provider field in UI
 */

class DistributionData {
  @tracked key;
  @tracked provider;
  @tracked operations;
  @tracked protection;
}

const VALID_TYPES_BY_PROVIDER = {
  gcpckms: ['aes256-gcm96', 'rsa-2048', 'rsa-3072', 'rsa-4096', 'ecdsa-p256', 'ecdsa-p384', 'ecdsa-p521'],
  awskms: ['aes256-gcm96'],
  azurekeyvault: ['rsa-2048', 'rsa-3072', 'rsa-4096'],
};
export default class KeymgmtDistribute extends Component {
  @service store;
  @service flashMessages;
  @service router;

  @tracked keyModel;
  @tracked isNewKey = false;
  @tracked providerType;
  @tracked formData;
  @tracked formErrors;

  constructor() {
    super(...arguments);
    this.formData = new DistributionData();
    // Set initial values passed in
    this.formData.key = this.args.key || '';
    this.formData.provider = this.args.provider || '';
    // Side effects to get types of key or provider passed in
    if (this.args.provider) {
      this.getProviderType(this.args.provider);
    }
    if (this.args.key) {
      this.getKeyInfo(this.args.key);
    }
    this.formData.operations = [];
  }

  get keyTypes() {
    return KEY_TYPES;
  }

  get validMatchError() {
    if (!this.providerType || !this.keyModel?.type) {
      return null;
    }
    const valid = VALID_TYPES_BY_PROVIDER[this.providerType]?.includes(this.keyModel.type);
    if (valid) return null;

    // default to showing error on provider unless @provider (field hidden)
    if (this.args.provider) {
      return {
        key: `This key type is incompatible with the ${this.providerType} provider. To distribute to this provider, change the key type or choose another key.`,
      };
    }

    const message = `This provider is incompatible with the ${this.keyModel.type} key type. Please choose another provider`;
    return {
      provider: this.args.key ? `${message}.` : `${message} or change the key type.`,
    };
  }

  get operations() {
    const pt = this.providerType;
    if (pt === 'awskms') {
      return ['encrypt', 'decrypt'];
    } else if (pt === 'gcpckms') {
      const kt = this.keyModel?.type || '';
      switch (kt) {
        case 'aes256-gcm96':
          return ['encrypt', 'decrypt'];
        case 'rsa-2048':
        case 'rsa-3072':
        case 'rsa-4096':
          return ['decrypt', 'sign'];
        case 'ecdsa-p256':
        case 'ecdsa-p384':
          return ['sign'];
        default:
          return ['encrypt', 'decrypt', 'sign', 'verify', 'wrap', 'unwrap'];
      }
    }

    return ['encrypt', 'decrypt', 'sign', 'verify', 'wrap', 'unwrap'];
  }

  get disableOperations() {
    return (
      this.validMatchError ||
      !this.formData.provider ||
      !this.formData.key ||
      (this.isNewKey && !this.keyModel.type)
    );
  }

  async getKeyInfo(keyName, isNew = false) {
    let key;
    if (isNew) {
      this.isNewKey = true;
      key = this.store.createRecord(`keymgmt/key`, {
        backend: this.args.backend,
        id: keyName,
        name: keyName,
      });
    } else {
      key = await this.store
        .queryRecord(`keymgmt/key`, {
          backend: this.args.backend,
          id: keyName,
          recordOnly: true,
        })
        .catch(() => {
          // Key type isn't essential for distributing, so if
          // we can't read it for some reason swallow the error
          // and allow the API to respond with any key/provider
          // type matching errors
        });
    }
    this.keyModel = key;
  }

  async getProviderType(id) {
    if (!id) {
      this.providerType = '';
      return;
    }

    const provider = await this.store
      .queryRecord('keymgmt/provider', {
        backend: this.args.backend,
        id,
      })
      .catch(() => {});
    this.providerType = provider?.provider;
  }

  destroyKey() {
    if (this.isNewKey) {
      // Delete record from store if it was created here
      this.keyModel.destroyRecord().finally(() => {
        this.keyModel = null;
      });
    }
    this.isNewKey = false;
    this.keyModel = null;
  }

  /**
   *
   * @param {DistributionData} rawData
   * @returns POJO formatted how the distribution endpoint needs
   */
  formatData(rawData) {
    const { key, provider, operations, protection } = rawData;
    if (!key || !provider || !operations || operations.length === 0) return null;
    return { key, provider, purpose: operations.join(','), protection };
  }

  distributeKey(backend, data) {
    const adapter = this.store.adapterFor('keymgmt/key');
    const { key, provider, purpose, protection } = data;
    return adapter
      .distribute(backend, provider, key, { purpose, protection })
      .then(() => {
        this.flashMessages.success(`Successfully distributed key ${key} to ${provider}`);
        // update keys on provider model
        this.store.clearDataset('keymgmt/key');
        const providerModel = this.store.peekRecord('keymgmt/provider', provider);
        providerModel.fetchKeys(providerModel.keys?.meta?.currentPage || 1);
        this.args.onClose();
      })
      .catch((e) => {
        this.formErrors = `${e.errors}`;
      });
  }

  @action
  handleProvider(selection) {
    let providerName = selection[0];
    if (typeof selection === 'string') {
      // Handles case if no list permissions and fallback component is used
      providerName = selection;
    }
    this.formData.provider = providerName;
    if (providerName) {
      this.getProviderType(providerName);
    }
  }
  @action
  handleKeyType(evt) {
    this.keyModel.set('type', evt.target.value);
  }

  @action
  handleOperation(evt) {
    const ops = [...this.formData.operations];
    if (evt.target.checked) {
      ops.push(evt.target.id);
    } else {
      const idx = ops.indexOf(evt.target.id);
      ops.splice(idx, 1);
    }
    this.formData.operations = ops;
  }

  @action
  async handleKeySelect(selected) {
    const selectedKey = selected[0] || null;
    if (!selectedKey) {
      this.formData.key = null;
      return this.destroyKey();
    }
    this.formData.key = selectedKey.id;
    return this.getKeyInfo(selectedKey.id, selectedKey.isNew);
  }

  @task
  @waitFor
  *createDistribution(evt) {
    evt.preventDefault();
    const { backend } = this.args;
    const data = this.formatData(this.formData);
    if (!data) {
      this.flashMessages.danger(`Key, provider, and operations are all required`);
      return;
    }
    if (this.isNewKey) {
      try {
        yield this.keyModel.save();
        this.flashMessages.success(`Successfully created key ${this.keyModel.name}`);
      } catch (e) {
        this.flashMessages.danger(`Error creating new key ${this.keyModel.name}: ${e.errors}`);
        return;
      }
    }
    yield this.distributeKey(backend, data);
    // Reload key to get dist info
    yield this.store.queryRecord(`keymgmt/key`, {
      backend: this.args.backend,
      id: this.keyModel.name,
    });
  }
}
