"use strict";

var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
Object.defineProperty(exports, "__esModule", {
  value: true
});
exports.ConfigService = void 0;
var _defineProperty2 = _interopRequireDefault(require("@babel/runtime/helpers/defineProperty"));
var _configSchema = require("@kbn/config-schema");
var _lodash = require("lodash");
var _saferLodashSet = require("@kbn/safer-lodash-set");
var _rxjs = require("rxjs");
var _operators = require("rxjs/operators");
var _docLinks = require("@kbn/doc-links");
var _std = require("@kbn/std");
var _config = require("./config");
var _deprecation = require("./deprecation");
var _object_to_config_adapter = require("./object_to_config_adapter");
/*
 * 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.
 */

/** @internal */

/** @internal */

/** @internal */
class ConfigService {
  /**
   * Whenever a config if read at a path, we mark that path as 'handled'. We can
   * then list all unhandled config paths when the startup process is completed.
   */

  constructor(rawConfigProvider, env, logger) {
    (0, _defineProperty2.default)(this, "log", void 0);
    (0, _defineProperty2.default)(this, "deprecationLog", void 0);
    (0, _defineProperty2.default)(this, "docLinks", void 0);
    (0, _defineProperty2.default)(this, "validated", false);
    (0, _defineProperty2.default)(this, "config$", void 0);
    (0, _defineProperty2.default)(this, "lastConfig", void 0);
    (0, _defineProperty2.default)(this, "deprecatedConfigPaths", new _rxjs.BehaviorSubject({
      set: [],
      unset: []
    }));
    (0, _defineProperty2.default)(this, "handledPaths", new Set());
    (0, _defineProperty2.default)(this, "schemas", new Map());
    (0, _defineProperty2.default)(this, "deprecations", new _rxjs.BehaviorSubject([]));
    (0, _defineProperty2.default)(this, "dynamicPaths", new Map());
    (0, _defineProperty2.default)(this, "overrides$", new _rxjs.BehaviorSubject({}));
    (0, _defineProperty2.default)(this, "handledDeprecatedConfigs", new Map());
    this.rawConfigProvider = rawConfigProvider;
    this.env = env;
    this.log = logger.get('config');
    this.deprecationLog = logger.get('config', 'deprecation');
    this.docLinks = (0, _docLinks.getDocLinks)({
      kibanaBranch: env.packageInfo.branch
    });
    this.config$ = (0, _rxjs.combineLatest)([this.rawConfigProvider.getConfig$(), this.deprecations, this.overrides$]).pipe((0, _operators.map)(([rawConfig, deprecations, overrides]) => {
      const overridden = (0, _lodash.merge)(rawConfig, overrides);
      const migrated = (0, _deprecation.applyDeprecations)(overridden, deprecations);
      this.deprecatedConfigPaths.next(migrated.changedPaths);
      return new _object_to_config_adapter.ObjectToConfigAdapter(migrated.config);
    }), (0, _operators.tap)(config => {
      this.lastConfig = config;
    }), (0, _operators.shareReplay)(1));
  }

  /**
   * Set config schema for a path and performs its validation
   */
  setSchema(path, schema) {
    const namespace = pathToString(path);
    if (this.schemas.has(namespace)) {
      throw new Error(`Validation schema for [${path}] was already registered.`);
    }
    this.schemas.set(namespace, schema);
    this.markAsHandled(path);
  }

  /**
   * Register a {@link ConfigDeprecationProvider} to be used when validating and migrating the configuration
   */
  addDeprecationProvider(path, provider) {
    const flatPath = pathToString(path);
    this.deprecations.next([...this.deprecations.value, ...provider(_deprecation.configDeprecationFactory).map(deprecation => ({
      deprecation,
      path: flatPath,
      context: this.createDeprecationContext()
    }))]);
  }

  /**
   * returns all handled deprecated configs
   */
  getHandledDeprecatedConfigs() {
    return [...this.handledDeprecatedConfigs.entries()];
  }

