"use strict";

var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
Object.defineProperty(exports, "__esModule", {
  value: true
});
exports.getActionStatuses = getActionStatuses;
exports.getCancelledActions = getCancelledActions;
exports.hasRolloutPeriodPassed = void 0;
var _moment = _interopRequireDefault(require("moment"));
var _constants = require("../../constants");
var _common = require("../../../common");
var _ = require("..");
/*
 * 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.
 */

/**
 * Return current bulk actions
 */
async function getActionStatuses(esClient, options) {
  const actions = await _getActions(esClient, options);
  const cancelledActions = await getCancelledActions(esClient);
  let acks;
  try {
    acks = await esClient.search({
      index: _common.AGENT_ACTIONS_RESULTS_INDEX,
      query: {
        bool: {
          // There's some perf/caching advantages to using filter over must
          // See https://www.elastic.co/guide/en/elasticsearch/reference/current/query-filter-context.html#filter-context
          filter: [{
            terms: {
              action_id: actions.map(a => a.actionId)
            }
          }]
        }
      },
      size: 0,
      aggs: {
        ack_counts: {
          terms: {
            field: 'action_id',
            size: actions.length || 10
          },
          aggs: {
            max_timestamp: {
              max: {
                field: '@timestamp'
              }
            }
          }
        }
      }
    });
  } catch (err) {
    if (err.statusCode === 404) {
      // .fleet-actions-results does not yet exist
      _.appContextService.getLogger().debug(err);
    } else {
      throw err;
    }
  }
  const results = [];
  for (const action of actions) {
    var _acks, _acks$aggregations, _acks$aggregations$ac, _acks$aggregations$ac2, _matchingBucket$doc_c, _matchingBucket$max_t;
    const matchingBucket = (_acks = acks) === null || _acks === void 0 ? void 0 : (_acks$aggregations = _acks.aggregations) === null || _acks$aggregations === void 0 ? void 0 : (_acks$aggregations$ac = _acks$aggregations.ack_counts) === null || _acks$aggregations$ac === void 0 ? void 0 : (_acks$aggregations$ac2 = _acks$aggregations$ac.buckets) === null || _acks$aggregations$ac2 === void 0 ? void 0 : _acks$aggregations$ac2.find(bucket => bucket.key === action.actionId);
    const nbAgentsActioned = action.nbAgentsActioned || action.nbAgentsActionCreated;
    const docCount = (_matchingBucket$doc_c = matchingBucket === null || matchingBucket === void 0 ? void 0 : matchingBucket.doc_count) !== null && _matchingBucket$doc_c !== void 0 ? _matchingBucket$doc_c : 0;
    const nbAgentsAck = Math.min(docCount, nbAgentsActioned);
    const completionTime = matchingBucket === null || matchingBucket === void 0 ? void 0 : (_matchingBucket$max_t = matchingBucket.max_timestamp) === null || _matchingBucket$max_t === void 0 ? void 0 : _matchingBucket$max_t.value_as_string;
    const complete = nbAgentsAck >= nbAgentsActioned;
    const cancelledAction = cancelledActions.find(a => a.actionId === action.actionId);
    let errorCount = 0;
    let latestErrors = [];
    try {
      var _ref, _hits$hits, _errorResults$aggrega, _errorResults$aggrega2;
      // query to find errors in action results, cannot do aggregation on text type
      const errorResults = await esClient.search({
        index: _common.AGENT_ACTIONS_RESULTS_INDEX,
        track_total_hits: true,
        rest_total_hits_as_int: true,
        query: {
          bool: {
            must: [{
              term: {
                action_id: action.actionId
              }
            }],
            should: [{
              exists: {
                field: 'error'
              }
            }],
            minimum_should_match: 1
          }
        },
        size: 0,
        aggs: {
          top_error_hits: {
            top_hits: {
              sort: [{
                '@timestamp': {
                  order: 'desc'
                }
              }],
              _source: {
                includes: ['@timestamp', 'agent_id', 'error']
              },
              size: options.errorSize
            }
          }
        }
      });
      errorCount = (_ref = errorResults.hits.total) !== null && _ref !== void 0 ? _ref : 0;
      latestErrors = ((_hits$hits = (_errorResults$aggrega = errorResults.aggregations) === null || _errorResults$aggrega === void 0 ? void 0 : (_errorResults$aggrega2 = _errorResults$aggrega.top_error_hits) === null || _errorResults$aggrega2 === void 0 ? void 0 : _errorResults$aggrega2.hits.hits) !== null && _hits$hits !== void 0 ? _hits$hits : []).map(hit => ({
        agentId: hit._source.agent_id,
        error: hit._source.error,
        timestamp: hit._source['@timestamp']
      }));
      if (latestErrors.length > 0) {
        const hostNames = await getHostNames(esClient, latestErrors.map(errorItem => errorItem.agentId));
        latestErrors.forEach(errorItem => {
          var _hostNames$errorItem$;
          errorItem.hostname = (_hostNames$errorItem$ = hostNames[errorItem.agentId]) !== null && _hostNames$errorItem$ !== void 0 ? _hostNames$errorItem$ : errorItem.agentId;
        });
      }
    } catch (err) {
      if (err.statusCode === 404) {
        // .fleet-actions-results does not yet exist
        _.appContextService.getLogger().debug(err);
      } else {
        throw err;
      }
    }
    results.push({
      ...action,
      nbAgentsAck: nbAgentsAck - errorCount,
      nbAgentsFailed: errorCount,
      status: cancelledAction ? 'CANCELLED' : errorCount > 0 && complete ? 'FAILED' : complete ? 'COMPLETE' : action.status,
      nbAgentsActioned,
      cancellationTime: cancelledAction === null || cancelledAction === void 0 ? void 0 : cancelledAction.timestamp,
      completionTime,
      latestErrors
    });
  }
  const policyChangeActions = await getPolicyChangeActions(esClient);
  return [...results, ...policyChangeActions].sort((a, b) => b.creationTime > a.creationTime ? 1 : -1);
}
async function getHostNames(esClient, agentIds) {
  const agentsRes = await esClient.search({
    index: _common.AGENTS_INDEX,
    query: {
      bool: {
        filter: {
          terms: {
            'agent.id': agentIds
          }
        }
      }
    },
    size: agentIds.length,
    _source: ['local_metadata.host.name']
  });
  const hostNames = agentsRes.hits.hits.reduce((acc, curr) => {
    acc[curr._id] = curr._source.local_metadata.host.name;
    return acc;
  }, {});
  return hostNames;
}
async function getCancelledActions(esClient) {
  const res = await esClient.search({
    index: _common.AGENT_ACTIONS_INDEX,
    ignore_unavailable: true,
    size: _constants.SO_SEARCH_LIMIT,
    query: {
      bool: {
        filter: [{
          term: {
            type: 'CANCEL'
          }
        }]
      }
    }
  });
  return res.hits.hits.map(hit => {
    var _hit$_source, _hit$_source$data, _hit$_source2;
    return {
      actionId: (_hit$_source = hit._source) === null || _hit$_source === void 0 ? void 0 : (_hit$_source$data = _hit$_source.data) === null || _hit$_source$data === void 0 ? void 0 : _hit$_source$data.target_id,
      timestamp: (_hit$_source2 = hit._source) === null || _hit$_source2 === void 0 ? void 0 : _hit$_source2['@timestamp']
    };
  });
}
async function _getActions(esClient, options) {
  var _options$page, _options$perPage;
  const res = await esClient.search({
    index: _common.AGENT_ACTIONS_INDEX,
    ignore_unavailable: true,
    from: (_options$page = options.page) !== null && _options$page !== void 0 ? _options$page : 0,
    size: (_options$perPage = options.perPage) !== null && _options$perPage !== void 0 ? _options$perPage : 20,
    query: {
      bool: {
        must_not: [{
          term: {
            type: 'CANCEL'
          }
        }]
      }
    },
    body: {
      sort: [{
        '@timestamp': 'desc'
      }]
    }
  });
  return Object.values(res.hits.hits.reduce((acc, hit) => {
    var _hit$_source$agents$l, _hit$_source$agents;
    if (!hit._source || !hit._source.action_id) {
      return acc;
    }
    const source = hit._source;
    if (!acc[source.action_id]) {
      var _hit$_source$data2, _source$total, _source$data;
      const isExpired = source.expiration && source.type !== 'UPGRADE' ? Date.parse(source.expiration) < Date.now() : false;
      acc[hit._source.action_id] = {
        actionId: hit._source.action_id,
        nbAgentsActionCreated: 0,
        nbAgentsAck: 0,
        version: (_hit$_source$data2 = hit._source.data) === null || _hit$_source$data2 === void 0 ? void 0 : _hit$_source$data2.version,
        startTime: source.start_time,
        type: source.type,
        nbAgentsActioned: (_source$total = source.total) !== null && _source$total !== void 0 ? _source$total : 0,
        status: isExpired ? 'EXPIRED' : hasRolloutPeriodPassed(source) ? 'ROLLOUT_PASSED' : 'IN_PROGRESS',
        expiration: source.expiration,
        newPolicyId: (_source$data = source.data) === null || _source$data === void 0 ? void 0 : _source$data.policy_id,
        creationTime: source['@timestamp'],
        nbAgentsFailed: 0,
        hasRolloutPeriod: !!source.rollout_duration_seconds
      };
    }
    acc[hit._source.action_id].nbAgentsActionCreated += (_hit$_source$agents$l = (_hit$_source$agents = hit._source.agents) === null || _hit$_source$agents === void 0 ? void 0 : _hit$_source$agents.length) !== null && _hit$_source$agents$l !== void 0 ? _hit$_source$agents$l : 0;
    return acc;
  }, {}));
}
const hasRolloutPeriodPassed = source => {
  var _source$start_time;
  return source.type === 'UPGRADE' && source.rollout_duration_seconds ? Date.now() > (0, _moment.default)((_source$start_time = source.start_time) !== null && _source$start_time !== void 0 ? _source$start_time : Date.now()).add(source.rollout_duration_seconds, 'seconds').valueOf() : false;
};
exports.hasRolloutPeriodPassed = hasRolloutPeriodPassed;
async function getPolicyChangeActions(esClient) {
  const latestAgentPoliciesRes = await esClient.search({
    index: _common.AGENT_POLICY_INDEX,
    size: 10,
    query: {
      bool: {
        filter: [{
          range: {
            revision_idx: {
              gt: 1
            }
          }
        }]
      }
    },
    sort: [{
      '@timestamp': {
        order: 'desc'
      }
    }],
    _source: ['revision_idx', '@timestamp', 'policy_id']
  });
  const latestAgentPolicies = latestAgentPoliciesRes.hits.hits.reduce((acc, curr) => {
    const hit = curr._source;
    acc[`${hit.policy_id}:${hit.revision_idx}`] = {
      policyId: hit.policy_id,
      revision: hit.revision_idx,
      timestamp: hit['@timestamp'],
      agentsAssignedToPolicy: 0,
      agentsOnAtLeastThisRevision: 0
    };
    return acc;
  }, {});
  const agentsPerPolicyRevisionRes = await esClient.search({
    index: _common.AGENTS_INDEX,
    size: 0,
    // ignore unenrolled agents
    query: {
      bool: {
        must_not: [{
          exists: {
            field: 'unenrolled_at'
          }
        }]
      }
    },
    aggs: {
      policies: {
        terms: {
          field: 'policy_id',
          size: 10
        },
        aggs: {
          agents_per_rev: {
            terms: {
              field: 'policy_revision_idx',
              size: 10
            }
          }
        }
      }
    }
  });
  const agentsPerPolicyRevisionMap = agentsPerPolicyRevisionRes.aggregations.policies.buckets.reduce((acc, policyBucket) => {
    const policyId = policyBucket.key;
    const policyAgentCount = policyBucket.doc_count;
    if (!acc[policyId]) acc[policyId] = {
      total: 0,
      agentsPerRev: []
    };
    acc[policyId].total = policyAgentCount;
    acc[policyId].agentsPerRev = policyBucket.agents_per_rev.buckets.map(agentsPerRev => {
      return {
        revision: agentsPerRev.key,
        agents: agentsPerRev.doc_count
      };
    });
    return acc;
  }, {});
  Object.values(latestAgentPolicies).forEach(agentPolicyRev => {
    const agentsPerPolicyRev = agentsPerPolicyRevisionMap[agentPolicyRev.policyId];
    if (agentsPerPolicyRev) {
      agentPolicyRev.agentsAssignedToPolicy = agentsPerPolicyRev.total;
      agentsPerPolicyRev.agentsPerRev.forEach(item => {
        if (agentPolicyRev.revision <= item.revision) {
          agentPolicyRev.agentsOnAtLeastThisRevision += item.agents;
        }
      });
    }
  });
  const agentPolicyUpdateActions = Object.entries(latestAgentPolicies).map(([updateKey, updateObj]) => {
    return {
      actionId: updateKey,
      creationTime: updateObj.timestamp,
      completionTime: updateObj.timestamp,
      type: 'POLICY_CHANGE',
      nbAgentsActioned: updateObj.agentsAssignedToPolicy,
      nbAgentsAck: updateObj.agentsOnAtLeastThisRevision,
      nbAgentsActionCreated: updateObj.agentsAssignedToPolicy,
      nbAgentsFailed: 0,
      status: updateObj.agentsAssignedToPolicy === updateObj.agentsOnAtLeastThisRevision ? 'COMPLETE' : 'IN_PROGRESS',
      policyId: updateObj.policyId,
      revision: updateObj.revision
    };
  });
  return agentPolicyUpdateActions;
}