'use strict';

const BbPromise = require('bluebird');
const getS3EndpointForRegion = require('../utils/get-s3-endpoint-for-region');
const { log, progress } = require('@serverless/utils/log');
const isChangeSetWithoutChanges = require('../utils/is-change-set-without-changes');

const NO_UPDATE_MESSAGE = 'No updates are to be performed.';

module.exports = {
  async createFallback() {
    this.createLater = false;
    progress.get('main').notice('Creating CloudFormation stack', { isMainEvent: true });

    const stackName = this.provider.naming.getStackName();
    const compiledTemplateFileName = this.provider.naming.getCompiledTemplateS3Suffix();
    const s3Endpoint = getS3EndpointForRegion(this.provider.getRegion());
    const templateUrl = `https://${s3Endpoint}/${this.bucketName}/${this.serverless.service.package.artifactDirectoryName}/${compiledTemplateFileName}`;

    let monitorCfData;
    if (this.serverless.service.provider.deploymentMethod === 'direct') {
      const params = this.getCreateStackParams({
        templateUrl,
      });

      monitorCfData = await this.provider.request('CloudFormation', 'createStack', params);
    } else {
      const changeSetName = this.provider.naming.getStackChangeSetName();

      const createChangeSetParams = this.getCreateChangeSetParams({
        changeSetType: 'CREATE',
        templateUrl,
      });

      const executeChangeSetParams = this.getExecuteChangeSetParams();

      // Create new change set
      this.provider.didCreateService = true;
      log.info('Creating new change set');
      await this.provider.request('CloudFormation', 'createChangeSet', createChangeSetParams);

      // Wait for changeset to be created
      log.info('Waiting for new change set to be created');
      const changeSetDescription = await this.waitForChangeSetCreation(changeSetName, stackName);

      // Check if stack has changes
      if (isChangeSetWithoutChanges(changeSetDescription)) {
        // Cleanup changeset when it does not include any changes
        log.info('Created change set does not include any changes, removing it');
        await this.provider.request('CloudFormation', 'deleteChangeSet', {
          StackName: stackName,
          ChangeSetName: changeSetName,
        });
        this.serverless.service.provider.deploymentWithEmptyChangeSet = true;
        return false;
      }

      log.info('Executing created change set');
      await this.provider.request('CloudFormation', 'executeChangeSet', executeChangeSetParams);
      monitorCfData = changeSetDescription;
    }
    await this.monitorStack('create', monitorCfData);
    return true;
  },

  async update() {
    const compiledTemplateFileName = this.provider.naming.getCompiledTemplateS3Suffix();
    const s3Endpoint = getS3EndpointForRegion(this.provider.getRegion());
    const templateUrl = `https://${s3Endpoint}/${this.bucketName}/${this.serverless.service.package.artifactDirectoryName}/${compiledTemplateFileName}`;

    const stackName = this.provider.naming.getStackName();

    let monitorCfData;
    if (this.serverless.service.provider.deploymentMethod === 'direct') {
      const params = this.getUpdateStackParams({ templateUrl });

      try {
        monitorCfData = await this.provider.request('CloudFormation', 'updateStack', params);
      } catch (e) {
        if (e.message.includes(NO_UPDATE_MESSAGE)) {
          return false;
        }
        throw e;
      }
    } else {
      const changeSetName = this.provider.naming.getStackChangeSetName();

      const createChangeSetParams = this.getCreateChangeSetParams({
        changeSetType: 'UPDATE',
        templateUrl,
      });

      const executeChangeSetParams = this.getExecuteChangeSetParams();

      // Ensure that previous change set has been removed
      await this.provider.request('CloudFormation', 'deleteChangeSet', {
        StackName: stackName,
        ChangeSetName: changeSetName,
      });

      // Create new change set
      log.info('Creating new change set');
      await this.provider.request('CloudFormation', 'createChangeSet', createChangeSetParams);

      // Wait for changeset to be created
      log.info('Waiting for new change set to be created');
      const changeSetDescription = await this.waitForChangeSetCreation(changeSetName, stackName);

      // Check if stack has changes
      if (isChangeSetWithoutChanges(changeSetDescription)) {
        // Cleanup changeset when it does not include any changes
        log.info('Created change set does not include any changes, removing it');
        await this.provider.request('CloudFormation', 'deleteChangeSet', {
          StackName: stackName,
          ChangeSetName: changeSetName,
        });
        this.serverless.service.provider.deploymentWithEmptyChangeSet = true;
        return false;
      }

      log.info('Executing created change set');
      await this.provider.request('CloudFormation', 'executeChangeSet', executeChangeSetParams);
      monitorCfData = changeSetDescription;
    }

    await this.monitorStack('update', monitorCfData);

    // Policy must have at least one statement, otherwise no updates would be possible at all
    // Stack policy must be set after change set has been executed,
    // as it might reference resources newly added in that change set.
    // Applied only for ChangeSet deployments which is a default method
    if (
      (!this.serverless.service.provider.deploymentMethod ||
        this.serverless.service.provider.deploymentMethod === 'changesets') &&
      this.serverless.service.provider.stackPolicy &&
      Object.keys(this.serverless.service.provider.stackPolicy).length
    ) {
      log.info('Setting stack policy');
      const stackPolicyBody = JSON.stringify({
        Statement: this.serverless.service.provider.stackPolicy,
      });
      await this.provider.request('CloudFormation', 'setStackPolicy', {
        StackName: stackName,
        StackPolicyBody: stackPolicyBody,
      });
    }

    return true;
  },

  async updateStack() {
    return BbPromise.bind(this).then(() => {
      if (this.createLater) {
        return BbPromise.bind(this).then(this.createFallback);
      }
      return BbPromise.bind(this).then(this.update);
    });
  },
};
