/* eslint-disable object-shorthand */
var _ = require('lodash').noConflict(),
    v1Common = require('../common/v1'),
    v2Common = require('../common/v2'),
    util = require('../util'),
    url = require('../url'),

    Builders = function (options) {
        this.options = options || {};
    },
    script = function (entityV1, listen, key) {
        return {
            listen: listen,
            script: {
                type: 'text/javascript',
                exec: _.isString(entityV1[key]) ? entityV1[key].split('\n') : entityV1[key]
            }
        };
    },

    authIdMap = {
        apikey: 'apikeyAuth',
        awsSigV4: 'awsSigV4',
        basic: 'basicAuth',
        bearer: 'bearerAuth',
        digest: 'digestAuth',
        hawk: 'hawkAuth',
        ntlm: 'ntlmAuth',
        oAuth1: 'oAuth1',
        oAuth2: 'oAuth2'
    },

    /**
     * Normalizes `description` field of an entity.
     * If `description` field is absent, this is a no-op.
     * Will mutate the entity.
     *
     * @param {Object} entity - Wrapper object, possibly containing a description field
     * @param {Object} builder - Builder instance that will be called to perform normalization
     * @param {Object} utilOptions - Options to be passed to util fn
     */
    normalizeDescription = function (entity, builder, utilOptions) {
        var retainEmptyValues = _.get(utilOptions, 'retainEmptyValues');

        if (_.has(entity, 'description')) {
            entity.description = builder.description(entity.description);
        }
        util.cleanEmptyValue(entity, 'description', retainEmptyValues);

        return entity;
    };

