<script>
import merge from 'lodash/merge';
import { ucFirst } from '@shell/utils/string';
import { isSimpleKeyValue, set } from '@shell/utils/object';
import { _CREATE, _VIEW } from '@shell/config/query-params';
import { SCHEMA, NAMESPACE } from '@shell/config/types';
import CreateEditView from '@shell/mixins/create-edit-view';
import NameNsDescription from '@shell/components/form/NameNsDescription';
import { RadioGroup } from '@components/Form/Radio';
import RuleSelector from '@shell/components/form/RuleSelector';
import Tab from '@shell/components/Tabbed/Tab';
import Tabbed from '@shell/components/Tabbed';
import YamlEditor, { EDITOR_MODES } from '@shell/components/YamlEditor';
import CruResource from '@shell/components/CruResource';
import { ENFORCEMENT_ACTION_VALUES } from '@shell/models/constraints.gatekeeper.sh.constraint';
import { saferDump } from '@shell/utils/create-yaml';
import NamespaceList, { NAMESPACE_FILTERS_HELPER } from './NamespaceList';
import MatchKinds from './MatchKinds';
import Scope, { SCOPE_OPTIONS } from './Scope';
import Banner from '@components/Banner/Banner';

function findConstraintTypes(schemas) {
  return schemas
    .filter((schema) => schema?.attributes?.group === 'constraints.gatekeeper.sh');
}

function findConstraintTypesIds(schemas) {
  return findConstraintTypes(schemas)
    .map((schema) => schema.id);
}

const CONSTRAINT_PREFIX = 'constraints.gatekeeper.sh.';

