text: 'use strict';
  
  const { ServerlessError, logWarning } = require('./Error');
  const util = require('util');
  const _ = require('lodash');
  const semver = require('semver');
  const { legacy, log } = require('@serverless-rewrite/utils/log');
  const resolveCliInput = require('../cli/resolve-input');
  const currentVersion = require('../../package').version;
  
  class Service {
    constructor(serverless, data) {
      // #######################################################################
      // ## KEEP SYNCHRONIZED WITH EQUIVALENT IN ~/lib/plugins/print/print.js ##
      // #######################################################################
      this.serverless = serverless;
  
      // Default properties
      this.service = null;
      this.serviceObject = null;
      this.provider = {
        stage: 'dev',
      };
      this.custom = {};
      this.plugins = [];
      this.pluginsData = {};
      this.functions = {};
      this.resources = {};
      this.package = {};
      this.configValidationMode = 'warn';
      this.disabledDeprecations = [];
  
      if (data) this.update(data);
    }
  
    async load(rawOptions) {
      const options = rawOptions || {};
      if (!options.stage && options.s) options.stage = options.s;
      if (!options.region && options.r) options.region = options.r;
      const serviceDir = this.serverless.serviceDir;
  
      // skip if the service path is not found
      // because the user might be creating a new service
      if (!serviceDir) return;
  
      try {
        this.loadServiceFileParam();
      } catch (error) {
        if (resolveCliInput().isHelpRequest) return;
        throw error;
      }
    }
  
    loadServiceFileParam() {
      // Not used internally, left set to not break plugins which depend on it
      // TOOD: Remove with next major
      this.serviceFilename = this.serverless.configurationFilename;
  
      const configurationInput = this.serverless.configurationInput;
  
      this.initialServerlessConfig = configurationInput;
  
      // TODO: Ideally below approach should be replaced at some point with:
      // 1. In "initialization" phase: Minimize reliance on service configuration
      //    to few core properties
      // 2. Before "run" phase: Validate and normalize (via AJV) `serverless.configurationInput`
      //    into `serverless.configuration`
      // 3. In "run" phase: Instead of relying on `serverless.service` rely on
      //    `serverless.configuration` internally
  
      // Below comments provide usage feedback helfpul for future refactor process.
  
      // ## Properties currently accessed at "initialization" phase
  
      this.disabledDeprecations = configurationInput.disabledDeprecations;
      this.deprecationNotificationMode = configurationInput.deprecationNotificationMode;
  
      // `provider` (`provder.name` by many plugin constructs, and few other core properties as
      //              `provider.stage` are read by dashboard plugin)
      if (!_.isObject(configurationInput.provider)) {
        const providerName = configurationInput.provider;
        this.provider = {
          name: providerName,
        };
      } else {
        this.provider = configurationInput.provider;
      }
      if (this.provider.stage == null) {
        this.provider.stage = 'dev';
      }
  
      // `service` (read by dashboard plugin)
      if (_.isObject(configurationInput.service)) {
        throw new ServerlessError(
          'Object notation for "service" property is not supported. Set "service" property directly with service name.',
          'SERVICE_OBJECT_NOTATION'
        );
      } else {
        this.serviceObject = { name: configurationInput.service };
        this.service = configurationInput.service;
      }
  
      this.plugins = configurationInput.plugins;
  
      // `package.path` is read by few core plugins at initialization
      if (configurationInput.package) {
        this.package = configurationInput.package;
      }
  
      // ## Properties accessed at "run" phase
      this.custom = configurationInput.custom; // (dashboard plugin)
      this.resources = configurationInput.resources;
      this.functions = configurationInput.functions || {};
      this.configValidationMode = configurationInput.configValidationMode || 'warn';
      this.unresolvedVariablesNotificationMode =
        configurationInput.unresolvedVariablesNotificationMode;
      if (this.provider.name === 'aws') {
        this.layers = configurationInput.layers || {};
      }
      this.outputs = configurationInput.outputs;
  
      // basic service level validation
      const version = this.serverless.utils.getVersion();
      let ymlVersion = configurationInput.frameworkVersion;
      if (ymlVersion && !semver.validRange(ymlVersion)) {
        if (configurationInput.configValidationMode === 'error') {
          throw new ServerlessError(
            'Configured "frameworkVersion" does not represent a valid semver version range.',
            'INVALID_FRAMEWORK_VERSION'
          );
        }
        logWarning(
          'Configured "frameworkVersion" does not represent a valid semver version range, version validation is skipped'
        );
        log.warning(
          'Configured "frameworkVersion" does not represent a valid semver version range, version validation is skipped'
        );
        ymlVersion = null;
      }
      if (!this.isLocallyInstalled && !ymlVersion && !semver.parse(version).prerelease.length) {
        log.info(
          'To ensure safe major version upgrades ensure "frameworkVersion" setting in ' +
            'service configuration ' +
            `(recommended setup: "frameworkVersion: ^${currentVersion}")\n`
        );
        if (process.env.SLS_DEBUG) {
          legacy.log(
            'To ensure safe major version upgrades ensure "frameworkVersion" setting in ' +
              'service configuration ' +
              `(recommended setup: "frameworkVersion: ^${currentVersion}")\n`
          );
        }
      }
      if (
        ymlVersion &&
        version !== ymlVersion &&
        !semver.satisfies(semver.coerce(version).raw, ymlVersion)
      ) {
        const errorMessage = [
          `The Serverless version (${version}) does not satisfy the`,
          ` "frameworkVersion" (${ymlVersion}) in ${this.serverless.configurationFilename}`,
        ].join('');
        throw new ServerlessError(errorMessage, 'FRAMEWORK_VERSION_MISMATCH');
      }
      if (!configurationInput.service) {
        throw new ServerlessError(
          `"service" property is missing in ${this.serverless.configurationFilename}`,
          'SERVICE_NAME_MISSING'
        );
      }
      if (!configurationInput.provider) {
        throw new ServerlessError(
          `"provider" property is missing in ${this.serverless.configurationFilename}`,
          'PROVIDER_NAME_MISSING'
        );
      }
      if (!_.isObject(configurationInput.provider)) {
        // Schema uncoditionally expects `provider` to be an object.
        // Ideally if it's fixed at some point, and either we do not support string notation for
        // provider, or we support string by schema
        configurationInput.provider = this.provider;
      }
  
      return this;
    }
  
    reloadServiceFileParam() {
      const configurationInput = this.initialServerlessConfig;
      if (_.isObject(configurationInput.provider)) {
        this.provider = configurationInput.provider;
        if (this.provider.stage == null) this.provider.stage = 'dev';
      }
      if (configurationInput.package) {
        this.package = configurationInput.package;
      }
      this.custom = configurationInput.custom;
      this.resources = configurationInput.resources;
      this.functions = configurationInput.functions || {};
      this.configValidationMode = configurationInput.configValidationMode || 'warn';
      this.unresolvedVariablesNotificationMode =
        configurationInput.unresolvedVariablesNotificationMode;
      if (this.provider.name === 'aws') {
        this.layers = configurationInput.layers || {};
      }
      this.outputs = configurationInput.outputs;
    }
  
    setFunctionNames(rawOptions) {
      const options = rawOptions || {};
      options.stage = options.stage || options.s;
      options.region = options.region || options.r;
  
      // Ensure that function is an object and setup function.name property
      const stageNameForFunction = options.stage || this.provider.stage;
  
      if (!_.isObject(this.functions)) {
        throw new ServerlessError(
          `Unexpected "functions" configuration: Expected object received: ${util.inspect(
            this.functions
          )}`,
          'NON_OBJECT_FUNCTIONS_CONFIGURATION'
        );
      }
      Object.entries(this.functions).forEach(([functionName, functionObj]) => {
        if (functionObj == null) {
          delete this.functions[functionName];
          return;
        }
        if (!_.isObject(functionObj)) {
          throw new ServerlessError(
            `Unexpected "${functionName}" function configuration: Expected object received ${util.inspect(
              functionObj
            )})`,
            'NON_OBJECT_FUNCTION_CONFIGURATION_ERROR'
          );
        }
        if (!functionObj.events) {
          this.functions[functionName].events = [];
        }
  
        if (!functionObj.name) {
          this.functions[
            functionName
          ].name = `${this.service}-${stageNameForFunction}-${functionName}`;
        }
      });
    }
  
    mergeArrays() {
      ['resources', 'functions'].forEach((key) => {
        if (Array.isArray(this[key])) {
          this[key] = this[key].reduce((memo, value) => {
            if (value) {
              if (typeof value === 'object') {
                return _.merge(memo, value);
              }
              throw new ServerlessError(
                `Non-object value specified in ${key} array: ${value}`,
                'LEGACY_CONFIGURATION_PROPERTY_MERGE_INVALID_INPUT'
              );
            }
  
            return memo;
          }, {});
        }
      });
    }
  
    validate() {
      const userConfig = this.initialServerlessConfig;
      userConfig.service = this.serviceObject;
  
      // Ensure to validate normalized (after mergeArrays) input
      if (userConfig.functions) userConfig.functions = this.functions;
      if (userConfig.resources) userConfig.resources = this.resources;
  
      this.serverless.configSchemaHandler.validateConfig(userConfig);
  
      return this;
    }
  
    update(data) {
      return _.merge(this, data);
    }
  
    getServiceName() {
      return this.serviceObject.name;
    }
  
    getServiceObject() {
      return this.serviceObject;
    }
  
    getAllFunctions() {
      return Object.keys(this.functions);
    }
  
    getAllLayers() {
      return this.layers ? Object.keys(this.layers) : [];
    }
  
    getAllFunctionsNames() {
      return this.getAllFunctions().map((func) => this.getFunction(func).name);
    }
  
    getFunction(functionName) {
      if (functionName in this.functions) {
        return this.functions[functionName];
      }
      throw new ServerlessError(
        `Function "${functionName}" doesn't exist in this Service`,
        'FUNCTION_MISSING_IN_SERVICE'
      );
    }
  
    getLayer(layerName) {
      if (layerName in this.layers) {
        return this.layers[layerName];
      }
      throw new ServerlessError(
        `Layer "${layerName}" doesn't exist in this Service`,
        'LAYER_MISSING_IN_SERVICE'
      );
    }
  
    getEventInFunction(eventName, functionName) {
      const event = this.getFunction(functionName).events.find(
        (e) => Object.keys(e)[0] === eventName
      );
      if (event) {
        return event;
      }
      throw new ServerlessError(
        `Event "${eventName}" doesn't exist in function "${functionName}"`,
        'EVENT_MISSING_FOR_FUNCTION'
      );
    }
  
    getAllEventsInFunction(functionName) {
      return this.getFunction(functionName).events;
    }
  
    publish(dataParam) {
      const data = dataParam || {};
      this.pluginsData = _.merge(this.pluginsData, data);
    }
  }
  
  module.exports = Service;
  