import getS3EndpointForRegion from '../utils/get-s3-endpoint-for-region.js'
import utils from '@serverlessinc/sf-core/src/utils.js'
import isChangeSetWithoutChanges from '../utils/is-change-set-without-changes.js'

const { log, progress } = utils
const NO_UPDATE_MESSAGE = 'No updates are to be performed.'

export default {
  async createFallback() {
    this.createLater = false
    progress
      .get('main')
      .notice('Creating AWS 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 ||
      this.serverless.service.provider.deploymentMethod === 'direct'
    ) {
      const params = this.getCreateStackParams({
        templateUrl,
      })

      monitorCfData = await this.provider.request(
        'CloudFormation',
        'createStack',
        params,
      )
    } else {
      // Change-set based deployment
      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 ||
      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 {
      // Change-set based deployment
      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 === '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 Promise.resolve(this).then(() => {
      if (this.createLater) {
        return Promise.resolve(this).then(() => this.createFallback())
      }
      return Promise.resolve(this).then(() => this.update())
    })
  },
}
