var _ = require('lodash'),
    aws4 = require('aws4'),
    crypto = require('crypto'),
    sdk = require('postman-collection'),
    urlEncoder = require('postman-url-encoder'),
    bodyBuilder = require('../requester/core-body-builder'),

    RequestBody = sdk.RequestBody,

    X_AMZ_PREFIX = 'X-Amz-',
    BODY_HASH_HEADER = 'X-Amz-Content-Sha256',

    /**
     * Calculates body hash with given algorithm and digestEncoding.
     *
     * @todo This function can also be used in Digest auth so that it works correctly for urlencoded and file body types
     *
     * @param {RequestBody} body
     * @param {String} algorithm
     * @param {String} digestEncoding
     * @param {Function} callback
     */
    computeBodyHash = function (body, algorithm, digestEncoding, callback) {
        if (!(body && algorithm && digestEncoding) || body.isEmpty()) { return callback(); }

        var hash = crypto.createHash(algorithm),
            originalReadStream,
            rawBody,
            urlencodedBody,
            graphqlBody;

        if (body.mode === RequestBody.MODES.raw) {
            rawBody = bodyBuilder.raw(body.raw).body;
            hash.update(rawBody);

            return callback(hash.digest(digestEncoding));
        }

        if (body.mode === RequestBody.MODES.urlencoded) {
            urlencodedBody = bodyBuilder.urlencoded(body.urlencoded).form;
            urlencodedBody = urlEncoder.encodeQueryString(urlencodedBody);
            hash.update(urlencodedBody);

            return callback(hash.digest(digestEncoding));
        }

        if (body.mode === RequestBody.MODES.file) {
            originalReadStream = _.get(body, 'file.content');

            if (!originalReadStream) {
                return callback();
            }

            return originalReadStream.cloneReadStream(function (err, clonedStream) {
                if (err) { return callback(); }

                clonedStream.on('data', function (chunk) {
                    hash.update(chunk);
                });

                clonedStream.on('end', function () {
                    callback(hash.digest(digestEncoding));
                });
            });
        }

        if (body.mode === RequestBody.MODES.graphql) {
            graphqlBody = bodyBuilder.graphql(body.graphql).body;
            hash.update(graphqlBody);

            return callback(hash.digest(digestEncoding));
        }

        // @todo: formdata body type requires adding new data to form instead of setting headers for AWS auth.
        //        Figure out how to do that. See below link:
        //        AWS auth with formdata: https://docs.aws.amazon.com/AmazonS3/latest/API/sigv4-post-example.html

        // ensure that callback is called if body.mode doesn't match with any of the above modes
        return callback();
    };

/**
 * @implements {AuthHandlerInterface}
 */
