/* eslint-disable object-shorthand */
var _ = require('lodash').noConflict(),
    util = require('../../util'),

    v2Common = require('../../common/v2'),

    Builders = function (options) {
        this.options = options || {};
    };

_.assign(Builders.prototype, {

    /**
     * Converts v2 style auth manifests into their v1 equivalents.
     *
     * @param {Object} entityV2  - The v1 auth manifest to be transformed into v1.
     * @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 null.
     *
     * @returns {Object} The transformed auth object.
     */
    auth: function (entityV2, options) {
        return util.authMapToArray(entityV2, options);
    },

    /**
     * Constructs a V1 "events" object from a V2 Postman entity
     *
     * @param {Object} entityV2 - The v2 event object to be converted.
     * @returns {Object[]} - The resultant v1 script definition.
     */
    events: function (entityV2) {
        // events is treated as the source of truth in v1, so handle that first and bail out.
        var source = entityV2.events || entityV2.event;

        if (_.isArray(source)) {
            // @todo: Improve this to order prerequest events before test events
            _.forEach(source, 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 source;
        }
    },

    /**
     * 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) {
        return util.handleVars(entity, options);
    },

    /**
     * Extracts all subfolders from a v2.0.0 collection or folder
     *
     * @param {Object} folderOrCollection - Thw entity to extract child items from.
     * @returns {Object[]} - The list of extracted folder items.
     */
    extractFolderItems: function (folderOrCollection) {
        if (!folderOrCollection) { return; }

        var i,
            self = this,
            folders = [],
            items = folderOrCollection.item || folderOrCollection.items;

        !_.isArray(items) && (items = [items]);

        for (i = 0; i < items.length; i++) {
            if (items[i] && (items[i].items || items[i].item)) {
                folders.push(items[i]);
                folders = [].concat(folders, self.extractFolderItems(items[i]));
            }
        }

        return folders;
    },

    /**
     * Extracts all requests from a v2.0.0 collection or folder
     *
     * @param {Object} folderOrCollection - The object to extract sub-items from.
     * @returns {Object[]} - The list of extracted folder items.
     */
    extractItems: function (folderOrCollection) {
        if (!folderOrCollection) { return; }

        var i,
            self = this,
            requests = [],
            isFolder,
            items = folderOrCollection.item || folderOrCollection.items;

        !_.isArray(items) && (items = [items]);
        for (i = 0; i < items.length; i++) {
            isFolder = items[i] && (items[i].items || items[i].item);
            if (items[i]) {
                isFolder ? (requests = [].concat(requests, self.extractItems(items[i]))) : requests.push(items[i]);
            }
        }

        return requests;
    },

    /**
     * Constructs a monolithic raw HTTP header block from a V2 header array
     *
     * @param {Object} item - The wrapper object to extract headers from.
     * @returns {*|string} - The resultant header string.
     */
    headers: function (item) {
        if (!(item && item.request)) { return; }

        return _.map(item.request.headers || item.request.header, function (header) {
            return (header.disabled ? '// ' : '') + header.key + ': ' + header.value;
        }).join('\n');
    },

    /**
     * Detects the data mode from a given Postman Collection V2 item
     *
     * @param {Object} item - .
     * @returns {*|number|string}
     */
    dataMode: function (item) {
        return v2Common.modeMap[_.get(item, 'request.body.mode')];
    },

    /**
     * Returns the appropriate request data based on the data mode.
     *
     * @param {Object} item - The .
     * @returns {Object} - The converted request body.
     */
    data: function (item) {
        var self = this,
            mode = _.get(item, 'request.body.mode'),
            retainEmpty = this.options.retainEmptyValues;

        if (mode === 'raw' || mode === 'file' || mode === 'graphql' || !mode) {
            return retainEmpty ? null : undefined;
        }

        return _.map(item.request.body[mode], function (elem) {
            // Only update the value in v1 if src in v2 is non-empty
            if (elem && elem.type === 'file' && _.has(elem, 'src')) {
                elem.value = (_.isString(elem.src) || _.isArray(elem.src)) ? elem.src : null;
                delete elem.src;
            }

            // Prevents empty request body descriptions from showing up in the result, keeps collections clean.
            elem.description = self.description(elem.description);
            _.has(elem, 'disabled') && (elem.enabled = !elem.disabled);

            delete elem.disabled;

            return elem;
        });
    },

    /**
     * In case of raw request bodies, this constructs the proper raw data from a V2 item.
     *
     * @param {Object} item - The v2 request to derive body information from.
     * @returns {String} - The inferred v1 request body mode.
     */
    rawModeData: function (item) {
        var mode = _.get(item, 'request.body.mode');

        if (mode === 'raw') {
            return item.request.body.raw;
        }
        else if (mode === 'file') {
            return _.get(item.request.body, 'file.src');
        }

        return this.options.retainEmptyValues ? null : undefined;
    },

    /**
     * Returns GraphQL data if mode is set to graphql.
     *
     * @param {Object} item - The v2 request to derive information form.
     * @returns {Object} - GraphQL request body.
     */
    graphqlModeData: function (item) {
        var mode = _.get(item, 'request.body.mode');

        if (mode === 'graphql') {
            return item.request.body.graphql;
        }

        return this.options.retainEmptyValues ? null : undefined;
    },

    /**
     * Creates options for body from v2 format.
     *
     * @param {Object} item - The v2 request to derive information form.
     * @returns {Object} - The converted body options.
     */
    dataOptions: function (item) {
        var options = _.get(item, 'request.body.options'),
            bodyOptions = {},
            mode;

        if (!options) {
            return;
        }

        // Convert v2 mode to v1 mode
        for (mode in v2Common.modeMap) {
            if (!_.isEmpty(options[mode])) {
                bodyOptions[v2Common.modeMap[mode]] = options[mode];
            }
        }

        return _.isEmpty(bodyOptions) ? undefined : bodyOptions;
    },

    /**
     * Creates an object of path-variables and their values from a V2 item
     *
     * @param {Object} item - The wrapper object containing path variable information.
     * @returns {Object} - The resultant hash of path variables.
     */
    pathVariables: function (item) {
        var variable = _.get(item, 'request.url.variables') || _.get(item, 'request.url.variable');

        if (!variable) { return; }

        return _.transform(variable, function (accumulator, v) {
            accumulator[v.key || v.id] = v.value; // v2.0.0 supports both key and id, v2.1.0 will drop id support
        }, {});
    },

    /**
     * Creates a V1 URL from a V2 item
     *
     * @param {Object} item - The v2 request to extract the URL from.
     * @returns {String} - The extracted URL of the request.
     */
    url: function (item) {
        var url = _.get(item, 'request.url');

        if (_.isString(url)) {
            return url;
        }

        if (!url) {
            return '';
        }

        return util.urlunparse(url);
    },

    /**
     * Extracts test from a V2 collection
     *
     * @param {Object} item - The wrapper object to extract test code from.
     * @returns {String} - The resultant test script code.
     */
    tests: function (item) {
        var allEvents = item.events || item.event,
            events;

        // Nothing to do if the item has no associated events
        if (!allEvents) {
            return;
        }

        events = _.filter(allEvents, { listen: 'test' });

        return _.map(events, function (event) {
            var tests = _.get(event, 'script.exec');

            // @todo: Possible dead code, remove when confirmed
            if (_.isArray(tests)) {
                tests = tests.join('\n');
            }

            return tests;
        }).join('\n');
    },

    /**
     * Extracts the pre-request script from an Item
     *
     * @param {Object} item - The wrapper object to extract pre-request code from.
     * @returns {String} - The resultant pre-request script code.
     */
    preRequestScript: function (item) {
        var allEvents = item.events || item.event,
            events;

        // Nothing to do if the item has no associated events
        if (!allEvents) {
            return;
        }

        events = _.filter(allEvents, { listen: 'prerequest' });

        return _.map(events, function (event) {
            var tests = _.get(event, 'script.exec');

            // @todo: Possible dead code, remove when confirmed
            if (_.isArray(tests)) {
                tests = tests.join('\n');
            }

            return tests;
        }).join('\n');
    },

    /**
     * Converts a V2 cookie to a V1 cookie.
     *
     * @param {Object} cookieV2 - The v2 cookie object to be converted.
     * @returns {{expirationDate: *, hostOnly: *, httpOnly: *,
         *          domain: (any), path: (any), secure: *, session: *, value: *, name: *}}
     */
    cookie: function (cookieV2) {
        return {
            expirationDate: cookieV2.expires,
            hostOnly: cookieV2.hostOnly,
            httpOnly: cookieV2.httpOnly,
            domain: cookieV2.domain,
            path: cookieV2.path,
            secure: cookieV2.secure,
            session: cookieV2.session,
            value: cookieV2.value,
            name: cookieV2.key || cookieV2.name
        };
    },

    /**
     * Converts a V2 response object to a V1 response
     *
     * @param {Object} responseV2 - The v2 response to be converted.
     * @returns {Object} - The converted v1 response.
     */
    response: function (responseV2) {
        var self = this,
            response = {},
            id = responseV2.id || responseV2._postman_id,
            originalRequest = responseV2.originalRequest || responseV2.request;

        // the true in the next line ensures that we don't recursively go on processing responses in a request.
        response.request = originalRequest ? self.request({ request: originalRequest }, undefined, true) : undefined;

        // add the requestObject to the response (needed by sync)
        try {
            response.request && (response.requestObject = JSON.stringify(response.request));
        }
        catch (e) { /* It's fine, not a fatal error, just move on. */ }

        // do not attempt to regenerate response id here when `retainIds` is set to false
        // if id is changed here the parent's `responses_order` also needs to be changed
        // that can't be done yet
        response.id = id || util.uid();
        response.name = responseV2.name;
        response.status = responseV2.status;
        response.responseCode = {
            code: responseV2.code,
            name: responseV2.status,
            // TODO: get a list of descriptions
            detail: ''
        };
        response.language = responseV2._postman_previewlanguage || 'Text';
        response.previewType = responseV2._postman_previewtype || 'html';
        response.time = responseV2.responseTime;
        response.headers = responseV2.headers || responseV2.header;
        response.cookies = _.map(responseV2.cookies || responseV2.cookie, self.cookie);
        response.text = responseV2.body;
        response.rawDataType = 'text';

        return response;
    },

    /**
     * Extracts the array of responses from a V2 Item.
     *
     * @param {Object} item - The v2 item to extract saved responses from.
     * @returns {Object[]} - The list of saved response objects for the current request.
     */
    responses: function (item) {
        var self = this,
            allResponses = item.responses || item.response;

        if (!allResponses) { return; }

        return _.map(allResponses, function (response) {
            return self.response(response, item);
        });
    },

    /**
     * Creates an ordering field for responses of a V2 Item.
     *
     * @param {Object} item - The v2 item to extract saved responses from.
     * @returns {Object[]} - The order of responses within the V2 Item.
     */
    responses_order: function (item) {
        var allResponses = item.responses || item.response;

        if (!allResponses) {
            return [];
        }

        return _.map(allResponses, 'id');
    },

    /**
     * Converts a V2 request to a V1 request.
     *
     * @param {Object} item - The v2 item to be converted.
     * @param {Object} collectionId - The collection id related to the current conversion routine.
     * @param {Boolean} [skipResponses=false] - When set to true, excludes saved responses from the result.
     * @returns {{id: *, name: *, description: (*|string|builders.description), url: *, collectionId: *, method: *,
         *          currentHelper: *, helperAttributes: *}|*}
     */
    request: function (item, collectionId, skipResponses) {
        if (!item) { return; }

        var units = ['headers', 'dataMode', 'data', 'rawModeData', 'graphqlModeData',
                'pathVariables', 'tests', 'preRequestScript', 'url', 'dataOptions'],
            self = this,
            request,
            description,
            currentHelper,
            helperAttributes,
            req = item && item.request,
            v2Auth = req && req.auth,
            auth = self.auth(req),
            events = self.events(item),
            variables = self.variables(item, { retainIds: self.options.retainIds }),
            url = req && req.url,
            retainEmpty = self.options.retainEmptyValues,
            urlObj = _.isString(url) ? util.urlparse(url) : url;

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

        if (v2Auth && v2Auth.type) {
            // @todo: Add support for custom auth helpers
            currentHelper = v2Common.authMap[v2Auth.type];
            if (util.authMappersFromCurrent[currentHelper]) {
                _.isArray(v2Auth[v2Auth.type]) && (v2Auth = util.authArrayToMap(req));
                helperAttributes = util.authMappersFromCurrent[currentHelper](v2Auth[v2Auth.type]);
            }
            else {
                helperAttributes = null;
            }
        }
        else if (v2Auth === null) {
            currentHelper = null;
            helperAttributes = null;
        }

        request = {

            // do not attempt to regenerate request id here when `retainIds` is set to false
            // if id is changed here the parent's `order` also needs to be changed
            // that can't be done yet
            id: item.id || item._postman_id || util.uid(),
            name: item.name,
            collectionId: collectionId,
            method: item.request ? item.request.method : undefined,
            currentHelper: currentHelper,
            helperAttributes: helperAttributes
        };

        // add protocolProfileBehavior property from item to the request
        util.addProtocolProfileBehavior(item, request);

        // only include the dataDisabled flag if truthy
        if (req && req.body && _.has(req.body, 'disabled') && (req.body.disabled || retainEmpty)) {
            request.dataDisabled = Boolean(req.body.disabled);
        }

        description = item.request && self.description(item.request.description);

        // Prevent empty request descriptions from showing up in the converted result, keeps collections clean.
        if (description) { request.description = description; }
        else if (retainEmpty) { request.description = null; }

        (auth || (auth === null)) && (request.auth = auth);
        events && events.length && (request.events = events);
        variables && variables.length && (request.variables = variables);

        _.forEach(units, function (unit) {
            request[unit] = self[unit](item);
        });

        // description transformations for v2 to v1
        urlObj && (request.pathVariableData = _.map(urlObj.variables || urlObj.variable, function (v) {
            var result = { key: v.key || v.id, value: v.value };

            // Prevent empty path variable descriptions from showing up in converted results, keeps collections clean.
            if (v.description) { result.description = self.description(v.description); }
            else if (retainEmpty) { result.description = null; }

            return result;
        }));

        urlObj && (request.queryParams = _.map(urlObj.query, function (queryParam) {
            // Prevents empty query param descriptions from showing up in the result, keeps collections clean.
            queryParam.description = self.description(queryParam.description);

            _.has(queryParam, 'disabled') && (queryParam.enabled = !queryParam.disabled);
            delete queryParam.disabled;

            return queryParam;
        }));

        // item truthiness is already validated by this point
        request.headerData = _.map(item.request && (item.request.headers || item.request.header), function (header) {
            // Prevents empty query param descriptions from showing up in the result, keeps collections clean.
            header.description = self.description(header.description);

            _.has(header, 'disabled') && (header.enabled = !header.disabled);
            delete header.disabled;

            return header;
        });

        return request;
    },

    /**
     * Creates a V1 compatible array of requests from a Postman V2 collection.
     *
     * @param {Object} collectionV2 - The v2 collection to derive v1 requests from.
     * @returns {Object[]} - The list of v1 request objects.
     */
    requests: function (collectionV2) {
        var self = this,
            requests = [],
            info = collectionV2 && collectionV2.info,
            id = info && (info.id || info._postman_id) || collectionV2.id;

        _.forEach(self.extractItems(collectionV2), function (item) {
            var requestV1 = self.request(item, id);

            requests.push(requestV1);
        });

        return requests;
    },

    /**
     * Creates a V1 compatible array of solo requestIds from a Postman collection V2
     *
     * @param {Object} collectionV2 - The v2 collection to be used for request order derivation.
     * @returns {Object[]} - The request order for the resultant v1 collection.
     */
    order: function (collectionV2) {
        var itemArray = collectionV2.items || collectionV2.item,
            allItems = _.isArray(itemArray) ? itemArray : [itemArray];

        // eslint-disable-next-line lodash/prefer-compact
        return _.filter(_.map(allItems, function (item) {
            if (!item) { return; }

            var isFolder = (item.items || item.item);

            if (!isFolder) {
                return item.id || item._postman_id;
            }
        }));
    },

    /**
     * Creates a V1 compatible array of folder orders from a Postman collection V2
     *
     * @param {Object} folderOrCollection - The object to derive folder order details from.
     * @returns {Object[]} - The list of folder ids that indicate the order.
     */
    folders_order: function (folderOrCollection) {
        var itemArray = folderOrCollection.items || folderOrCollection.item,
            allItems = _.isArray(itemArray) ? itemArray : [itemArray];

        // eslint-disable-next-line lodash/prefer-compact
        return _.filter(_.map(allItems, function (item) {
            if (!item) { return; }

            var isFolder = (item.items || item.item);

            if (isFolder) {
                return item.id || item._postman_id;
            }
        }));
    },

    /**
     * Creates an array of V1 compatible folders from a V2 collection
     *
     * @param {Object} collectionV2 - The v2 collection to derive folder structure information from.
     * @returns {Object[]} - The list of folder definitions.
     */
    folders: function (collectionV2) {
        var self = this,
            retainEmpty = self.options.retainEmptyValues;

        return _.map(self.extractFolderItems(collectionV2), function (folder) {
            if (!folder) { return; }

            var folderItems = folder.items || folder.item,
                description = self.description(folder.description),
                auth = self.auth(folder),
                events = self.events(folder),
                variables = self.variables(folder, { retainIds: self.options.retainIds }),

                result = {

                    // do not attempt to regenerate folder id here when `retainIds` is set to false
                    // if id is changed here the parent's `folder_order` also needs to be changed
                    // that can't be done yet
                    id: folder.id || folder._postman_id || util.uid(),
                    name: folder.name,
                    // eslint-disable-next-line lodash/prefer-compact
                    order: _.filter(_.map(folderItems, function (f) {
                        if (!f) { return; }

                        var isFolder = (f.items || f.item);

                        return !isFolder && (f.id || f._postman_id);
                    })),
                    folders_order: self.folders_order(folder)
                };

            ((auth && auth.type) || (auth === null)) && (result.auth = auth);
            events && events.length && (result.events = events);
            variables && variables.length && (result.variables = variables);
            util.addProtocolProfileBehavior(folder, result);

            // Prevent empty folder descriptions from showing up in the result, keeps collections clean.
            if (description) { result.description = description; }
            else if (retainEmpty) { result.description = null; }

            return result;
        });
    },

    /**
     * Creates the v1.0.0 compatible description string from the v2.0.0 description format.
     *
     * @param {Object} descriptionV2 - The v2 style description to be converted
     *
     * @returns {String} - The resultant v1 description.
     */
    description: function (descriptionV2) {
        var description,
            retainEmpty = this.options.retainEmptyValues;

        description = _.isObject(descriptionV2) ? descriptionV2.content : descriptionV2;

        if (description) { return description; }
        else if (retainEmpty) { return null; }
    }
});

module.exports = {
    input: '2.0.0',
    output: '1.0.0',
    Builders: Builders,

    /**
     * Converts a single V2 item to a V1 request.
     *
     * @param {Object} request - The request to be converted.
     * @param {Object} options - The set of options for request conversion.
     * @param {Function} callback - The function to be invoked after conversion has completed.
     */
    convertSingle: function (request, options, callback) {
        var builders = new Builders(options),
            clone = _.cloneDeep(request),
            converted,
            err;

        try {
            clone = v2Common.populateIds(clone);
            converted = builders.request(clone);
        }
        catch (e) {
            err = e;
        }

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

        if (err) {
            throw err;
        }

        return converted;
    },

    /**
     * Converts a single V2 item to a V1 request.
     *
     * @param {Object} response - The response to be converted.
     * @param {Object} options - The set of options for request conversion.
     * @param {Function} callback - The function to be invoked after conversion has completed.
     */
    convertResponse: function (response, options, callback) {
        var builders = new Builders(options),
            converted,
            err;

        try {
            converted = builders.response(_.cloneDeep(response));
        }
        catch (e) {
            err = e;
        }

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

        if (err) {
            throw err;
        }

        return converted;
    },

    /**
     * Converts a V2 collection to a V1 collection (performs ID replacement, etc as necessary).
     *
     * @param {Object} collection - The collection to be converted.
     * @param {Object} options - The set of options for request conversion.
     * @param {Function} callback - The function to be invoked after conversion has completed.
     */
    convert: function (collection, options, callback) {
        collection = _.cloneDeep(collection);

        var auth,
            events,
            variables,
            builders = new Builders(options),
            authOptions = { excludeNoauth: true },
            varOpts = options && { fallback: options.env, retainIds: options.retainIds },
            units = ['order', 'folders_order', 'folders', 'requests'],
            id = _.get(collection, 'info._postman_id') || _.get(collection, 'info.id'),
            info = collection && collection.info,
            newCollection = {
                id: id && options && options.retainIds ? id : util.uid(),
                name: info && info.name
            };

        // ensure that each item has an ID
        collection = v2Common.populateIds(collection);
        try {
            // eslint-disable-next-line max-len
            newCollection.description = builders.description(info && info.description);
            (auth = builders.auth(collection, authOptions)) && (newCollection.auth = auth);
            (events = builders.events(collection)) && (newCollection.events = events);
            (variables = builders.variables(collection, varOpts)) && (newCollection.variables = variables);
            util.addProtocolProfileBehavior(collection, newCollection);

            units.forEach(function (unit) {
                newCollection[unit] = builders[unit](collection);
            });
        }
        catch (e) {
            if (callback) {
                return callback(e, null);
            }
            throw e;
        }

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

        return newCollection;
    }
};
