text: 'use strict';
  
  const path = require('path');
  const os = require('os');
  const ensureString = require('type/string/ensure');
  const ensureArray = require('type/array/ensure');
  const ensurePlainObject = require('type/plain-object/ensure');
  const CLI = require('./classes/CLI');
  const Config = require('./classes/Config');
  const YamlParser = require('./classes/YamlParser');
  const PluginManager = require('./classes/PluginManager');
  const Utils = require('./classes/Utils');
  const Service = require('./classes/Service');
  const Variables = require('./classes/Variables');
  const ConfigSchemaHandler = require('./classes/ConfigSchemaHandler');
  const ServerlessError = require('./serverless-error');
  const version = require('./../package.json').version;
  const isStandaloneExecutable = require('./utils/isStandaloneExecutable');
  const resolveConfigurationPath = require('./cli/resolve-configuration-path');
  const logDeprecation = require('./utils/logDeprecation');
  const eventuallyUpdate = require('./utils/eventuallyUpdate');
  const resolveLocalServerlessPath = require('./cli/resolve-local-serverless-path');
  const commmandsSchema = require('./cli/commands-schema');
  const resolveCliInput = require('./cli/resolve-input');
  const readConfiguration = require('./configuration/read');
  const conditionallyLoadDotenv = require('./cli/conditionally-load-dotenv');
  
  const serverlessPath = path.resolve(__dirname, '..');
  
  class Serverless {
    constructor(config) {
      let configObject = config;
      configObject = configObject || {};
      this._isInvokedByGlobalInstallation = Boolean(configObject._isInvokedByGlobalInstallation);
  
      if (configObject.serviceDir != null) {
        // Modern intialization way, to be the only supported way with v3
        this.serviceDir = path.resolve(
          ensureString(configObject.serviceDir, {
            name: 'config.serviceDir',
            Error: ServerlessError,
            errorCode: 'INVALID_NON_STRING_SERVICE_DIR',
          })
        );
        this.configurationFilename = ensureString(configObject.configurationFilename, {
          name: 'config.configurationFilename',
          Error: ServerlessError,
          errorCode: 'INVALID_NON_STRING_CONFIGURATION_FILENAME',
        });
        if (path.isAbsolute(this.configurationFilename)) {
          throw new ServerlessError(
            `"config.configurationFilename" cannot be absolute path. Received: ${configObject.configurationFilename}`,
            'INVALID_ABSOLUTE_PATH_CONFIGURATION_FILENAME'
          );
        }
        this.configurationInput = ensurePlainObject(configObject.configuration, {
          name: 'config.configuration',
          Error: ServerlessError,
          errorCode: 'INVALID_NON_OBJECT_CONFIGURATION',
        });
        this.isConfigurationInputResolved = Boolean(configObject.isConfigurationResolved);
      } else if (configObject.configurationPath != null) {
        // Semi-modern initialization way, mid-step introduced over the course of v2 refactor
        const configurationPath = path.resolve(
          ensureString(configObject.configurationPath, {
            name: 'config.configurationPath',
            Error: ServerlessError,
            errorCode: 'INVALID_NON_STRING_CONFIGURATION_PATH',
          })
        );
        this.serviceDir = process.cwd();
        this.configurationFilename = configurationPath.slice(this.serviceDir.length + 1);
        this.configurationInput = ensurePlainObject(configObject.configuration, {
          isOptional: true,
          name: 'config.configuration',
          Error: ServerlessError,
          errorCode: 'INVALID_NON_OBJECT_CONFIGURATION',
        });
        if (this.configurationInput) {
          this.isConfigurationInputResolved = Boolean(configObject.isConfigurationResolved);
        }
        this._shouldReportMissingServiceDeprecation = true;
      } else if (
        configObject.configurationPath === undefined &&
        configObject.serviceDir === undefined
      ) {
        // Old intialization way
        this._shouldResolveConfigurationInternally = true;
        this._shouldReportMissingServiceDeprecation = true;
      }
      const commands = ensureArray(configObject.commands, { isOptional: true });
      let options = ensurePlainObject(configObject.options, { isOptional: true });
      // This is a temporary workaround to ensure that original `options` are not mutated
      // Should be removed after addressing: https://github.com/serverless/serverless/issues/2582
      if (options) options = { ...options };
      if (!commands || !options) {
        this._shouldReportCommandsDeprecation = true;
        this.processedInput = resolveCliInput();
      } else {
        this.processedInput = { commands, options };
      }
      this.hasResolvedCommandsExternally = Boolean(configObject.hasResolvedCommandsExternally);
      this.isTelemetryReportedExternally = Boolean(configObject.isTelemetryReportedExternally);
  
      // Due to design flaw properties of configObject (which is to be merged onto `this.config`)
      // also are subject to variables resolution.
      // To avoid that we clear configObject after consuming it's properties.
      // Once new variables engine is in, we can remove that patch
      delete configObject.configurationPath;
      delete configObject.configuration;
      delete configObject.serviceDir;
      delete configObject._isInvokedByGlobalInstallation;
      delete configObject.commands;
      delete configObject.isConfigurationResolved;
      delete configObject.hasResolvedCommandsExternally;
      delete configObject.isTelemetryReportedExternally;
      delete configObject.options;
  
      this.providers = {};
  
      this.version = version;
  
      this.yamlParser = new YamlParser(this);
      this.utils = new Utils(this);
      this.service = new Service(this);
      this.variables = new Variables(this);
      this.pluginManager = new PluginManager(this);
      this.configSchemaHandler = new ConfigSchemaHandler(this);
  
      this.config = new Config(this, configObject);
  
      this.classes = {};
      this.classes.CLI = CLI;
      this.classes.YamlParser = YamlParser;
      this.classes.Utils = Utils;
      this.classes.Service = Service;
      this.classes.Variables = Variables;
      this.classes.Error = ServerlessError;
      this.classes.PluginManager = PluginManager;
      this.classes.ConfigSchemaHandler = ConfigSchemaHandler;
  
      this.serverlessDirPath = path.join(os.homedir(), '.serverless');
      this.isStandaloneExecutable = isStandaloneExecutable;
      this.isLocallyInstalled = false;
      this.triggeredDeprecations = logDeprecation.triggeredDeprecations;
  
      // TODO: Remove once "@serverless/dashboard-plugin" is integrated into this repository
      this._commandsSchema = commmandsSchema;
    }
  
    async init() {
      if (!this._isInvokedByGlobalInstallation) {
        if (this._shouldReportMissingServiceDeprecation) {
          this._logDeprecation(
            'MISSING_SERVICE_CONFIGURATION',
            'Serverless constructor expects service configuration details to be provided.\n' +
              'Starting from next major Serverless will no longer auto resolve it internally.'
          );
        }
        if (this._shouldReportCommandsDeprecation) {
          this._logDeprecation(
            'MISSING_COMMANDS_OR_OPTIONS_AT_CONSTRUCTION',
            'Serverless constructor expects resolved CLI commands and options to be provided ' +
              'via "config.commands" and "config.options".\n' +
              'Starting from next major Serverless will no longer auto resolve CLI arguments internally.'
          );
        }
      }
      if (this._shouldResolveConfigurationInternally) {
        const configurationPath = await resolveConfigurationPath();
        if (configurationPath) {
          this.serviceDir = process.cwd();
          this.configurationFilename = configurationPath.slice(this.serviceDir.length + 1);
        }
      }
      if (this.configurationFilename && !this.configurationInput) {
        this.configurationInput = await (async () => {
          try {
            return await readConfiguration(path.resolve(this.serviceDir, this.configurationFilename));
          } catch (error) {
            if (resolveCliInput().isHelpRequest) return null;
            throw error;
          }
        })();
      }
  
      // create an instanceId (can be e.g. used when a predictable random value is needed)
      this.instanceId = new Date().getTime().toString();
  
      // create a new CLI instance
      this.cli = new this.classes.CLI(this);
  
      await this.eventuallyFallbackToLocal();
      if (this.isOverridenByLocal) return;
      eventuallyUpdate(this);
      // set the options and commands which were processed by the CLI
      this.pluginManager.setCliOptions(this.processedInput.options);
      this.pluginManager.setCliCommands(this.processedInput.commands);
  
      // TODO: Remove with next major
      await this.loadEnvVariables();
  
      await this.service.load(this.processedInput.options);
      // load all plugins
      await this.pluginManager.loadAllPlugins(this.service.plugins);
      // give the CLI the plugins and commands so that it can print out
      // information such as options when the user enters --help
      this.cli.setLoadedPlugins(this.pluginManager.getPlugins());
      this.cli.setLoadedCommands(this.pluginManager.getCommands());
      await this.pluginManager.updateAutocompleteCacheFile();
    }
    async eventuallyFallbackToLocal() {
      if (
        this.configurationInput &&
        this.configurationInput.enableLocalInstallationFallback != null
      ) {
        this._logDeprecation(
          'DISABLE_LOCAL_INSTALLATION_FALLBACK_SETTING',
          'Starting with next major version, "enableLocalInstallationFallback" setting will no longer be supported.' +
            'CLI will unconditionally fallback to service local installation when its found.\n' +
            'Remove this setting to clear this deprecation warning'
        );
      }
      if (this.isLocallyInstalled) return;
      const localServerlessPath = await resolveLocalServerlessPath();
      if (!localServerlessPath) return;
      if (localServerlessPath === serverlessPath) {
        this.isLocallyInstalled = true;
        return;
      }
      if (
        this.configurationInput &&
        this.configurationInput.enableLocalInstallationFallback != null &&
        !this.configurationInput.enableLocalInstallationFallback
      ) {
        return;
      }
      this.cli.log('Running "serverless" installed locally (in service node_modules)');
      // TODO: Replace below fallback logic with more straightforward one at top of the CLI
      // when we willl drop support for the "disableLocalInstallationFallback" setting
      this.isOverridenByLocal = true;
      const ServerlessLocal = require(localServerlessPath);
      const serverlessLocal = new ServerlessLocal({
        serviceDir: this.serviceDir || null,
        configurationFilename: this.configurationFilename,
        configurationPath:
          (this.configurationFilename && path.resolve(this.serviceDir, this.configurationFilename)) ||
          null,
        configuration: this.configurationInput,
        isConfigurationResolved: this.isConfigurationInputResolved,
        hasResolvedCommandsExternally: this.hasResolvedCommandsExternally,
        commands: this.processedInput.commands,
        options: this.processedInput.options,
        _isInvokedByGlobalInstallation: true,
      });
      serverlessLocal.isLocallyInstalled = true;
      if (!serverlessLocal._isInvokedByGlobalInstallation) {
        // if we fallback to older version it may recognize "isInvokedByGlobalInstallation" instead
        // of "_isInvokedByGlobalInstallation". Ensure to set it, in such case
        serverlessLocal.isInvokedByGlobalInstallation = true;
      }
      this.invokedInstance = serverlessLocal;
      await serverlessLocal.init();
    }
  
    async loadEnvVariables() {
      const configurationInput = this.configurationInput;
      if (this.configurationInput == null) return;
      await conditionallyLoadDotenv(this.processedInput.options, configurationInput);
    }
  
    async run() {
      if (!this.hasResolvedCommandsExternally) {
        // Invoked by older global version. Ensure to have command schema saturated
        // which is needed for complete help output
        // TODO: Remove with next major
        const commandsSchema = require('./cli/commands-schema/resolve-final')(
          this.pluginManager.externalPlugins,
          {
            providerName: this.service.provider.name,
            configuration: this.configurationInput,
          }
        );
        const resolveInput = require('./cli/resolve-input');
        resolveInput.clear();
        resolveInput(commandsSchema);
      }
      if (this.cli.displayHelp(this.processedInput)) {
        return;
      }
      this.cli.suppressLogIfPrintCommand(this.processedInput);
  
      // make sure the command exists before doing anything else
      this.pluginManager.validateCommand(this.processedInput.commands);
  
      if (!this.isConfigurationInputResolved) {
        // populate variables after --help, otherwise help may fail to print
        // (https://github.com/serverless/serverless/issues/2041)
        await this.variables.populateService(this.pluginManager.cliOptions);
      } else {
        // Some plugins resolve additional variables on their own by runnning `variables.populateObject`
        // e.g. https://github.com/serverless-operations/serverless-step-functions/blob/016da8db78f1972ba80d37941c34c8fd038fd8ca/lib/yamlParser.js#L26
        // and that requires `variableSyntax` and `options` initizalization which is guaranteed by
        // `variables.populateService`. Below lines ensure they're set
        this.variables.loadVariableSyntax();
        this.variables.options = this.pluginManager.cliOptions;
        if (process.env.SLS_DEBUG) {
          this.cli.log(
            'Skipping variables resolution with old resolver ' +
              '(new resolver reported no more variables to resolve)'
          );
        }
      }
  
      // merge arrays after variables have been populated
      // (https://github.com/serverless/serverless/issues/3511)
      this.service.mergeArrays();
  
      // populate function names after variables are loaded in case functions were externalized
      // (https://github.com/serverless/serverless/issues/2997)
      this.service.setFunctionNames(this.processedInput.options);
  
      // If in context of service, validate the service configuration
      if (this.serviceDir) this.service.validate();
  
      // trigger the plugin lifecycle when there's something which should be processed
      await this.pluginManager.run(this.processedInput.commands);
    }
  
    setProvider(name, provider) {
      this.providers[name] = provider;
    }
  
    getProvider(name) {
      return this.providers[name] ? this.providers[name] : false;
    }
  
    getVersion() {
      return this.version;
    }
  
    // Only for internal use
    _logDeprecation(code, message) {
      return logDeprecation(code, message, { serviceConfig: this.service });
    }
  
    // To be used by external plugins
    logDeprecation(code, message) {
      return this._logDeprecation(`EXT_${ensureString(code)}`, ensureString(message));
    }
  
    // If this instance is initialized by older version of the Framework,
    // it may set "isInvokedByGlobalInstallation" directly.
    // This fallback ensures it ends at "_isInvokedByGlobalInstallation"
    set isInvokedByGlobalInstallation(value) {
      this._isInvokedByGlobalInstallation = value;
    }
    get isInvokedByGlobalInstallation() {
      return this._isInvokedByGlobalInstallation;
    }
  }
  
  module.exports = Serverless;
  