"use strict";
/**
 * @module utils
 */
var crypto = require('crypto');
var logger = require('./logger');
var TraceID = require('./segments/attributes/trace_id');
var utils = {
    /**
     * Checks a HTTP response code, where 4xx are 'error' and 5xx are 'fault'.
     * @param {string} status - the HTTP response status code.
     * @returns [string] - 'error', 'fault' or nothing on no match
     * @alias module:utils.getCauseTypeFromHttpStatus
     */
    getCauseTypeFromHttpStatus: function getCauseTypeFromHttpStatus(status) {
        var stat = status.toString();
        if (stat.match(/^[4][0-9]{2}$/) !== null) {
            return 'error';
        }
        else if (stat.match(/^[5][0-9]{2}$/) !== null) {
            return 'fault';
        }
    },
    /**
     * Removes the query string parameters from a given http request path
     * as it may contain sensitive information
     *
     * Related issue: https://github.com/aws/aws-xray-sdk-node/issues/246
     *
     * Node documentation: https://nodejs.org/api/http.html#http_http_request_url_options_callback
     *
     * @param {string} path - options.path in a http.request callback
     * @returns [string] - removes query string element from path
     * @alias module:utils.stripQueryStringFromPath
     */
    stripQueryStringFromPath: function stripQueryStringFromPath(path) {
        return path ? path.split('?')[0] : '';
    },
    /**
     * Performs a case-insensitive wildcard match against two strings. This method works with pseduo-regex chars; specifically ? and * are supported.
     *   An asterisk (*) represents any combination of characters
     *   A question mark (?) represents any single character
     *
     * @param {string} pattern - the regex-like pattern to be compared against.
     * @param {string} text - the string to compare against the pattern.
     * @returns boolean
     * @alias module:utils.wildcardMatch
     */
    wildcardMatch: function wildcardMatch(pattern, text) {
        if (pattern === undefined || text === undefined) {
            return false;
        }
        if (pattern.length === 1 && pattern.charAt(0) === '*') {
            return true;
        }
        var patternLength = pattern.length;
        var textLength = text.length;
        var indexOfGlob = pattern.indexOf('*');
        pattern = pattern.toLowerCase();
        text = text.toLowerCase();
        // Infix globs are relatively rare, and the below search is expensive especially when
        // Balsa is used a lot. Check for infix globs and, in their absence, do the simple thing
        if (indexOfGlob === -1 || indexOfGlob === (patternLength - 1)) {
            var match = function simpleWildcardMatch() {
                var j = 0;
                for (var i = 0; i < patternLength; i++) {
                    var patternChar = pattern.charAt(i);
                    if (patternChar === '*') {
                        // Presumption for this method is that globs only occur at end
                        return true;
                    }
                    else if (patternChar === '?') {
                        if (j === textLength) {
                            return false;
                        } // No character to match
                        j++;
                    }
                    else {
                        if (j >= textLength || patternChar != text.charAt(j)) {
                            return false;
                        }
                        j++;
                    }
                }
                // Ate up all the pattern and didn't end at a glob, so a match will have consumed all
                // the text
                return j === textLength;
            };
            return match();
        }
        /*
         * The matchArray[i] is used to record if there is a match between the first i chars in =
         * text and the first j chars in pattern.
         * So will return matchArray[textLength+1] in the end
         * Loop from the beginning of the pattern
         * case not '*': if text[i]==pattern[j] or pattern[j] is '?', and matchArray[i] is true,
         *   set matchArray[i+1] to true, otherwise false
         * case '*': since '*' can match any globing, as long as there is a true in matchArray before i
         *   all the matchArray[i+1], matchArray[i+2],...,matchArray[textLength] could be true
        */
        var matchArray = [];
        matchArray[0] = true;
        for (var j = 0; j < patternLength; j++) {
            var i;
            var patternChar = pattern.charAt(j);
            if (patternChar != '*') {
                for (i = textLength - 1; i >= 0; i--) {
                    matchArray[i + 1] = !!matchArray[i] && (patternChar === '?' || (patternChar === text.charAt(i)));
                }
            }
            else {
                i = 0;
                while (i <= textLength && !matchArray[i]) {
                    i++;
                }
                for (i; i <= textLength; i++) {
                    matchArray[i] = true;
                }
            }
            matchArray[0] = (matchArray[0] && patternChar === '*');
        }
        return matchArray[textLength];
    },
    LambdaUtils: {
        validTraceData: function (xAmznTraceId) {
            var valid = false;
            if (xAmznTraceId) {
                var data = utils.processTraceData(xAmznTraceId);
                valid = !!(data && data.root && data.parent && data.sampled);
            }
            return valid;
        },
        /**
         * Populates trace ID, parent ID, and sampled decision of given segment. Will always populate valid values,
         * even if xAmznTraceId contains missing or invalid values. This ensures downstream services receive valid
         * headers.
         * @param {Segment} segment - Facade segment to be populated
         * @param {String} xAmznTraceId - Raw Trace Header to supply trace data
         * @returns {Boolean} - true if required fields are present and Trace ID is valid, false otherwise
         */
        populateTraceData: function (segment, xAmznTraceId) {
            logger.getLogger().debug('Lambda trace data found: ' + xAmznTraceId);
            let traceData = utils.processTraceData(xAmznTraceId);
            var valid = false;
            if (!traceData) {
                traceData = {};
                logger.getLogger().error('_X_AMZN_TRACE_ID is empty or has an invalid format');
            }
            else if (traceData.root && !traceData.parent && !traceData.sampled) {
                // Lambda PassThrough only has root, treat as valid in this case and mark the segment
                segment.noOp = true;
                valid = true;
            }
            else if (!traceData.root || !traceData.parent || !traceData.sampled) {
                logger.getLogger().error('_X_AMZN_TRACE_ID is missing required information');
            }
            else {
                valid = true;
            }
            segment.trace_id = TraceID.FromString(traceData.root).toString(); // Will always assign valid trace_id
            segment.id = traceData.parent || crypto.randomBytes(8).toString('hex');
            if (traceData.root && segment.trace_id !== traceData.root) {
                logger.getLogger().error('_X_AMZN_TRACE_ID contains invalid trace ID');
                valid = false;
            }
            if (!parseInt(traceData.sampled)) {
                segment.notTraced = true;
            }
            else {
                delete segment.notTraced;
            }
            if (traceData.data) {
                segment.additionalTraceData = traceData.data;
            }
            logger.getLogger().debug('Segment started: ' + JSON.stringify(traceData));
            return valid;
        }
    },
    /**
     * Splits out the data from the trace id format.  Used by the middleware.
     * @param {String} traceData - The additional trace data (typically in req.headers.x-amzn-trace-id).
     * @returns {object}
     * @alias module:mw_utils.processTraceData
     */
    processTraceData: function processTraceData(traceData) {
        var amznTraceData = {};
        var data = {};
        var reservedKeywords = ['root', 'parent', 'sampled', 'self'];
        var remainingBytes = 256;
        if (!(typeof traceData === 'string' && traceData)) {
            return amznTraceData;
        }
        traceData.split(';').forEach(function (header) {
            if (!header) {
                return;
            }
            var pair = header.split('=');
            if (pair[0] && pair[1]) {
                let key = pair[0].trim();
                let value = pair[1].trim();
                let lowerCaseKey = key.toLowerCase();
                let reserved = reservedKeywords.indexOf(lowerCaseKey) !== -1;
                if (reserved) {
                    amznTraceData[lowerCaseKey] = value;
                }
                else if (!reserved && remainingBytes - (lowerCaseKey.length + value.length) >= 0) {
                    data[key] = value;
                    remainingBytes -= (key.length + value.length);
                }
            }
        });
        amznTraceData['data'] = data;
        return amznTraceData;
    },
    /**
     * Makes a shallow copy of an object without given keys - keeps prototype
     * @param {Object} obj - The object to copy
     * @param {string[]} [keys=[]] - The keys that won't be copied
     * @param {boolean} [preservePrototype=false] - If true also copy prototype properties
     * @returns {}
     */
    objectWithoutProperties: function objectWithoutProperties(obj, keys, preservePrototype) {
        keys = Array.isArray(keys) ? keys : [];
        preservePrototype = typeof preservePrototype === 'boolean' ? preservePrototype : false;
        var target = preservePrototype ? Object.create(Object.getPrototypeOf(obj)) : {};
        for (var property in obj) {
            if (keys.indexOf(property) >= 0) {
                continue;
            }
            if (!Object.prototype.hasOwnProperty.call(obj, property)) {
                continue;
            }
            target[property] = obj[property];
        }
        return target;
    },
    /**
     * Safely gets an integer from a string or number
     * @param {String | Number} - input to cast to integer
     * @returns {Number} - Integer representation of input, or 0 if input is not castable to int
     */
    safeParseInt: (val) => {
        if (!val || isNaN(val)) {
            return 0;
        }
        return parseInt(val);
    }
};
module.exports = utils;