_.assign(Builders.prototype, {

    /**
     * Normalizes inherited v1 auth manifests.
     *
     * @param {Object} entityV1 - A v1 compliant wrapped auth manifest.
     * @param {?Object} options - The set of options for the current auth cleansing operation.
     * @param {?Boolean} [options.includeNoauth=false] - When set to true, noauth is set to ''.
     *
     * @returns {Object} - A v1 compliant set of auth helper attributes.
     */
    auth: function (entityV1, options) {
        if (!entityV1) { return; }

        var auth,
            params,
            mapper,
            currentHelper,
            helperAttributes,
            prioritizeV2 = this.options.prioritizeV2;

        // if prioritize v2 is true, use auth as the source of truth
        if (util.notLegacy(entityV1, 'auth') || (entityV1.auth && prioritizeV2)) {
            return util.sanitizeAuthArray(entityV1, options);
        }
        if ((entityV1.currentHelper === null) || (entityV1.currentHelper === 'normal')) { return null; }

        currentHelper = entityV1.currentHelper;
        helperAttributes = entityV1.helperAttributes;

        // if noDefaults is false and there is no currentHelper, bail out
        if (!(currentHelper || this.options.noDefaults)) { return; }
        // if there is a currentHelper without helperAttributes, bail out.
        if (currentHelper && !helperAttributes) { return this.options.noDefaults ? undefined : null; }

        !currentHelper && (currentHelper = authIdMap[helperAttributes && helperAttributes.id]);
        auth = { type: v1Common.authMap[currentHelper] };
        mapper = util.authMappersFromLegacy[currentHelper];

        // @todo: Change this to support custom auth helpers
        mapper && helperAttributes && (params = mapper(helperAttributes)) && (auth[auth.type] = params);

        return util.authMapToArray({ auth: auth }, options);
    },

    /**
     * Normalizes v1 collection events.
     *
     * @param {Object} entityV1 - The v1 entity to be normalized.
     * @returns {Array|null} - The normalized events.
     */
    events: function (entityV1) {
        if (!entityV1) { return; }
        if ((util.notLegacy(entityV1, 'event') || this.options.prioritizeV2) && !_.isEmpty(entityV1.events)) {
            // @todo: Improve this to order prerequest events before test events
            _.forEach(entityV1.events, function (event) {
                !event.listen && (event.listen = 'test');

                if (event.script) {
                    !event.script.type && (event.script.type = 'text/javascript');

                    // @todo: Add support for src
                    _.isString(event.script.exec) && (event.script.exec = event.script.exec.split('\n'));
                }
            });

            return entityV1.events;
        }

        var events = [];

        entityV1.preRequestScript && events.push(script(entityV1, 'prerequest', 'preRequestScript'));
        entityV1.tests && events.push(script(entityV1, 'test', 'tests'));

        if (events.length) { return events; }

        // retain `null` events
        if (entityV1.events === null) { return null; }
    },

    /**
     * Facilitates sanitized variable transformations across all levels for v1 collection normalization.
     *
     * @param {Object} entity - The wrapper object containing variable definitions.
     * @param {?Object} options - The set of options for the current variable transformation.
     * @param {?Object} options.fallback - The set of fallback values to be applied when no variables exist.
     * @param {?Boolean} options.noDefaults - When set to true, no defaults are applied.
     * @param {?Boolean} options.retainIds - When set to true, ids are left as is.
     * @returns {Object[]} - The set of sanitized variables.
     */
    variables: function (entity, options) {
        var self = this,
            // Use builder's own options if override is not requested
            results = util.handleVars(entity, options || this.options);

        // Normalize descriptions that may have been passed in as objects
        results = _.map(results, function (item) {
            return normalizeDescription(item, self, options || self.options);
        });

        if (results.length) {
            return results;
        }
    },

    /**
     * Sanitizes request v1 data.
     *
     * @param {Object} requestV1 - The wrapper v1 request object around the data list to be sanitized.
     * @returns {Object[]} - The normalized list of request body parameters.
     */
    data: function (requestV1) {
        if (!requestV1) { return; }

        var self = this,
            mode = requestV1.dataMode,
            noDefaults = this.options.noDefaults,
            retainEmptyValues = this.options.retainEmptyValues;

        if ((!mode || mode === 'binary') && !noDefaults) {
            return retainEmptyValues ? [] : undefined;
        }
        if (!requestV1.data) { return; }

        _.isArray(requestV1.data) && _.forEach(requestV1.data, function (datum) {
            if (datum.type === 'file' && (_.has(datum, 'value') || !noDefaults)) {
                datum.value = (_.isString(datum.value) || _.isArray(datum.value)) ? datum.value : null;
            }

            normalizeDescription(datum, self, self.options);
        });

        return requestV1.data;
    },

    /**
     * Normalizes a list of header data from the incoming raw v1 request.
     *
     * @param {Object} requestV1 - The raw v1 request object.
     * @returns {Object[]} - The normalized list of header datum values.
     */
    headerData: function (requestV1) {
        var self = this,
            normalizedHeaderData;

        if (!requestV1) { return; }
        if (requestV1.headers && _.isEmpty(requestV1.headerData)) {
            // this converts a newline concatenated string of headers to an array, so there are no descriptions
            return v1Common.parseHeaders(requestV1.headers, true);
        }

        // however, if non empty headerData already exists, sanitize it.
        normalizedHeaderData = _.map(requestV1.headerData, function (entity) {
            return normalizeDescription(entity, self, self.options);
        });

        if (normalizedHeaderData.length) {
            return normalizedHeaderData;
        }
    },

    queryParams: function (requestV1) {
        if (!requestV1) { return; }

        var self = this,
            normalizedQueryParams,
            urlObj;

        if (!requestV1.queryParams) {
            return requestV1.url && (urlObj = url.parse(requestV1.url)) && urlObj.query;
        }

        normalizedQueryParams = _.map(requestV1.queryParams, function (entity) {
            return normalizeDescription(entity, self, self.options);
        });

        if (normalizedQueryParams.length) {
            return normalizedQueryParams;
        }
    },

    /**
     * Facilitates sanitized variable transformations across all levels for v1 collection normalization.
     *
     * @param {Object} entity - The wrapper object containing variable definitions.
     * @param {?Object} [options] - The set of options for the current variable transformation.
     * @param {?Object} [options.fallback] - The set of fallback values to be applied when no variables exist.
     * @param {?Boolean} [options.noDefaults] - When set to true, no defaults are applied.
     * @param {?Boolean} [options.retainEmptyValues] - When set to true, empty values are set to null instead of being
     * removed.
     * @param {?Boolean} [options.retainIds] - When set to true, ids are left as is.
     * @returns {Object[]} - The set of sanitized variables.
     */
    pathVariableData: function (entity, options) {
        var self = this,
            results = util.handleVars(entity, options, { isV1: true });

        // Normalize descriptions that may have been passed in as objects
        results = _.map(results, function (item) {
            return normalizeDescription(item, self, self.options);
        });

        if (results.length) {
            return results;
        }
    },

    /**
     * Normalizes a potentially raw v1 request object.
     *
     * @param {Object} requestV1 - The potentially raw v1 request object.
     * @param {?String} collectionId - A unique identifier for the v1 collection.
     * @param {?Boolean} [skipResponses=false] - When set to true, saved responses will be excluded from the result..
     * @returns {Object} - The normalized v1 request object.
     */
    request: function (requestV1, collectionId, skipResponses) {
        if (!requestV1) { return; }

        var map,
            auth,
            tests,
            events,
            mapper,
            variables,
            self = this,
            helperAttributes,
            preRequestScript,
            options = this.options,
            noDefaults = options.noDefaults,
            retainEmpty = options.retainEmptyValues,
            varOpts = { noDefaults: options.noDefaults, retainIds: options.retainIds },
            units = ['queryParams', 'pathVariableData', 'headerData', 'data'];

        if (!skipResponses) {
            units.push('responses');
            units.push('responses_order');
        }

        // if noDefaults is true, do not replace the id
        // else
        // if id is falsy, replace the id
        // if retainIds is false, replace the id
        !((options.retainIds && requestV1.id) || options.noDefaults) && (requestV1.id = util.uid());

        normalizeDescription(requestV1, self, self.options);

        units.forEach(function (unit) {
            var result = self[unit](requestV1, self.options);

            result && (requestV1[unit] = result);
        });

        if (requestV1.dataDisabled) { requestV1.dataDisabled = true; }
        else if (retainEmpty) { requestV1.dataDisabled = false; }
        else { delete requestV1.dataDisabled; }

        // remove invalid protocolProfileBehavior property from requestV1
        !util.addProtocolProfileBehavior(requestV1) && delete requestV1.protocolProfileBehavior;

        collectionId && !noDefaults && (requestV1.collectionId = collectionId);

        // normalized v1 requests should not have falsy helperAttributes or currentHelper
        if (_.has(requestV1, 'currentHelper')) {
            (requestV1.currentHelper === 'normal') && (requestV1.currentHelper = null);

            if (!requestV1.currentHelper) {
                (requestV1.currentHelper !== null) && (requestV1.currentHelper = null);

                // @todo: Should currentHelper be recreated from helperAttributes.id if falsy?
                requestV1.helperAttributes = null;
            }
        }

        auth = self.auth(requestV1);

        if (auth) {
            requestV1.auth = auth;

            if (_.has(requestV1, 'helperAttributes') && !requestV1.currentHelper) {
                requestV1.currentHelper = authIdMap[auth.type];
            }
        }
        else if (auth === null) { // eslint-disable-line security/detect-possible-timing-attacks
            requestV1.auth = requestV1.currentHelper = requestV1.helperAttributes = null;
        }
        else { delete requestV1.auth; }

        events = self.events(requestV1);
        if (events || events === null) {
            requestV1.events = events;
        }
        else {
            delete requestV1.events;
        }

        variables = self.variables(requestV1, varOpts);
        if (variables) {
            requestV1.variables = variables;
        }
        else {
            delete requestV1.variables;
        }

        if (requestV1.auth && (util.notLegacy(requestV1, 'auth') || options.prioritizeV2)) {
            requestV1.currentHelper = v2Common.authMap[requestV1.auth.type];
            (requestV1.currentHelper === null) && (requestV1.helperAttributes = null);

            mapper = util.authMappersFromCurrent[requestV1.currentHelper];

            if (mapper) {
                (map = util.authArrayToMap(requestV1)) && (helperAttributes = mapper(map[requestV1.auth.type]));
                helperAttributes && (requestV1.helperAttributes = helperAttributes);
            }
        }

        if (requestV1.events && (util.notLegacy(requestV1, 'event') || options.prioritizeV2)) {
            tests = preRequestScript = '';

            _.forEach(requestV1.events, function (event) {
                var exec = event && event.script && event.script.exec;

                if (!_.isArray(exec)) { return; }
                if (event.listen === 'prerequest') {
                    preRequestScript += exec.join('\n');
                }
                else if (event.listen === 'test') {
                    tests += exec.join('\n');
                }
            });

            requestV1.preRequestScript = preRequestScript ? preRequestScript : null;
            requestV1.tests = tests ? tests : null;
        }

        // prune
        ['preRequestScript', 'tests'].forEach(function (script) {
            if (_.has(requestV1, script) && !requestV1[script] && requestV1[script] !== null) {
                delete requestV1[script];
            }
        });

        return requestV1;
    },

    /**
     * Normalizes a potentially raw v1 response object.
     *
     * @param {Object} responseV1 - The potentially raw v1 response object.
     * @returns {Object} - The normalized v1 response object.
     */
    response: function (responseV1) {
        var self = this;

        // if noDefaults is true, do not replace the id
        // else
        // if id is falsy, replace the id
        // if retainIds is false, replace the id
        !((self.options.retainIds && responseV1.id) || self.options.noDefaults) && (responseV1.id = util.uid());

        // the true in the next line ensures that we don't recursively go on processing responses in a request.
        responseV1.request = self.request(responseV1.request, undefined, true);

        !responseV1.language && (responseV1.language = 'Text');
        !responseV1.previewType && (responseV1.previewType = 'html');

        _.isEmpty(responseV1.cookies) && (delete responseV1.cookies);

        return responseV1;
    },

    responses: function (requestV1) {
        if (_.isEmpty(requestV1 && requestV1.responses)) { return; }

        var self = this;

        requestV1.responses.forEach(function (response) {
            self.response(response);
        });

        return requestV1.responses;
    },

    /**
     * Normalizes a request order list.
     *
     * @param {Object} entityV1 - An object containing a potentially raw list of folder ids.
     * @returns {Array} - The normalized list of folder ids.
     */
    order: function (entityV1) {
        return !this.options.noDefaults && _.compact(entityV1 && entityV1.order);
    },

    /**
     * Normalizes a folder order list.
     *
     * @param {Object} entityV1 - An object containing a potentially raw list of folder ids.
     * @returns {Array} - The normalized list of folder ids.
     */
    folders_order: function (entityV1) {
        return !this.options.noDefaults && _.compact(entityV1 && entityV1.folders_order);
    },

    /**
     * Normalizes a response order list.
     *
     * @param {Object} entityV1 - An object containing a potentially raw list of response ids.
     * @returns {Array} - The normalized list of response ids.
     */
    responses_order: function (entityV1) {
        return !this.options.noDefaults && _.compact(entityV1 && entityV1.responses_order);
    },

    /**
     * Normalizes a potentially raw v1 folders list.
     *
     * @param {Object} collectionV1 - The potentially raw v1 collection object.
     * @returns {Object[]} - The normalized v1 collection folders list.
     */
    folders: function (collectionV1) {
        if (_.isEmpty(collectionV1 && collectionV1.folders)) { return; }

        var auth,
            events,
            variables,
            self = this,
            order,
            foldersOrder,
            retainEmpty = self.options.retainEmptyValues,
            varOpts = { noDefaults: self.options.noDefaults, retainIds: self.options.retainIds };

        _.forEach(collectionV1.folders, function (folder) {
            if (!folder) { return; }

            // if noDefaults is true, do not replace the id
            // else
            // if id is falsy, replace the id
            // if retainIds is false, replace the id
            !((self.options.retainIds && folder.id) || self.options.noDefaults) && (folder.id = util.uid());

            folder.description = self.description(folder.description);
            util.cleanEmptyValue(folder, 'description', retainEmpty);

            // remove invalid protocolProfileBehavior property
            !util.addProtocolProfileBehavior(folder) && delete folder.protocolProfileBehavior;

            auth = self.auth(folder);

            !_.isEmpty((order = self.order(folder))) && (folder.order = order);
            !_.isEmpty((foldersOrder = self.folders_order(folder))) && (folder.folders_order = foldersOrder);

            (auth || (auth === null)) && (folder.auth = auth);
            (events = self.events(folder)) && (folder.events = events);
            (variables = self.variables(folder, varOpts)) && (folder.variables = variables);
        });

        return _.compact(collectionV1.folders);
    },

    /**
     * Normalizes a potentially raw v1 request object.
     *
     * @param {Object} collectionV1 - The potentially raw v1 collection object.
     * @returns {Object[]|*} - The normalized v1 request list.
     */
    requests: function (collectionV1) {
        if (_.isEmpty(collectionV1 && collectionV1.requests)) { return; }

        var self = this;

        collectionV1.requests.forEach(function (request) {
            self.request(request);
        });

        return _.compact(collectionV1.requests);
    },

    /**
     * Creates the v1.0.0 compatible description string.
     *
     * @param {Object} maybeObjectDescription - The description to be converted
     *
     * @returns {String} - The resultant v1 description.
     */
    description: function (maybeObjectDescription) {
        var description,
            retainEmpty = _.get(this.options, 'retainEmptyValues'),
            createDefaultValue = !_.get(this.options, 'noDefaults', false);

        if (_.isObject(maybeObjectDescription)) {
            description = _.toString(_.get(maybeObjectDescription, 'content'));
        }
        else {
            description = maybeObjectDescription;
        }

        if (description) {
            return description;
        }
        else if (description === undefined && createDefaultValue) {
            return null;
        }
        else if (_.isEmpty(description) && retainEmpty) {
            return null;
        }

        return undefined;
    }
});

