/* eslint-disable guard-for-in, default-case, no-case-declarations */

/**
 * Converted from Angular based  "kubernetes-label-selector"
 *    git://github.com/kubernetes-ui/label-selector.git
 *    author: Jessica Forrester <jforrest@redhat.com>
 **/
import * as _ from 'lodash-es';
// selector (optional) - the JSON format as returned by k8s API, will also
//      handle {key: null} as the key exists operator (not currently returned
//      by API)
// emptySelectsAll (optional) - whether a label selector with no conjuncts
//      selects objects.  Typical behavior is false.  Example of an
//      exceptional case is when filtering by labels, no label selectors
//      means no filters.
export class LabelSelector {
  constructor(selector, emptySelectsAll) {
    this._conjuncts = {};
    this._emptySelectsAll = !!emptySelectsAll;
    // expects the JSON format as returned by k8s API
    // Supports both the old selector syntax of just key: value pairs like on RCs
    // as well as the new matchLabel and matchExpression syntax on newer controllers like ReplicaSets
    // For now it will also handle key: null as key exists for backwards compatibility from before
    // the matchExpression support was added.
    this._OPERATOR_MAP = {
      In: 'in',
      NotIn: 'not in',
      Exists: 'exists',
      DoesNotExist: 'does not exist',
    };
    this._REVERSE_OPERATOR_MAP = {
      in: 'In',
      'not in': 'NotIn',
      exists: 'Exists',
      'does not exist': 'DoesNotExist',
    };
    if (selector) {
      if (selector.matchLabels || selector.matchExpressions) {
        _.forEach(
          selector.matchLabels,
          (details, key) => {
            this.addConjunct(key, 'in', [details]);
          },
          this,
        );
        _.forEach(
          selector.matchExpressions,
          (expression) => {
            this.addConjunct(
              expression.key,
              this._OPERATOR_MAP[expression.operator],
              expression.values,
            );
          },
          this,
        );
      } else {
        _.forEach(
          selector,
          (details, key) => {
            if (details || details === '') {
              this.addConjunct(key, 'in', [details]);
            } else {
              this.addConjunct(key, 'exists', []);
            }
          },
          this,
        );
      }
    }
  }

  addConjunct(key, operator, values) {
    const conjunct = {
      key,
      operator,
      values,
    };
    const id = this._getIdForConjunct(conjunct);
    this._conjuncts[id] = conjunct;
    conjunct.id = id;
    conjunct.string = this._getStringForConjunct(conjunct);
    return conjunct;
  }