export default {
  emits: ['update:value', 'set-subtype'],

  components: {
    CruResource,
    MatchKinds,
    NameNsDescription,
    NamespaceList,
    RuleSelector,
    RadioGroup,
    Scope,
    Tab,
    Tabbed,
    YamlEditor,
    Banner
  },

  mixins: [CreateEditView],

  inheritAttrs: false,

  props: {
    value: {
      type:     Object,
      required: true
    }
  },

  data() {
    const emptySpec = {
      enforcementAction: ENFORCEMENT_ACTION_VALUES.DENY,
      parameters:        {},
      match:             {
        kinds:              [{}],
        namespaces:         [],
        excludedNamespaces: [],
        labelSelector:      { matchExpressions: [] },
        namespaceSelector:  { matchExpressions: [] }
      }
    };

    if (this.mode === _CREATE) {
      this.value['spec'] = merge(this.value.spec, emptySpec);

      if (!this.value.spec.match.scope) {
        this.value.spec.match['scope'] = SCOPE_OPTIONS[0].value;
      }
    } else {
      this.value.spec['match'] = this.value.spec.match || {};
      this.value.spec.match['kinds'] = this.value.spec.match.kinds || [{}];
      this.value.spec.match['labelSelector'] = this.value.spec.match.labelSelector || {};
      this.value.spec.match.labelSelector['matchExpressions'] = this.value.spec.match.labelSelector.matchExpressions || [];
      this.value.spec.match['namespaceSelector'] = this.value.spec.match.namespaceSelector || {};
      this.value.spec.match.namespaceSelector['labelSelector'] = this.value.spec.match.namespaceSelector.labelSelector || [];
    }

    const parametersYaml = saferDump(this.value?.spec?.parameters);

    return {
      emptySpec,
      parametersYaml,
      enforcementActionOptions: [
        {
          label: `${ ENFORCEMENT_ACTION_VALUES.DENY } - deny admission requests with any violation`,
          value: ENFORCEMENT_ACTION_VALUES.DENY
        },
        {
          label: `${ ENFORCEMENT_ACTION_VALUES.DRYRUN } - enables constraints to be deployed in the cluster without making actual changes`,
          value: ENFORCEMENT_ACTION_VALUES.DRYRUN
        }
      ],
      enforcementActionLabels: Object.values(ENFORCEMENT_ACTION_VALUES).map(ucFirst),
      NAMESPACE_FILTERS_HELPER,
    };
  },

  computed: {
    templateOptions() {
      const schemas = this.$store.getters['cluster/all'](SCHEMA);
      const constraintTypes = findConstraintTypesIds(schemas);

      constraintTypes.sort();

      return constraintTypes.map((type) => {
        return {
          label: type.replace(CONSTRAINT_PREFIX, ''),
          value: type
        };
      });
    },
    templateSubtypes() {
      const schemas = this.$store.getters['cluster/all'](SCHEMA);
      const constraintTypes = findConstraintTypes(schemas);

      constraintTypes.sort();

      return constraintTypes.map((type) => {
        const splitId = type.id.split('.');

        return {
          label:       type.id.replace(CONSTRAINT_PREFIX, ''),
          description: '',
          id:          splitId[splitId.length - 1],
          bannerAbbrv: 'CT'
        };
      });
    },
    editorMode() {
      if ( this.mode === _VIEW ) {
        return EDITOR_MODES.VIEW_CODE;
      } else {
        return EDITOR_MODES.EDIT_CODE;
      }
    },
    canShowForm() {
      return this.value?.spec?.parameters && isSimpleKeyValue(this.value.spec.parameters) && !this.isView;
    },
    systemNamespaceIds() {
      return this.$store.getters['cluster/all'](NAMESPACE)
        .filter((namespace) => namespace.isSystem)
        .map((namespace) => namespace.id);
    },
    emptyDefaults() {
      return {
        type: this.templateOptions[0].value,
        spec: this.emptySpec
      };
    },
    isTemplateSelectorDisabled() {
      return !this.isCreate;
    },
    showNamespaceLists() {
      return this.value.spec.match.scope !== 'Cluster';
    }
  },

  watch: {
    value: {
      handler(value) {
        // We have to set the type for the CreateEditView mixin to know what the type is when creating
        this.type = value.type;
        this.purgeNamespacesField(this.value);
      },
      deep: true
    },
  },

  created() {
    this.registerBeforeHook(this.willSave, 'willSave');
    if (!this.value.save) {
      this.$emit('update:value', merge(this.value, this.emptyDefaults));
    }
  },

  methods: {
    willSave() {
      this.value.spec.match.kinds.forEach((kind) => {
        const apiGroups = kind.apiGroups || [];

        if (apiGroups.length === 0) {
          kind.apiGroups = ['*'];
        }
      });
    },
    /**
     * There's an upstream issue which prevents gatekeeper from processing namespaces with empty lists incorrectly.
     * We need to remove the namespaces field if it's empty.
     * https://github.com/open-policy-agent/gatekeeper/issues/508
     */
    purgeNamespacesField(value) {
      if (value?.spec?.match?.namespaces && (value.spec.match.namespaces.length === 0)) {
        delete value.spec.match['namespaces'];
      }
    },

    updateType(type) {
      this.value['type'] = type;
    },
    onTabChanged({ tab }) {
      // This is necessary to force the yamlEditor to adjust the size once it has space to fill.
      if (tab.name === 'parameters' && this.$refs.yamlEditor?.refresh) {
        this.$refs.yamlEditor.refresh();
      }
    },
    selectTemplateSubtype(subType) {
      this.value['kind'] = subType;
      this.$emit('set-subtype', subType);
    },
    onScopeChange(newScope) {
      if (newScope === 'Cluster') {
        this.value.spec.match.namespaces = [];
        this.value.spec.match.excludedNamespaces = [];
      }
    },
    setParameters(e) {
      return set(this.value.spec, 'parameters', e);
    }
  }
};
</script>
<template>
  <Banner
    v-if="templateSubtypes.length === 0"
    color="warning"
  >
    {{ t('gatekeeperConstraint.templateRequired') }}
  </Banner>
  <CruResource
    v-else
    :done-route="doneRoute"
    :mode="mode"
    :resource="value"
    :subtypes="templateSubtypes"
    :validation-passed="true"
    :errors="errors"
    @error="e=>errors = e"
    @finish="save"
    @select-type="selectTemplateSubtype"
  >
    <div
      v-if="value"
      class="gatekeeper-constraint"
    >
      <div>
        <NameNsDescription
          v-if="!isView"
          :value="value"
          :mode="mode"
          :namespaced="false"
        />
      </div>
      <div class="row mb-40">
        <div class="col span-12">
          <h3>Enforcement Action</h3>
          <RadioGroup
            v-model:value="value.spec.enforcementAction"
            name="enforcementAction"
            class="enforcement-action"
            :options="enforcementActionOptions"
            :labels="enforcementActionLabels"
            :mode="mode"
          />
        </div>
      </div>
      <Tabbed
        :side-tabs="true"
        :use-hash="useTabbedHash"
        @changed="onTabChanged"
      >
        <Tab
          name="namespaces"
          :label="t('gatekeeperConstraint.tab.namespaces.title')"
          :weight="3"
        >
          <div class="row">
            <div class="col span-6">
              <Scope
                v-model:value="value.spec.match.scope"
                :mode="mode"
                @update:value="onScopeChange($event)"
              />
            </div>
          </div>
          <div
            v-if="showNamespaceLists"
            class="row mt-20"
          >
            <div class="col span-12">
              <NamespaceList
                v-model:value="value.spec.match.namespaces"
                :label="t('gatekeeperConstraint.tab.namespaces.sub.namespaces')"
                tooltip="If defined, a constraint will only apply to resources in a listed namespace."
                :mode="mode"
                :namespace-filter="NAMESPACE_FILTERS_HELPER.nonSystem"
                add-label="Add Namespace"
              />
            </div>
          </div>
          <div
            v-if="showNamespaceLists"
            class="row mt-20"
          >
            <div class="col span-12">
              <NamespaceList
                v-model:value="value.spec.match.excludedNamespaces"
                :label="t('gatekeeperConstraint.tab.namespaces.sub.excludedNamespaces')"
                tooltip="If defined, a constraint will only apply to resources not in a listed namespace."
                :mode="mode"
                add-label="Add Excluded Namespace"
              />
            </div>
          </div>
          <div class="row mt-40">
            <div class="col span-12">
              <h3>{{ t('gatekeeperConstraint.tab.namespaces.sub.namespaceSelector.title') }}</h3>
              <RuleSelector
                v-model:value="value.spec.match.namespaceSelector.matchExpressions"
                add-label="Add Namespace Selector"
                :mode="mode"
              />
            </div>
          </div>
        </Tab>
        <Tab
          name="rules"
          :label="t('gatekeeperConstraint.tab.rules.title')"
          :weight="2"
        >
          <div class="row">
            <div class="col span-12">
              <MatchKinds
                v-model:value="value.spec.match.kinds"
                :mode="mode"
              />
            </div>
          </div>
          <div class="row mt-40">
            <div class="col span-12">
              <h3>{{ t('gatekeeperConstraint.tab.rules.sub.labelSelector.title') }}</h3>
              <RuleSelector
                v-model:value="value.spec.match.labelSelector.matchExpressions"
                :add-label="t('gatekeeperConstraint.tab.rules.sub.labelSelector.addLabel')"
                :mode="mode"
              />
            </div>
          </div>
        </Tab>
        <Tab
          name="parameters"
          :label="t('gatekeeperConstraint.tab.parameters.title')"
          :weight="1"
        >
          <YamlEditor
            ref="yamlEditor"
            v-model:value="parametersYaml"
            class="yaml-editor"
            :editor-mode="editorMode"
            @newObject="setParameters"
          />
        </Tab>
      </Tabbed>
    </div>
  </CruResource>
</template>
