"use strict";

var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
Object.defineProperty(exports, "__esModule", {
  value: true
});
exports.AlertsClient = void 0;
var _defineProperty2 = _interopRequireDefault(require("@babel/runtime/helpers/defineProperty"));
var _ruleDataUtils = require("@kbn/rule-data-utils");
var _lodash = require("lodash");
var _coreSavedObjectsUtilsServer = require("@kbn/core-saved-objects-utils-server");
var _legacy_alerts_client = require("./legacy_alerts_client");
var _resource_installer_utils = require("../alerts_service/resource_installer_utils");
var _lib = require("./lib");
var _alerts_service = require("../alerts_service");
var _alert_conflict_resolver = require("./lib/alert_conflict_resolver");
/*
 * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
 * or more contributor license agreements. Licensed under the Elastic License
 * 2.0; you may not use this file except in compliance with the Elastic License
 * 2.0.
 */

// Term queries can take up to 10,000 terms
const CHUNK_SIZE = 10000;
class AlertsClient {
  // Query for alerts from the previous execution in order to identify the
  // correct index to use if and when we need to make updates to existing active or
  // recovered alerts
  constructor(options) {
    var _this$options$ruleTyp, _this$options$ruleTyp2;
    (0, _defineProperty2.default)(this, "legacyAlertsClient", void 0);
    (0, _defineProperty2.default)(this, "fetchedAlerts", void 0);
    (0, _defineProperty2.default)(this, "rule", void 0);
    (0, _defineProperty2.default)(this, "ruleType", void 0);
    (0, _defineProperty2.default)(this, "indexTemplateAndPattern", void 0);
    (0, _defineProperty2.default)(this, "reportedAlerts", {});
    (0, _defineProperty2.default)(this, "_isUsingDataStreams", void 0);
    this.options = options;
    this.legacyAlertsClient = new _legacy_alerts_client.LegacyAlertsClient({
      logger: this.options.logger,
      ruleType: this.options.ruleType
    });
    this.indexTemplateAndPattern = (0, _resource_installer_utils.getIndexTemplateAndPattern)({
      context: (_this$options$ruleTyp = this.options.ruleType.alerts) === null || _this$options$ruleTyp === void 0 ? void 0 : _this$options$ruleTyp.context,
      namespace: (_this$options$ruleTyp2 = this.options.ruleType.alerts) !== null && _this$options$ruleTyp2 !== void 0 && _this$options$ruleTyp2.isSpaceAware ? this.options.namespace : _coreSavedObjectsUtilsServer.DEFAULT_NAMESPACE_STRING
    });
    this.fetchedAlerts = {
      indices: {},
      data: {},
      seqNo: {},
      primaryTerm: {}
    };
    this.rule = (0, _lib.formatRule)({
      rule: this.options.rule,
      ruleType: this.options.ruleType
    });
    this.ruleType = options.ruleType;
    this._isUsingDataStreams = this.options.dataStreamAdapter.isUsingDataStreams();
  }
  async initializeExecution(opts) {
    var _this$ruleType$alerts;
    await this.legacyAlertsClient.initializeExecution(opts);
    if (!((_this$ruleType$alerts = this.ruleType.alerts) !== null && _this$ruleType$alerts !== void 0 && _this$ruleType$alerts.shouldWrite)) {
      return;
    }
    // Get tracked alert UUIDs to query for
    // TODO - we can consider refactoring to store the previous execution UUID and query
    // for active and recovered alerts from the previous execution using that UUID
    const trackedAlerts = this.legacyAlertsClient.getTrackedAlerts();
    const uuidsToFetch = [];
    (0, _lodash.keys)(trackedAlerts).forEach(key => {
      const tkey = key;
      (0, _lodash.keys)(trackedAlerts[tkey]).forEach(alertId => {
        uuidsToFetch.push(trackedAlerts[tkey][alertId].getUuid());
      });
    });
    if (!uuidsToFetch.length) {
      return;
    }
    const queryByUuid = async uuids => {
      const result = await this.search({
        size: uuids.length,
        seq_no_primary_term: true,
        query: {
          bool: {
            filter: [{
              term: {
                [_ruleDataUtils.ALERT_RULE_UUID]: this.options.rule.id
              }
            }, {
              terms: {
                [_ruleDataUtils.ALERT_UUID]: uuids
              }
            }]
          }
        }
      });
      return result.hits;
    };
    try {
      const results = await Promise.all((0, _lodash.chunk)(uuidsToFetch, CHUNK_SIZE).map(uuidChunk => queryByUuid(uuidChunk)));
      for (const hit of results.flat()) {
        const alertHit = hit._source;
        const alertUuid = (0, _lodash.get)(alertHit, _ruleDataUtils.ALERT_UUID);
        const alertId = (0, _lodash.get)(alertHit, _ruleDataUtils.ALERT_INSTANCE_ID);

        // Keep track of existing alert document so we can copy over data if alert is ongoing
        this.fetchedAlerts.data[alertId] = alertHit;

        // Keep track of index so we can update the correct document
        this.fetchedAlerts.indices[alertUuid] = hit._index;
        this.fetchedAlerts.seqNo[alertUuid] = hit._seq_no;
        this.fetchedAlerts.primaryTerm[alertUuid] = hit._primary_term;
      }
    } catch (err) {
      this.options.logger.error(`Error searching for tracked alerts by UUID - ${err.message}`);
    }
  }
  async search(queryBody) {
    const esClient = await this.options.elasticsearchClientPromise;
    const index = this.isUsingDataStreams() ? this.indexTemplateAndPattern.alias : this.indexTemplateAndPattern.pattern;
    const {
      hits: {
        hits,
        total
      }
    } = await esClient.search({
      index,
      body: queryBody,
      ignore_unavailable: true
    });
    return {
      hits,
      total
    };
  }
  report(alert) {
    const context = alert.context ? alert.context : {};
    const state = !(0, _lodash.isEmpty)(alert.state) ? alert.state : null;

    // Create a legacy alert
    const legacyAlert = this.legacyAlertsClient.factory().create(alert.id).scheduleActions(alert.actionGroup, context);
    if (state) {
      legacyAlert.replaceState(state);
    }

    // Save the alert payload
    if (alert.payload) {
      this.reportedAlerts[alert.id] = alert.payload;
    }
    return {
      uuid: legacyAlert.getUuid(),
      start: legacyAlert.getStart()
    };
  }
  setAlertData(alert) {
    const context = alert.context ? alert.context : {};

    // Allow setting context and payload on known alerts only
    // Alerts are known if they have been reported in this execution or are recovered
    const alertToUpdate = this.legacyAlertsClient.getAlert(alert.id);
    if (!alertToUpdate) {
      throw new Error(`Cannot set alert data for alert ${alert.id} because it has not been reported and it is not recovered.`);
    }

    // Set the alert context
    alertToUpdate.setContext(context);

    // Save the alert payload
    if (alert.payload) {
      this.reportedAlerts[alert.id] = alert.payload;
    }
  }
  hasReachedAlertLimit() {
    return this.legacyAlertsClient.hasReachedAlertLimit();
  }
  checkLimitUsage() {
    return this.legacyAlertsClient.checkLimitUsage();
  }
  processAndLogAlerts(opts) {
    this.legacyAlertsClient.processAndLogAlerts(opts);
  }
  getProcessedAlerts(type) {
    return this.legacyAlertsClient.getProcessedAlerts(type);
  }
  async persistAlerts() {
    var _this$ruleType$alerts2;
    if (!((_this$ruleType$alerts2 = this.ruleType.alerts) !== null && _this$ruleType$alerts2 !== void 0 && _this$ruleType$alerts2.shouldWrite)) {
      var _this$ruleType$alerts3;
      this.options.logger.debug(`Resources registered and installed for ${(_this$ruleType$alerts3 = this.ruleType.alerts) === null || _this$ruleType$alerts3 === void 0 ? void 0 : _this$ruleType$alerts3.context} context but "shouldWrite" is set to false.`);
      return;
    }
    const currentTime = new Date().toISOString();
    const esClient = await this.options.elasticsearchClientPromise;
    const {
      alertsToReturn,
      recoveredAlertsToReturn
    } = this.legacyAlertsClient.getAlertsToSerialize(false);
    const activeAlerts = this.legacyAlertsClient.getProcessedAlerts('active');
    const recoveredAlerts = this.legacyAlertsClient.getProcessedAlerts('recovered');

    // TODO - Lifecycle alerts set some other fields based on alert status
    // Example: workflow status - default to 'open' if not set
    // event action: new alert = 'new', active alert: 'active', otherwise 'close'

    const activeAlertsToIndex = [];
    for (const id of (0, _lodash.keys)(alertsToReturn)) {
      // See if there's an existing active alert document
      if (!!activeAlerts[id]) {
        if (this.fetchedAlerts.data.hasOwnProperty(id) && (0, _lodash.get)(this.fetchedAlerts.data[id], _ruleDataUtils.ALERT_STATUS) === 'active') {
          activeAlertsToIndex.push((0, _lib.buildOngoingAlert)({
            alert: this.fetchedAlerts.data[id],
            legacyAlert: activeAlerts[id],
            rule: this.rule,
            timestamp: currentTime,
            payload: this.reportedAlerts[id],
            kibanaVersion: this.options.kibanaVersion
          }));
        } else {
          activeAlertsToIndex.push((0, _lib.buildNewAlert)({
            legacyAlert: activeAlerts[id],
            rule: this.rule,
            timestamp: currentTime,
            payload: this.reportedAlerts[id],
            kibanaVersion: this.options.kibanaVersion
          }));
        }
      } else {
        this.options.logger.error(`Error writing alert(${id}) to ${this.indexTemplateAndPattern.alias} - alert(${id}) doesn't exist in active alerts`);
      }
    }
    const recoveredAlertsToIndex = [];
    for (const id of (0, _lodash.keys)(recoveredAlertsToReturn)) {
      // See if there's an existing alert document
      // If there is not, log an error because there should be
      if (this.fetchedAlerts.data.hasOwnProperty(id)) {
        recoveredAlertsToIndex.push(recoveredAlerts[id] ? (0, _lib.buildRecoveredAlert)({
          alert: this.fetchedAlerts.data[id],
          legacyAlert: recoveredAlerts[id],
          rule: this.rule,
          timestamp: currentTime,
          payload: this.reportedAlerts[id],
          recoveryActionGroup: this.options.ruleType.recoveryActionGroup.id,
          kibanaVersion: this.options.kibanaVersion
        }) : (0, _lib.buildUpdatedRecoveredAlert)({
          alert: this.fetchedAlerts.data[id],
          legacyRawAlert: recoveredAlertsToReturn[id],
          timestamp: currentTime,
          rule: this.rule
        }));
      } else {
        this.options.logger.debug(`Could not find alert document to update for recovered alert with id ${id} and uuid ${recoveredAlerts[id].getUuid()}`);
      }
    }
    const alertsToIndex = [...activeAlertsToIndex, ...recoveredAlertsToIndex].filter(alert => {
      const alertUuid = (0, _lodash.get)(alert, _ruleDataUtils.ALERT_UUID);
      const alertIndex = this.fetchedAlerts.indices[alertUuid];
      if (!alertIndex) {
        return true;
      } else if (!(0, _alerts_service.isValidAlertIndexName)(alertIndex)) {
        this.options.logger.warn(`Could not update alert ${alertUuid} in ${alertIndex}. Partial and restored alert indices are not supported.`);
        return false;
      }
      return true;
    });
    if (alertsToIndex.length > 0) {
      const bulkBody = (0, _lodash.flatMap)(alertsToIndex.map(alert => {
        const alertUuid = (0, _lodash.get)(alert, _ruleDataUtils.ALERT_UUID);
        return [getBulkMeta(alertUuid, this.fetchedAlerts.indices[alertUuid], this.fetchedAlerts.seqNo[alertUuid], this.fetchedAlerts.primaryTerm[alertUuid], this.isUsingDataStreams()), alert];
      }));
      try {
        const response = await esClient.bulk({
          refresh: true,
          index: this.indexTemplateAndPattern.alias,
          require_alias: !this.isUsingDataStreams(),
          body: bulkBody
        });

        // If there were individual indexing errors, they will be returned in the success response
        if (response && response.errors) {
          await (0, _alert_conflict_resolver.resolveAlertConflicts)({
            logger: this.options.logger,
            esClient,
            bulkRequest: {
              refresh: 'wait_for',
              index: this.indexTemplateAndPattern.alias,
              require_alias: !this.isUsingDataStreams(),
              operations: bulkBody
            },
            bulkResponse: response
          });
        }
      } catch (err) {
        this.options.logger.error(`Error writing ${alertsToIndex.length} alerts to ${this.indexTemplateAndPattern.alias} - ${err.message}`);
      }
    }
    function getBulkMeta(uuid, index, seqNo, primaryTerm, isUsingDataStreams) {
      if (index && seqNo != null && primaryTerm != null) {
        return {
          index: {
            _id: uuid,
            _index: index,
            if_seq_no: seqNo,
            if_primary_term: primaryTerm,
            require_alias: false
          }
        };
      }
      return {
        create: {
          _id: uuid,
          ...(isUsingDataStreams ? {} : {
            require_alias: true
          })
        }
      };
    }
  }
  getAlertsToSerialize() {
    // The flapping value that is persisted inside the task manager state (and used in the next execution)
    // is different than the value that should be written to the alert document. For this reason, we call
    // getAlertsToSerialize() twice, once before building and bulk indexing alert docs and once after to return
    // the value for task state serialization

    // This will be a blocker if ever we want to stop serializing alert data inside the task state and just use
    // the fetched alert document.
    return this.legacyAlertsClient.getAlertsToSerialize();
  }
  factory() {
    return this.legacyAlertsClient.factory();
  }
  async getSummarizedAlerts({
    ruleId,
    spaceId,
    excludedAlertInstanceIds,
    alertsFilter,
    start,
    end,
    executionUuid
  }) {
    var _this$ruleType$alerts4, _this$ruleType$autoRe;
    if (!ruleId || !spaceId) {
      throw new Error(`Must specify both rule ID and space ID for AAD alert query.`);
    }
    const queryByExecutionUuid = !!executionUuid;
    const queryByTimeRange = !!start && !!end;
    // Either executionUuid or start/end dates must be specified, but not both
    if (!queryByExecutionUuid && !queryByTimeRange || queryByExecutionUuid && queryByTimeRange) {
      throw new Error(`Must specify either execution UUID or time range for AAD alert query.`);
    }
    const getQueryParams = {
      executionUuid,
      start,
      end,
      ruleId,
      excludedAlertInstanceIds,
      alertsFilter
    };
    const formatAlert = (_this$ruleType$alerts4 = this.ruleType.alerts) === null || _this$ruleType$alerts4 === void 0 ? void 0 : _this$ruleType$alerts4.formatAlert;
    const isLifecycleAlert = (_this$ruleType$autoRe = this.ruleType.autoRecoverAlerts) !== null && _this$ruleType$autoRe !== void 0 ? _this$ruleType$autoRe : false;
    if (isLifecycleAlert) {
      const queryBodies = (0, _lib.getLifecycleAlertsQueries)(getQueryParams);
      const responses = await Promise.all(queryBodies.map(queryBody => this.search(queryBody)));
      return {
        new: (0, _lib.getHitsWithCount)(responses[0], formatAlert),
        ongoing: (0, _lib.getHitsWithCount)(responses[1], formatAlert),
        recovered: (0, _lib.getHitsWithCount)(responses[2], formatAlert)
      };
    }
    const response = await this.search((0, _lib.getContinualAlertsQuery)(getQueryParams));
    return {
      new: (0, _lib.getHitsWithCount)(response, formatAlert),
      ongoing: {
        count: 0,
        data: []
      },
      recovered: {
        count: 0,
        data: []
      }
    };
  }
  client() {
    return {
      report: alert => this.report(alert),
      setAlertData: alert => this.setAlertData(alert),
      getAlertLimitValue: () => this.factory().alertLimit.getValue(),
      setAlertLimitReached: reached => this.factory().alertLimit.setLimitReached(reached),
      getRecoveredAlerts: () => {
        var _getRecoveredAlerts;
        const {
          getRecoveredAlerts
        } = this.factory().done();
        const recoveredLegacyAlerts = (_getRecoveredAlerts = getRecoveredAlerts()) !== null && _getRecoveredAlerts !== void 0 ? _getRecoveredAlerts : [];
        return recoveredLegacyAlerts.map(alert => ({
          alert,
          hit: this.fetchedAlerts.data[alert.getId()]
        }));
      }
    };
  }
  isUsingDataStreams() {
    return this._isUsingDataStreams;
  }
}
exports.AlertsClient = AlertsClient;