module.exports = {
    /**
     * @property {AuthHandlerInterface~AuthManifest}
     */
    manifest: {
        info: {
            name: 'awsv4',
            version: '1.0.0'
        },
        updates: [
            {
                property: 'Host',
                type: 'header'
            },
            {
                property: 'Authorization',
                type: 'header'
            },
            {
                property: 'X-Amz-Date',
                type: 'header'
            },
            {
                property: 'X-Amz-Security-Token',
                type: 'header'
            },
            {
                property: 'X-Amz-Content-Sha256',
                type: 'header'
            },
            {
                property: 'X-Amz-Security-Token',
                type: 'url.param'
            },
            {
                property: 'X-Amz-Expires',
                type: 'url.param'
            },
            {
                property: 'X-Amz-Date',
                type: 'url.param'
            },
            {
                property: 'X-Amz-Algorithm',
                type: 'url.param'
            },
            {
                property: 'X-Amz-Credential',
                type: 'url.param'
            },
            {
                property: 'X-Amz-SignedHeaders',
                type: 'url.param'
            },
            {
                property: 'X-Amz-Signature',
                type: 'url.param'
            }
        ]
    },

    /**
     * Initializes a item (fetches all required parameters, etc) before the actual authorization step.
     *
     * @param {AuthInterface} auth
     * @param {Response} response
     * @param {AuthHandlerInterface~authInitHookCallback} done
     */
    init: function (auth, response, done) {
        done(null);
    },

    /**
     * Checks the item, and fetches any parameters that are not already provided.
     *
     * @param {AuthInterface} auth
     * @param {AuthHandlerInterface~authPreHookCallback} done
     */
    pre: function (auth, done) {
        done(null, true);
    },

    /**
     * Verifies whether the request was successful after being sent.
     *
     * @param {AuthInterface} auth
     * @param {Requester} response
     * @param {AuthHandlerInterface~authPostHookCallback} done
     */
    post: function (auth, response, done) {
        done(null, true);
    },

    /**
     * Generates the signature and adds auth data to the request as additional headers/query params.
     * AWS v4 auth mandates that a content type header be present in each request.
     *
     * @param {Request} request request to add auth data
     * @param {Object} params data required for auth
     * @param {Object} params.credentials Should contain the AWS credentials, "accessKeyId" and "secretAccessKey"
     * @param {String} params.host Contains the host name for the request
     * @param {String} params.path Contains the complete path, with query string as well, e.g: /something/kane?hi=ho
     * @param {String} params.service The name of the AWS service
     * @param {String} params.region AWS region
     * @param {String} params.method Request method
     * @param {String} params.body Stringified request body
     * @param {Object} params.headers Each key should be a header key, and the value should be a header value
     * @param {Boolean} params.signQuery Add auth data to query params if true, otherwise add it to headers
     */
    addAuthDataToRequest: function (request, params) {
        var signedData = aws4.sign(params, params.credentials);

        if (params.signQuery) {
            _.forEach(sdk.Url.parse(signedData.path).query, function (param) {
                // only add additional AWS specific params to request
                if (_.startsWith(param.key, X_AMZ_PREFIX) && !request.url.query.has(param.key)) {
                    param.system = true;
                    request.url.query.add(param);
                }
            });
        }

        _.forEach(signedData.headers, function (value, key) {
            request.upsertHeader({
                key: key,
                value: value,
                system: true
            });
        });
    },

    /**
     * Signs a request.
     *
     * @param {AuthInterface} auth
     * @param {Request} request
     * @param {AuthHandlerInterface~authSignHookCallback} done
     */
    sign: function (auth, request, done) {
        var self = this,
            params = auth.get([
                'accessKey',
                'secretKey',
                'sessionToken',
                'service',
                'region',
                'addAuthDataToQuery'
            ]),
            url = urlEncoder.toNodeUrl(request.url),
            dataToSign;

        // Clean up the request (if needed)
        request.removeHeader('Authorization', {ignoreCase: true});
        request.removeHeader('X-Amz-Date', {ignoreCase: true});
        request.removeHeader('X-Amz-Security-Token', {ignoreCase: true});
        request.removeHeader('X-Amz-Content-Sha256', {ignoreCase: true});

        // Not removing `X-Amz-Expires` from params here allowing user to override
        // default value
        request.removeQueryParams([
            'X-Amz-Security-Token',
            'X-Amz-Date',
            'X-Amz-Algorithm',
            'X-Amz-Credential',
            'X-Amz-SignedHeaders',
            'X-Amz-Signature'
        ]);

        dataToSign = {
            credentials: {
                accessKeyId: params.accessKey,
                secretAccessKey: params.secretKey,
                sessionToken: params.sessionToken || undefined
            },
            host: url.host,
            path: url.path, // path = pathname + query
            service: params.service || 'execute-api', // AWS API Gateway is the default service.
            region: params.region || 'us-east-1',
            method: request.method,
            body: undefined, // no need to give body since we are setting 'X-Amz-Content-Sha256' header
            headers: _.transform(request.getHeaders({enabled: true}), function (accumulator, value, key) {
                accumulator[key] = value;
            }, {}),
            signQuery: params.addAuthDataToQuery
        };

        // Removed the code which was adding content-type header if it is not there in the request. Because
        // aws4 does not require content-type header. It is only mandatory to include content-type header in signature
        // calculation if it is there in the request.
        // Refer: https://docs.aws.amazon.com/AmazonS3/latest/API/sig-v4-header-based-auth.html#canonical-request

        // body hash is not required when adding auth data to qury params
        // @see: https://docs.aws.amazon.com/AmazonS3/latest/API/sigv4-query-string-auth.html
        if (params.addAuthDataToQuery) {
            self.addAuthDataToRequest(request, dataToSign);

            return done();
        }

        // aws4 module can't calculate body hash for body with ReadStream.
        // So we calculate it our self and set 'X-Amz-Content-Sha256' header which will be used by aws4 module
        // to calculate the signature.
        computeBodyHash(request.body, 'sha256', 'hex', function (bodyHash) {
            if (bodyHash) {
                request.upsertHeader({
                    key: BODY_HASH_HEADER,
                    value: bodyHash,
                    system: true
                });

                dataToSign.headers[BODY_HASH_HEADER] = bodyHash;
            }

            self.addAuthDataToRequest(request, dataToSign);

            return done();
        });
    }
};