  /**
   * Validate the whole configuration and log the deprecation warnings.
   *
   * This must be done after every schemas and deprecation providers have been registered.
   */
  async validate(params = {
    logDeprecations: true
  }) {
    const namespaces = [...this.schemas.keys()];
    for (let i = 0; i < namespaces.length; i++) {
      await this.getValidatedConfigAtPath$(namespaces[i]).pipe((0, _operators.first)()).toPromise();
    }
    if (params.logDeprecations) {
      await this.logDeprecation();
    }
    this.validated = true;
  }

  /**
   * Returns the full config object observable. This is not intended for
   * "normal use", but for internal features that _need_ access to the full object.
   */
  getConfig$() {
    return this.config$;
  }

  /**
   * Reads the subset of the config at the specified `path` and validates it
   * against its registered schema.
   *
   * @param path - The path to the desired subset of the config.
   * @param ignoreUnchanged - If true (default), will not emit if the config at path did not change.
   */
  atPath(path, {
    ignoreUnchanged = true
  } = {}) {
    return this.getValidatedConfigAtPath$(path, {
      ignoreUnchanged
    });
  }

  /**
   * Similar to {@link atPath}, but return the last emitted value synchronously instead of an
   * observable.
   *
   * @param path - The path to the desired subset of the config.
   */
  atPathSync(path) {
    if (!this.validated) {
      throw new Error('`atPathSync` called before config was validated');
    }
    const configAtPath = this.lastConfig.get(path);
    return this.validateAtPath(path, configAtPath);
  }
  async isEnabledAtPath(path) {
    const namespace = pathToString(path);
    const hasSchema = this.schemas.has(namespace);
    const config = await (0, _rxjs.firstValueFrom)(this.config$);
    if (!hasSchema && config.has(path)) {
      // Throw if there is no schema, but a config exists at the path.
      throw new Error(`No validation schema has been defined for [${namespace}]`);
    }
    const validatedConfig = hasSchema ? await this.atPath(path).pipe((0, _operators.first)()).toPromise() : undefined;
    const isDisabled = (validatedConfig === null || validatedConfig === void 0 ? void 0 : validatedConfig.enabled) === false;
    if (isDisabled) {
      // If the plugin is explicitly disabled, we mark the entire plugin
      // path as handled, as it's expected that it won't be used.
      this.markAsHandled(path);
      return false;
    }

    // If the schema exists and the config is explicitly set to true,
    // _or_ if the `enabled` config is undefined, then we treat the
    // plugin as enabled.
    return true;
  }
  async getUnusedPaths() {
    const config = await (0, _rxjs.firstValueFrom)(this.config$);
    const handledPaths = [...this.handledPaths.values()].map(pathToString);
    return config.getFlattenedPaths().filter(path => !isPathHandled(path, handledPaths));
  }
  async getUsedPaths() {
    const config = await (0, _rxjs.firstValueFrom)(this.config$);
    const handledPaths = [...this.handledPaths.values()].map(pathToString);
    return config.getFlattenedPaths().filter(path => isPathHandled(path, handledPaths));
  }
  getDeprecatedConfigPath$() {
    return this.deprecatedConfigPaths.asObservable();
  }

  /**
   * Adds a specific setting to be allowed to change dynamically.
   * @param configPath The namespace of the config
   * @param dynamicConfigPaths The config keys that can be dynamically changed
   */
  addDynamicConfigPaths(configPath, dynamicConfigPaths) {
    const _configPath = Array.isArray(configPath) ? configPath.join('.') : configPath;
    this.dynamicPaths.set(_configPath, dynamicConfigPaths);
  }