module.exports = {

    /**
     * Normalizes a single v1 request.
     *
     * @param {Object} request - The v1 request to be normalized.
     * @param {Object} options - The set of options for the current normalization.
     * @param {?Boolean} [options.mutate=false] - When set to true, normalization is done in place.
     * @param {?Boolean} [options.noDefaults=false] - When set to true, sensible defaults are not added.
     * @param {?Boolean} [options.prioritizeV2=false] - When set to true, v2 style properties are checked first.
     * @param {?Boolean} [options.retainEmptyValues=false] - When set to true, empty values are set to '' instead of
     * being removed.
     * @param {Function} callback - A function that is invoked when the normalization has completed.
     * @returns {*}
     */
    normalizeSingle: function (request, options, callback) {
        var err,
            normalized,
            builders = new Builders(options);

        // At this stage, mutate will not be passed ordinarily. Hence, the falsy nature of options.mutate can be used
        // to selectively clone the request.
        options && !options.mutate && (request = _.cloneDeep(request));

        try { normalized = builders.request(request); }
        catch (e) { err = e; }

        if (callback) { return callback(err, normalized); }

        if (err) { throw err; }

        return normalized;
    },

    /**
     * Normalizes a single v1 response.
     *
     * @param {Object} response - The v1 request to be normalized.
     * @param {Object} options - The set of options for the current normalization.
     * @param {?Boolean} [options.mutate=false] - When set to true, normalization is done in place.
     * @param {?Boolean} [options.noDefaults=false] - When set to true, sensible defaults are not added.
     * @param {?Boolean} [options.prioritizeV2=false] - When set to true, v2 style properties are checked first.
     * @param {?Boolean} [options.retainEmptyValues=false] - When set to true, empty values are set to '' instead of
     * being removed.
     * @param {Function} callback - A function that is invoked when the normalization has completed.
     * @returns {*}
     */
    normalizeResponse: function (response, options, callback) {
        var err,
            normalized,
            builders = new Builders(options);

        // At this stage, mutate will not be passed ordinarily. Hence, the falsy nature of options.mutate can be used
        // to selectively clone the response.
        options && !options.mutate && (response = _.cloneDeep(response));

        try { normalized = builders.response(response); }
        catch (e) { err = e; }

        if (callback) { return callback(err, normalized); }

        if (err) { throw err; }

        return normalized;
    },

    /**
     * Converts a V1 collection to a V2 collection (performs ID replacement, etc as necessary).
     *
     * @param {Object} collection - The v1 collection to be normalized.
     * @param {Object} options - The options for the current normalization sequence.
     * @param {?Boolean} [options.mutate=false] - When set to true, normalization is done in place.
     * @param {?Boolean} [options.noDefaults=false] - When set to true, sensible defaults are not added.
     * @param {?Boolean} [options.prioritizeV2=false] - When set to true, v2 style properties are checked first.
     * @param {?Boolean} [options.retainEmptyValues=false] - When set to true, empty values are set to '' instead of
     * being removed.
     * @param {Function} callback - A function invoked to indicate that the normalization has completed.
     * @returns {*}
     */
    normalize: function (collection, options, callback) {
        // At this stage, mutate will not be passed ordinarily. Hence, the falsy nature of options.mutate can be used
        // to selectively clone the collection.
        options && !options.mutate && (collection = _.cloneDeep(collection));

        var auth,
            authOptions = { excludeNoauth: true },
            builders = new Builders(options),
            units = ['events', 'variables', 'order', 'folders_order', 'folders', 'requests'];

        // if noDefaults is true, do not replace the id
        // else
        // if id is falsy, replace the id
        // if retainIds is false, replace the id
        !((options.retainIds && collection.id) || options.noDefaults) && (collection.id = util.uid());

        normalizeDescription(collection, builders, options);

        // remove invalid protocolProfileBehavior property
        !util.addProtocolProfileBehavior(collection) && delete collection.protocolProfileBehavior;

        try {
            auth = builders.auth(collection, authOptions);
            if (auth || (options.retainEmptyValues && auth === null)) {
                collection.auth = auth;
            }
            else {
                delete collection.auth;
            }

            units.forEach(function (unit) {
                var result,
                    _options;

                if (unit === 'variables') {
                    _options = { retainIds: options.retainIds, noDefaults: options.noDefaults };
                }

                if (!_.isEmpty(result = builders[unit](collection, _options)) || (unit === 'folders')) {
                    collection[unit] = result;
                }
            });
        }
        catch (e) {
            if (callback) { return callback(e, null); }
            throw e;
        }

        if (callback) { return callback(null, collection); }

        return collection;
    }
};
