import _ from 'lodash'
import fsp from 'fs/promises'
import path from 'path'
import crypto from 'crypto'
import promiseLimit from 'ext/promise/limit.js'
import { filesize } from 'filesize'
import normalizeFiles from '../../lib/normalize-files.js'
import getLambdaLayerArtifactPath from '../../utils/get-lambda-layer-artifact-path.js'
import ServerlessError from '../../../../serverless-error.js'
import setS3UploadEncryptionOptions from '../../../../aws/set-s3-upload-encryption-options.js'
import utils from '@serverlessinc/sf-core/src/utils.js'

const limit = promiseLimit.bind(Promise)
const { log, progress } = utils

const MAX_CONCURRENT_ARTIFACTS_UPLOADS =
  Number(process.env.SLS_MAX_CONCURRENT_ARTIFACTS_UPLOADS) || 3

export default {
  async getFileStats(filepath) {
    try {
      return await fsp.stat(filepath)
    } catch (error) {
      throw new ServerlessError(
        `Cannot read file artifact "${filepath}": ${error.message}`,
        'INACCESSIBLE_FILE_ARTIFACT',
      )
    }
  },

  async uploadArtifacts() {
    const artifactFilePaths = [
      ...(await this.getFunctionArtifactFilePaths()),
      ...this.getLayerArtifactFilePaths(),
    ]
    if (artifactFilePaths.length === 1) {
      const stats = await this.getFileStats(artifactFilePaths[0])
      progress.get('main').notice(`Uploading (${filesize(stats.size)})`)
    } else {
      progress.get('main').notice(`Uploading (0/${artifactFilePaths.length})`)
    }

    await this.uploadCloudFormationFile()
    await this.uploadStateFile()
    await this.uploadFunctionsAndLayers()
    await this.uploadCustomResources()
  },

  async uploadCloudFormationFile() {
    log.info('Uploading CloudFormation file to S3')

    const compiledTemplateFileName =
      this.provider.naming.getCompiledTemplateS3Suffix()

    const compiledCfTemplate =
      this.serverless.service.provider.compiledCloudFormationTemplate
    const normCfTemplate =
      normalizeFiles.normalizeCloudFormationTemplate(compiledCfTemplate)
    const fileHash = crypto
      .createHash('sha256')
      .update(JSON.stringify(normCfTemplate))
      .digest('base64')

    let params = {
      Bucket: this.bucketName,
      Key: `${this.serverless.service.package.artifactDirectoryName}/${compiledTemplateFileName}`,
      Body: JSON.stringify(compiledCfTemplate),
      ContentType: 'application/json',
      Metadata: {
        filesha256: fileHash,
      },
    }

    const deploymentBucketObject =
      this.serverless.service.provider.deploymentBucketObject
    if (deploymentBucketObject) {
      params = setS3UploadEncryptionOptions(params, deploymentBucketObject)
    }

    return this.provider.request('S3', 'upload', params)
  },
  async uploadStateFile() {
    log.info('Uploading State file to S3')

    const basename = this.provider.naming.getServiceStateFileName()
    const content = await fsp.readFile(
      path.join(this.serverless.serviceDir, '.serverless', basename),
      'utf-8',
    )

    const stateObject = JSON.parse(content)
    const fileHash = crypto
      .createHash('sha256')
      .update(JSON.stringify(normalizeFiles.normalizeState(stateObject)))
      .digest('base64')

    let params = {
      Bucket: this.bucketName,
      Key: `${this.serverless.service.package.artifactDirectoryName}/${basename}`,
      Body: content,
      ContentType: 'application/json',
      Metadata: { filesha256: fileHash },
    }

    const deploymentBucketObject =
      this.serverless.service.provider.deploymentBucketObject
    if (deploymentBucketObject) {
      params = setS3UploadEncryptionOptions(params, deploymentBucketObject)
    }

    return this.provider.request('S3', 'upload', params)
  },

  async getFunctionArtifactFilePaths() {
    const functionNames = this.serverless.service.getAllFunctions()
    return _.uniq(
      (
        await Promise.all(
          functionNames.map(async (name) => {
            const functionObject = this.serverless.service.getFunction(name)
            if (functionObject.image) return null
            const functionArtifactFileName =
              this.provider.naming.getFunctionArtifactName(name)
            functionObject.package = functionObject.package || {}
            let artifactFilePath =
              functionObject.package.artifact ||
              this.serverless.service.package.artifact

            artifactFilePath =
              artifactFilePath &&
              path.resolve(this.serverless.serviceDir, artifactFilePath)

            if (
              !artifactFilePath ||
              (this.serverless.service.artifact &&
                !functionObject.package.artifact)
            ) {
              if (
                this.serverless.service.package.individually ||
                functionObject.package.individually
              ) {
                const artifactFileName = functionArtifactFileName
                artifactFilePath = path.join(this.packagePath, artifactFileName)
              } else {
                artifactFilePath = path.join(
                  this.packagePath,
                  this.provider.naming.getServiceArtifactName(),
                )
              }
            }
            functionObject.artifactSize = (
              await this.getFileStats(artifactFilePath)
            ).size
            return artifactFilePath
          }),
        )
      ).filter(Boolean),
    )
  },

  getLayerArtifactFilePaths() {
    const layerNames = this.serverless.service.getAllLayers()
    return layerNames
      .map((name) => {
        const layerObject = this.serverless.service.getLayer(name)
        if (layerObject.artifactAlreadyUploaded) {
          log.info(`Skipped uploading ${name}`)
          return null
        }
        return getLambdaLayerArtifactPath(
          this.packagePath,
          this.serverless.serviceDir,
          name,
          this.provider.serverless.service,
          this.provider.naming,
        )
      })
      .filter(Boolean)
  },

  async uploadFunctionsAndLayers() {
    const artifactFilePaths = [
      ...(await this.getFunctionArtifactFilePaths()),
      ...this.getLayerArtifactFilePaths(),
    ]

    const shouldReportDetailedProgress = artifactFilePaths.length > 1
    let alreadyUploadedCount = 0

    const limitedUpload = limit(
      MAX_CONCURRENT_ARTIFACTS_UPLOADS,
      async ({ filename, s3KeyDirname }) => {
        const stats = await this.getFileStats(filename)
        const fileName = path.basename(filename)
        log.info(
          `Uploading service ${fileName} file to S3 (${filesize(stats.size)})`,
        )
        if (shouldReportDetailedProgress) {
          progress
            .get(`upload:${fileName}`)
            .notice(
              `Uploading service ${fileName} file to S3 (${filesize(
                stats.size,
              )})`,
            )
        }
        const result = await this.uploadZipFile({
          filename,
          s3KeyDirname,
        })
        alreadyUploadedCount += 1
        if (shouldReportDetailedProgress) {
          progress
            .get('main')
            .notice(
              `Uploading (${alreadyUploadedCount}/${artifactFilePaths.length})`,
            )
          progress.get(`upload:${fileName}`).remove()
        }
        return result
      },
    )
    const uploadPromises = artifactFilePaths.map((filename) =>
      limitedUpload({
        filename,
        s3KeyDirname: this.serverless.service.package.artifactDirectoryName,
      }),
    )
    await Promise.all(uploadPromises)
  },

  async uploadCustomResources() {
    const artifactFilePath = path.join(
      this.serverless.serviceDir,
      '.serverless',
      this.provider.naming.getCustomResourcesArtifactName(),
    )

    if (this.serverless.utils.fileExistsSync(artifactFilePath)) {
      log.info('Uploading custom CloudFormation resources')
      await this.uploadZipFile({
        filename: artifactFilePath,
        s3KeyDirname: this.serverless.service.package.artifactDirectoryName,
      })
    }
  },
}