  /**
   * Used for dynamically extending the overrides.
   * These overrides are not persisted and will be discarded after restarts.
   * @param newOverrides
   */
  setDynamicConfigOverrides(newOverrides) {
    const globalOverrides = (0, _lodash.cloneDeep)(this.overrides$.value);
    const flattenedOverrides = (0, _std.getFlattenedObject)(newOverrides);
    const validateWithNamespace = new Set();
    keyLoop: for (const key in flattenedOverrides) {
      // this if is enforced by an eslint rule :shrug:
      if (key in flattenedOverrides) {
        for (const [configPath, dynamicConfigKeys] of this.dynamicPaths.entries()) {
          if (key.startsWith(`${configPath}.`) && dynamicConfigKeys.some(
          // The key is explicitly allowed OR its prefix is
          dynamicConfigKey => key === `${configPath}.${dynamicConfigKey}` || key.startsWith(`${configPath}.${dynamicConfigKey}.`))) {
            validateWithNamespace.add(configPath);
            (0, _saferLodashSet.set)(globalOverrides, key, flattenedOverrides[key]);
            continue keyLoop;
          }
        }
        throw new _configSchema.ValidationError(new _configSchema.SchemaTypeError(`not a valid dynamic option`, [key]));
      }
    }
    const globalOverridesAsConfig = new _object_to_config_adapter.ObjectToConfigAdapter((0, _lodash.merge)({}, this.lastConfig, globalOverrides));
    validateWithNamespace.forEach(ns => this.validateAtPath(ns, globalOverridesAsConfig.get(ns)));
    this.overrides$.next(globalOverrides);
  }
  async logDeprecation() {
    const rawConfig = await (0, _rxjs.firstValueFrom)(this.rawConfigProvider.getConfig$());
    const deprecations = await (0, _rxjs.firstValueFrom)(this.deprecations);
    const deprecationMessages = [];
    const createAddDeprecation = domainId => context => {
      if (!context.silent) {
        deprecationMessages.push(context.message);
      }
      this.markDeprecatedConfigAsHandled(domainId, context);
    };
    (0, _deprecation.applyDeprecations)(rawConfig, deprecations, createAddDeprecation);
    deprecationMessages.forEach(msg => {
      this.deprecationLog.warn(msg);
    });
  }
  validateAtPath(path, config) {
    const namespace = pathToString(path);
    const schema = this.schemas.get(namespace);
    if (!schema) {
      throw new Error(`No validation schema has been defined for [${namespace}]`);
    }
    return schema.validate(config, {
      dev: this.env.mode.dev,
      prod: this.env.mode.prod,
      serverless: this.env.cliArgs.serverless === true,
      ...this.env.packageInfo
    }, `config validation of [${namespace}]`);
  }
  getValidatedConfigAtPath$(path, {
    ignoreUnchanged = true
  } = {}) {
    return this.config$.pipe((0, _operators.map)(config => config.get(path)), ignoreUnchanged ? (0, _operators.distinctUntilChanged)(_lodash.isEqual) : _rxjs.identity, (0, _operators.map)(config => this.validateAtPath(path, config)));
  }
  markAsHandled(path) {
    this.log.debug(`Marking config path as handled: ${path}`);
    this.handledPaths.add(path);
  }
  markDeprecatedConfigAsHandled(domainId, config) {
    const handledDeprecatedConfig = this.handledDeprecatedConfigs.get(domainId) || [];
    handledDeprecatedConfig.push(config);
    this.handledDeprecatedConfigs.set(domainId, handledDeprecatedConfig);
  }
  createDeprecationContext() {
    return {
      branch: this.env.packageInfo.branch,
      version: this.env.packageInfo.version,
      docLinks: this.docLinks
    };
  }
}
exports.ConfigService = ConfigService;
const pathToString = path => Array.isArray(path) ? path.join('.') : path;

/**
 * A path is considered 'handled' if it is a subset of any of the already
 * handled paths.
 */
const isPathHandled = (path, handledPaths) => handledPaths.some(handledPath => (0, _config.hasConfigPathIntersection)(path, handledPath));