'use strict';

const _ = require('lodash');
const fs = require('fs');
const path = require('path');
const crypto = require('crypto');
const BbPromise = require('bluebird');
const filesize = require('filesize');
const normalizeFiles = require('../../lib/normalizeFiles');
const getLambdaLayerArtifactPath = require('../../utils/getLambdaLayerArtifactPath');

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

module.exports = {
  async uploadArtifacts() {
    return BbPromise.bind(this)
      .then(this.uploadCloudFormationFile)
      .then(this.uploadFunctionsAndLayers)
      .then(this.uploadCustomResources);
  },

  async uploadCloudFormationFile() {
    this.serverless.cli.log('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 = setServersideEncryptionOptions(params, deploymentBucketObject);
    }

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

  async uploadZipFile(artifactFilePath) {
    const fileName = artifactFilePath.split(path.sep).pop();

    // TODO refactor to be async (use util function to compute checksum async)
    const data = fs.readFileSync(artifactFilePath);
    const fileHash = crypto.createHash('sha256').update(data).digest('base64');

    const artifactStream = fs.createReadStream(artifactFilePath);
    // As AWS SDK request might be postponed (requests are queued)
    // eventual stream error may crash the process (it's thrown as uncaught if not observed).
    // Below lines prevent that
    let streamError;
    artifactStream.on('error', (error) => (streamError = error));

    let params = {
      Bucket: this.bucketName,
      Key: `${this.serverless.service.package.artifactDirectoryName}/${fileName}`,
      Body: artifactStream,
      ContentType: 'application/zip',
      Metadata: {
        filesha256: fileHash,
      },
    };

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

    const response = await this.provider.request('S3', 'upload', params);
    // Interestingly, if request handling was queued, and stream errored (before being consumed by
    // AWS SDK) then SDK call succeeds without actually uploading a file to S3 bucket.
    // Below line ensures that eventual stream error is communicated
    if (streamError) throw streamError;
    return response;
  },

  async uploadFunctionsAndLayers() {
    this.serverless.cli.log('Uploading artifacts...');

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

          if (
            !artifactFilePath ||
            (this.serverless.service.artifact && !functionObject.package.artifact)
          ) {
            if (
              this.serverless.service.package.individually ||
              functionObject.package.individually
            ) {
              const artifactFileName = functionArtifactFileName;
              return path.join(this.packagePath, artifactFileName);
            }
            return path.join(this.packagePath, this.provider.naming.getServiceArtifactName());
          }

          return artifactFilePath;
        })
        .filter(Boolean)
    );

    const layerNames = this.serverless.service.getAllLayers();
    artifactFilePaths = artifactFilePaths.concat(
      layerNames
        .map((name) => {
          const layerObject = this.serverless.service.getLayer(name);
          if (layerObject.artifactAlreadyUploaded) {
            this.serverless.cli.log(`Skip uploading ${name}`);
            return null;
          }
          return getLambdaLayerArtifactPath(
            this.packagePath,
            name,
            this.provider.serverless.service,
            this.provider.naming
          );
        })
        .filter(Boolean)
    );

    return BbPromise.map(
      artifactFilePaths,
      (artifactFilePath) => {
        const stats = fs.statSync(artifactFilePath);
        const fileName = path.basename(artifactFilePath);
        this.serverless.cli.log(
          `Uploading service ${fileName} file to S3 (${filesize(stats.size)})...`
        );
        return this.uploadZipFile(artifactFilePath);
      },
      { concurrency: MAX_CONCURRENT_ARTIFACTS_UPLOADS }
    );
  },

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

    if (this.serverless.utils.fileExistsSync(artifactFilePath)) {
      this.serverless.cli.log('Uploading custom CloudFormation resources...');
      return this.uploadZipFile(artifactFilePath);
    }

    return BbPromise.resolve();
  },
};

function setServersideEncryptionOptions(putParams, deploymentBucketOptions) {
  const encryptionFields = [
    ['serverSideEncryption', 'ServerSideEncryption'],
    ['sseCustomerAlgorithim', 'SSECustomerAlgorithm'],
    ['sseCustomerKey', 'SSECustomerKey'],
    ['sseCustomerKeyMD5', 'SSECustomerKeyMD5'],
    ['sseKMSKeyId', 'SSEKMSKeyId'],
  ];

  const params = putParams;

  encryptionFields.forEach((element) => {
    if (deploymentBucketOptions[element[0]]) {
      params[element[1]] = deploymentBucketOptions[element[0]];
    }
  }, this);

  return params;
}