  // Can accept either the id of the conjunct to remove, or the conjunct
  // object that was returned from a call to addConjunct
  removeConjunct(conjunct) {
    if (conjunct.id) {
      delete this._conjuncts[conjunct.id];
    } else {
      delete this._conjuncts[conjunct];
    }
  }
  clearConjuncts() {
    this._conjuncts = {};
  }
  isEmpty() {
    return _.isEmpty(this._conjuncts);
  }
  each(fn) {
    _.forEach(this._conjuncts, fn);
  }
  select(resources) {
    // If passed an array, return an array.
    if (_.isArray(resources)) {
      return _.filter(resources, _.bind(this.matches, this));
    }
    // Otherwise handle it as a map.
    return _.pickBy(resources, _.bind(this.matches, this));
  }
  matches(resource) {
    if (!resource) {
      return false;
    }
    if (this.isEmpty()) {
      return this._emptySelectsAll;
    }
    const labels = resource.metadata.labels || {};
    for (const id in this._conjuncts) {
      const conjunct = this._conjuncts[id];
      switch (conjunct.operator) {
        case 'exists':
          if (!labels[conjunct.key] && labels[conjunct.key] !== '') {
            return false;
          }
          break;
        case 'does not exist':
          if (labels[conjunct.key] || labels[conjunct.key] === '') {
            return false;
          }
          break;
        case 'in':
          let found = false;
          if (labels[conjunct.key] || labels[conjunct.key] === '') {
            for (let i = 0; !found && i < conjunct.values.length; i++) {
              if (labels[conjunct.key] === conjunct.values[i]) {
                found = true;
              }
            }
          }
          if (!found) {
            return false;
          }
          break;
        case 'not in':
          let keep = true;
          if (labels[conjunct.key]) {
            for (let i = 0; keep && i < conjunct.values.length; i++) {
              keep = labels[conjunct.key] !== conjunct.values[i];
            }
          }
          if (!keep) {
            return false;
          }
      }
    }
    return true;
  }
  hasConjunct(conjunct) {
    return !!this._conjuncts[this._getIdForConjunct(conjunct)];
  }
  findConjunctsMatching(operator, key) {
    return _.pickBy(
      this._conjuncts,
      _.matches({
        operator,
        key,
      }),
    );
  }
  // Test whether this label selector covers the given selector
  covers(selector) {
    if (this.isEmpty()) {
      // TODO don't think we ever want to consider an empty
      // label selector as covering any other label selector
      return false;
    }
    return _.every(this._conjuncts, function(conjunct) {
      // Return true immediately if we find an exact match for operator/key/values
      if (selector.hasConjunct(conjunct)) {
        return true;
      }
      // If we can't find a conjunct that matches exactly, do a more detailed check
      switch (conjunct.operator) {
        case 'exists':
          // If an Exists conjunct existed for the same key in selector it
          // would have passed the exact match, just need to check if an In
          // conjunct exists for the same key
          return !_.isEmpty(selector.findConjunctsMatching('in', conjunct.key));
        case 'does not exist':
          // A DoesNotExist can only cover a DoesNotExist operator, if we got here
          // then we didn't have a DNE with the same key so we know we can't cover
          return false;
        case 'in':
          // In (A,B,C) covers In (A,B) AND In (B,C)
          const inConjuncts = selector.findConjunctsMatching('in', conjunct.key);
          if (_.isEmpty(inConjuncts)) {
            return false;
          }
          return _.every(inConjuncts, function(inConjunct) {
            return (
              inConjunct.values.length === _.intersection(inConjunct.values, conjunct.values).length
            );
          });
        case 'not in':
          // NotIn (A,B) covers NotIn (A,B,C) AND NotIn (A,B,D)
          const notInConjuncts = selector.findConjunctsMatching('not in', conjunct.key);
          if (_.isEmpty(notInConjuncts)) {
            return false;
          }
          return _.every(notInConjuncts, function(notInConjunct) {
            return (
              conjunct.values.length ===
              _.intersection(notInConjunct.values, conjunct.values).length
            );
          });
      }
      return true;
    });
  }

  // Exports the labelSelector as a string in the API format, exports as matchExpressions
  exportJSON() {
    const result = {
      matchExpressions: [],
    };
    for (const id in this._conjuncts) {
      const conjunct = this._conjuncts[id];
      const expression = {
        key: conjunct.key,
        operator: this._REVERSE_OPERATOR_MAP[conjunct.operator],
        values: conjunct.values,
      };
      result.matchExpressions.push(expression);
    }
    return JSON.stringify(result);
  }

  // We assume label values have no whitespace, commas, parens, etc. based
  // on k8s def for label values
  _getStringForConjunct(conjunct) {
    let conjunctString = conjunct.key;
    if (conjunct.operator === 'exists') {
      return `${conjunctString} exists`;
    } else if (conjunct.operator === 'does not exist') {
      return `${conjunctString} does not exist`;
    }
    if (conjunct.operator === 'not in') {
      conjunctString += ' not';
    }
    conjunctString += ' in (';
    for (let i = 0; i < conjunct.values.length; i++) {
      if (conjunct.values[i] === '') {
        conjunctString += '""';
      } else {
        conjunctString += conjunct.values[i];
      }
      if (i !== conjunct.values.length - 1) {
        conjunctString += ', ';
      }
    }
    conjunctString += ')';

    return conjunctString;
  }

  _getIdForConjunct(conjunct) {
    let id = `${conjunct.key}-${conjunct.operator}`;
    if (conjunct.values) {
      id += `-${conjunct.values.join(',')}`;
    }
    return id;
  }
}
