"use strict";

var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
Object.defineProperty(exports, "__esModule", {
  value: true
});
exports.SearchSessionService = void 0;
var _defineProperty2 = _interopRequireDefault(require("@babel/runtime/helpers/defineProperty"));
var _boom = require("@hapi/boom");
var _esQuery = require("@kbn/es-query");
var _common = require("@kbn/kibana-utils-plugin/common");
var _lodash = require("lodash");
var _common2 = require("../../../common");
var _ = require("../..");
var _utils = require("./utils");
var _get_session_status = require("./get_session_status");
/*
 * 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 and the Server Side Public License, v 1; you may not use this file except
 * in compliance with, at your election, the Elastic License 2.0 or the Server
 * Side Public License, v 1.
 */

// eslint-disable-next-line @typescript-eslint/no-empty-interface

/**
 * Used to batch requests that add searches into the session saved object
 */
const DEBOUNCE_TRACK_ID_WAIT = 1000;
const DEBOUNCE_TRACK_ID_MAX_WAIT = 5000;
class SearchSessionService {
  constructor(logger, config, version) {
    (0, _defineProperty2.default)(this, "sessionConfig", void 0);
    (0, _defineProperty2.default)(this, "security", void 0);
    (0, _defineProperty2.default)(this, "setupCompleted", false);
    (0, _defineProperty2.default)(this, "save", async (deps, user, sessionId, {
      name,
      appId,
      locatorId,
      initialState = {},
      restoreState = {}
    }) => {
      if (!this.sessionConfig.enabled) throw new Error('Search sessions are disabled');
      if (!name) throw new Error('Name is required');
      if (!appId) throw new Error('AppId is required');
      if (!locatorId) throw new Error('locatorId is required');
      return this.create(deps, user, sessionId, {
        name,
        appId,
        locatorId,
        initialState,
        restoreState
      });
    });
    (0, _defineProperty2.default)(this, "create", ({
      savedObjectsClient
    }, user, sessionId, attributes) => {
      this.logger.debug(`SearchSessionService: create | ${sessionId}`);
      const realmType = user === null || user === void 0 ? void 0 : user.authentication_realm.type;
      const realmName = user === null || user === void 0 ? void 0 : user.authentication_realm.name;
      const username = user === null || user === void 0 ? void 0 : user.username;
      return savedObjectsClient.create(_common2.SEARCH_SESSION_TYPE, {
        sessionId,
        expires: new Date(Date.now() + this.sessionConfig.defaultExpiration.asMilliseconds()).toISOString(),
        created: new Date().toISOString(),
        idMapping: {},
        version: this.version,
        realmType,
        realmName,
        username,
        ...attributes
      }, {
        id: sessionId
      });
    });
    (0, _defineProperty2.default)(this, "get", async ({
      savedObjectsClient
    }, user, sessionId) => {
      this.logger.debug(`get | ${sessionId}`);
      const session = await savedObjectsClient.get(_common2.SEARCH_SESSION_TYPE, sessionId);
      this.throwOnUserConflict(user, session);
      return session;
    });
    (0, _defineProperty2.default)(this, "find", async ({
      savedObjectsClient,
      internalElasticsearchClient
    }, user, options) => {
      const userFilters = user === null ? [] : [_esQuery.nodeBuilder.is(`${_common2.SEARCH_SESSION_TYPE}.attributes.realmType`, `${user.authentication_realm.type}`), _esQuery.nodeBuilder.is(`${_common2.SEARCH_SESSION_TYPE}.attributes.realmName`, `${user.authentication_realm.name}`), _esQuery.nodeBuilder.is(`${_common2.SEARCH_SESSION_TYPE}.attributes.username`, `${user.username}`)];
      const filterKueryNode = typeof options.filter === 'string' ? (0, _esQuery.fromKueryExpression)(options.filter) : options.filter;
      const filter = _esQuery.nodeBuilder.and(userFilters.concat(filterKueryNode !== null && filterKueryNode !== void 0 ? filterKueryNode : []));
      const findResponse = await savedObjectsClient.find({
        ...options,
        filter,
        type: _common2.SEARCH_SESSION_TYPE
      });
      const sessionStatuses = await Promise.all(findResponse.saved_objects.map(async so => {
        const sessionStatus = await (0, _get_session_status.getSessionStatus)({
          internalClient: internalElasticsearchClient
        }, so.attributes, this.sessionConfig);
        return sessionStatus;
      }));
      return {
        ...findResponse,
        statuses: sessionStatuses.reduce((res, {
          status,
          errors
        }, index) => {
          res[findResponse.saved_objects[index].id] = {
            status,
            errors
          };
          return res;
        }, {})
      };
    });
    (0, _defineProperty2.default)(this, "update", async (deps, user, sessionId, attributes) => {
      this.logger.debug(`SearchSessionService: update | ${sessionId}`);
      if (!this.sessionConfig.enabled) throw new Error('Search sessions are disabled');
      await this.get(deps, user, sessionId); // Verify correct user
      return deps.savedObjectsClient.update(_common2.SEARCH_SESSION_TYPE, sessionId, {
        ...attributes
      }, {
        retryOnConflict: this.sessionConfig.maxUpdateRetries
      });
    });
    (0, _defineProperty2.default)(this, "cancel", async (deps, user, sessionId) => {
      this.logger.debug(`SearchSessionService: cancel | ${sessionId}`);
      return this.update(deps, user, sessionId, {
        isCanceled: true
      });
    });
    (0, _defineProperty2.default)(this, "delete", async (deps, user, sessionId) => {
      if (!this.sessionConfig.enabled) throw new Error('Search sessions are disabled');
      this.logger.debug(`SearchSessionService: delete | ${sessionId}`);
      await this.get(deps, user, sessionId); // Verify correct user
      return deps.savedObjectsClient.delete(_common2.SEARCH_SESSION_TYPE, sessionId);
    });
    /**
     * Used to batch requests that add searches into the session saved object
     * Requests are grouped and executed per sessionId
     * @private
     */
    (0, _defineProperty2.default)(this, "trackIdBatchQueueMap", new Map());
    /**
     * Tracks the given search request/search ID in the saved session.
     * Instead of updating search-session saved object immediately, it debounces and batches updates internally,
     * to reduce number of saved object updates and reduce a chance of running over update retries limit
     * @internal
     */
    (0, _defineProperty2.default)(this, "trackId", async (deps, user, searchRequest, searchId, options) => {
      const {
        sessionId,
        strategy = _common2.ENHANCED_ES_SEARCH_STRATEGY
      } = options;
      if (!this.sessionConfig.enabled || !sessionId || !searchId) return;
      if (!searchRequest.params) return;
      const requestHash = (0, _utils.createRequestHash)(searchRequest.params);
      this.logger.debug(`SearchSessionService: trackId | sessionId: "${sessionId}" | searchId:"${searchId}" | requestHash: "${requestHash}"`);
      const searchInfo = {
        id: searchId,
        strategy
      };
      if (!this.trackIdBatchQueueMap.has(sessionId)) {
        this.trackIdBatchQueueMap.set(sessionId, {
          queue: [],
          scheduleProcessQueue: (0, _lodash.debounce)(() => {
            var _this$trackIdBatchQue, _this$trackIdBatchQue2;
            const queue = (_this$trackIdBatchQue = (_this$trackIdBatchQue2 = this.trackIdBatchQueueMap.get(sessionId)) === null || _this$trackIdBatchQue2 === void 0 ? void 0 : _this$trackIdBatchQue2.queue) !== null && _this$trackIdBatchQue !== void 0 ? _this$trackIdBatchQue : [];
            if (queue.length === 0) return;
            this.trackIdBatchQueueMap.delete(sessionId);
            const batchedIdMapping = queue.reduce((res, next) => {
              res[next.requestHash] = next.searchInfo;
              return res;
            }, {});
            this.update(queue[0].deps, queue[0].user, sessionId, {
              idMapping: batchedIdMapping
            }).then(() => {
              queue.forEach(q => q.resolve());
            }).catch(e => {
              queue.forEach(q => q.reject(e));
            });
          }, DEBOUNCE_TRACK_ID_WAIT, {
            maxWait: DEBOUNCE_TRACK_ID_MAX_WAIT
          })
        });
      }
      const deferred = (0, _common.defer)();
      const {
        queue,
        scheduleProcessQueue
      } = this.trackIdBatchQueueMap.get(sessionId);
      queue.push({
        deps,
        sessionId,
        searchInfo,
        requestHash,
        resolve: deferred.resolve,
        reject: deferred.reject,
        user
      });
      scheduleProcessQueue();
      return deferred.promise;
    });
    /**
     * Look up an existing search ID that matches the given request in the given session so that the
     * request can continue rather than restart.
     * @internal
     */
    (0, _defineProperty2.default)(this, "getId", async (deps, user, searchRequest, {
      sessionId,
      isStored,
      isRestore
    }) => {
      if (!this.sessionConfig.enabled) {
        throw new Error('Search sessions are disabled');
      } else if (!sessionId) {
        throw new Error('Session ID is required');
      } else if (!isStored) {
        throw new Error('Cannot get search ID from a session that is not stored');
      } else if (!isRestore) {
        throw new Error('Get search ID is only supported when restoring a session');
      }
      const session = await this.get(deps, user, sessionId);
      const requestHash = (0, _utils.createRequestHash)(searchRequest.params);
      if (!session.attributes.idMapping.hasOwnProperty(requestHash)) {
        this.logger.error(`SearchSessionService: getId | ${sessionId} | ${requestHash} not found`);
        this.logger.debug(`SearchSessionService: getId not found search with params: ${JSON.stringify(searchRequest.params)}`);
        throw new _.NoSearchIdInSessionError();
      }
      this.logger.debug(`SearchSessionService: getId | ${sessionId} | ${requestHash}`);
      return session.attributes.idMapping[requestHash].id;
    });
    (0, _defineProperty2.default)(this, "asScopedProvider", ({
      savedObjects,
      elasticsearch
    }) => {
      return request => {
        var _this$security$authc$, _this$security;
        const user = (_this$security$authc$ = (_this$security = this.security) === null || _this$security === void 0 ? void 0 : _this$security.authc.getCurrentUser(request)) !== null && _this$security$authc$ !== void 0 ? _this$security$authc$ : null;
        const savedObjectsClient = savedObjects.getScopedClient(request, {
          includedHiddenTypes: [_common2.SEARCH_SESSION_TYPE]
        });
        const internalElasticsearchClient = elasticsearch.client.asScoped(request).asInternalUser;
        const deps = {
          savedObjectsClient,
          internalElasticsearchClient
        };
        return {
          getId: this.getId.bind(this, deps, user),
          trackId: this.trackId.bind(this, deps, user),
          getSearchIdMapping: this.getSearchIdMapping.bind(this, deps, user),
          save: this.save.bind(this, deps, user),
          get: this.get.bind(this, deps, user),
          find: this.find.bind(this, deps, user),
          update: this.update.bind(this, deps, user),
          extend: this.extend.bind(this, deps, user),
          cancel: this.cancel.bind(this, deps, user),
          delete: this.delete.bind(this, deps, user),
          status: this.status.bind(this, deps, user),
          getConfig: () => this.config.search.sessions
        };
      };
    });
    (0, _defineProperty2.default)(this, "throwOnUserConflict", (user, session) => {
      if (user === null || !session) return;
      if (user.authentication_realm.type !== session.attributes.realmType || user.authentication_realm.name !== session.attributes.realmName || user.username !== session.attributes.username) {
        this.logger.debug(`User ${user.username} has no access to search session ${session.attributes.sessionId}`);
        throw (0, _boom.notFound)();
      }
    });
    this.logger = logger;
    this.config = config;
    this.version = version;
    this.sessionConfig = this.config.search.sessions;
  }
  setup(core, deps) {
    this.security = deps.security;
    this.setupCompleted = true;
  }
  start(core, deps) {
    if (!this.setupCompleted) throw new Error('SearchSessionService setup() must be called before start()');
  }
  stop() {}
  async extend(deps, user, sessionId, expires) {
    this.logger.debug(`SearchSessionService: extend | ${sessionId}`);
    return this.update(deps, user, sessionId, {
      expires: expires.toISOString()
    });
  }
  async getSearchIdMapping(deps, user, sessionId) {
    const searchSession = await this.get(deps, user, sessionId);
    const searchIdMapping = new Map();
    Object.values(searchSession.attributes.idMapping).forEach(requestInfo => {
      searchIdMapping.set(requestInfo.id, requestInfo.strategy);
    });
    return searchIdMapping;
  }
  async status(deps, user, sessionId) {
    this.logger.debug(`SearchSessionService: status | ${sessionId}`);
    const session = await this.get(deps, user, sessionId);
    const sessionStatus = await (0, _get_session_status.getSessionStatus)({
      internalClient: deps.internalElasticsearchClient
    }, session.attributes, this.sessionConfig);
    return {
      status: sessionStatus.status,
      errors: sessionStatus.errors
    };
  }
}
exports.SearchSessionService = SearchSessionService;