/**
 * @module data/Transport
 */
'use strict';

import {readCookie} from '../utils/cookie';
import EventBus from '../utils/EventBus';
import {getConfig, constants} from '../../utils';

// NOTE: event name is all lower case as per DOM convention
window.addEventListener('unhandledrejection', function unhandledRejectionHandler(e) {
    // NOTE: e.preventDefault() must be manually called to prevent the default
    // action which is currently to log the stack trace to console.warn
    e.preventDefault();
    // NOTE: parameters are properties of the event detail property
    let reason = e.reason || (e.detail ? e.detail.reason : null);
    let promise = e.promise || (e.detail ? e.detail.promise : null);
    // See Promise.onPossiblyUnhandledRejection for parameter documentation

    if (reason) {
        // eslint-disable-next-line
        console.error(reason.stack || reason);
    } else {
        // eslint-disable-next-line
        console.error('unhandled rejection');
    }

    if (promise && promise.catch) {
        promise.catch(error => {
            if (error && error.xmlhttp) {
                EventBus.emit('invalidResponse', error);
            }
        });
    }
});

/**
 * @alias data/Store:Transport
 */
export default class Transport {
    constructor() {
        this.requests = {};
    }

    getRequestId() {
        return null;
    }

    getRequestIdFieldName() {
        return null;
    }

    createError(message, xmlhttp, response) {
        let error = new Error(message);
        error.status = xmlhttp.status;
        error.statusText = xmlhttp.statusText;
        error.responseJSON = response;
        error.xmlhttp = xmlhttp;
        return error;
    }

    /**
     * Low level method that uses XHR to send the request and get response
     * @param {string} requestKey this key will be used to store result of response, it has to be a hash of parameters
     * @param {string} method
     * @param {string} url
     * @param {object} [headers]
     * @param {string} requestBody
     * @param {XMLHttpRequestResponseType} responseType ("" | "arraybuffer" | "blob" | "document" | "json" | "text")
     * @param {boolean} [skipLoader] if true no events will be emitted to EventBus
     * @param {boolean} [responseStatusValidation] JEDI may respond with 200 and an error, setting this to true validates the result
     * @param {boolean} [skipParseResponseJSON] JEDI may respond with valid result which non compatible with JSON format (for example, file upload mechanism)
     * @param {boolean} [compactResponseFormat] whether or not return additional information
     * @return {Promise<object>}
     */
    ajaxRequest({
        requestKey,
        method,
        url,
        headers,
        requestBody,
        responseType,
        skipLoader,
        responseStatusValidation,
        skipParseResponseJSON,
        compactResponseFormat = true
    }) {
        //TODO Handle empty request key
        if (this.requests[requestKey]) {
            return this.requests[requestKey];
        }

        if (!skipLoader) {
            EventBus.emit(constants.eventBeforeAjaxRequest);
        }

        return (this.requests[requestKey] = new Promise((resolve, reject) => {
            let xmlhttp = new XMLHttpRequest();

            // prevent ie caching, can't stop it via headers for IE
            if (method.toUpperCase() === 'GET') {
                url = Transport.addUrlParameter(url, 'nocache', new Date().getTime());
            }

            const requestId = this.getRequestId();
            const requestIdFieldName = this.getRequestIdFieldName();
            if (requestId && requestIdFieldName) {
                url = Transport.addUrlParameter(url, requestIdFieldName, requestId);
            }

            xmlhttp.open(method, url, true);

            if (responseType) {
                // Paying some respect to poor miserable IE
                // responseType can be changed only after XMLHttpRequest is open
                xmlhttp.responseType = responseType;
            }

            xmlhttp.setRequestHeader('CSRFTOKEN', readCookie('CSRFTOKEN'));

            if (getConfig().isDetailedLogsEnabled) {
                xmlhttp.setRequestHeader('detailedRef', top.location.href);
            }

            Object.keys(headers || {}).forEach(name => {
                xmlhttp.setRequestHeader(name, headers[name]);
            });

            xmlhttp.onreadystatechange = () => {
                if (xmlhttp.readyState === XMLHttpRequest.DONE) {
                    delete this.requests[requestKey];

                    if (!skipLoader) {
                        EventBus.emit(constants.eventAfterAjaxRequest);
                    }

                    let response;

                    if (xmlhttp.response !== '') {
                        if (!skipParseResponseJSON) {
                            try {
                                response = JSON.parse(xmlhttp.response);
                            } catch (e) {
                                //TODO Should it cause an error?
                                response = null;
                            }
                        } else {
                            response = xmlhttp.response;
                        }
                    }

                    if ([200, 201, 204].indexOf(xmlhttp.status) > -1) {
                        if (responseStatusValidation) {
                            if (response && response.status && response.status.code === 10008) {
                                // Not logged in or session timeout
                                EventBus.emit('sessionExpired');
                                return;
                            }
                            if (
                                response !== null &&
                                response.status !== undefined &&
                                !response.status.success
                            ) {
                                reject(this.createError('JEDI Error', xmlhttp, response));
                                return;
                            }
                        }
                        if (compactResponseFormat) {
                            resolve(response);
                        } else {
                            const rawHeaders = xmlhttp
                                .getAllResponseHeaders()
                                .trim()
                                .split(/[\r\n]+/);

                            const headersMap = rawHeaders.reduce((result, header) => {
                                const [key, ...parts] = header.split(': ');
                                result[key.toLowerCase()] = parts.join(': ');
                                return result;
                            }, {});

                            resolve({
                                data: response,
                                headers: headersMap
                            });
                        }
                        return;
                    }

                    if (xmlhttp.status === 207) {
                        // Multi-parts response
                        let boundaryPrefix = 'boundary=';

                        let boundaryHeader = xmlhttp
                            .getResponseHeader('Content-Type')
                            .split(';')
                            .find(
                                s =>
                                    s
                                        .trim()
                                        .toLowerCase()
                                        .substr(0, boundaryPrefix.length) === boundaryPrefix
                            );

                        let boundary =
                            boundaryHeader && boundaryHeader.substr(boundaryPrefix.length);

                        if (!boundary) {
                            reject(
                                this.createError(
                                    'Invalid response: empty boundary',
                                    xmlhttp,
                                    response
                                )
                            );
                            return;
                        }

                        let parts = xmlhttp.responseText
                            .split('--' + boundary)
                            .map(part => part.trim())
                            .filter(part => part && part !== '--')
                            .map(part => {
                                let [headers, body] = part.split(/\r?\n\r?\n/, 2);

                                if (!/Content-Type:\s*application\/json/i.test(headers)) {
                                    let error = this.createError(
                                        'Invalid Content-Type, "application/json" expected',
                                        xmlhttp,
                                        response
                                    );
                                    error.part = part;
                                    return error;
                                }

                                try {
                                    return JSON.parse(body);
                                } catch (error) {
                                    return error;
                                }
                            });

                        resolve(parts);
                        return;
                    }

                    reject(this.createError('Network Error', xmlhttp, response));
                }
            };

            xmlhttp.send(requestBody);
        }));
    }

    /**
     * JEDI-specific load method
     * @param cmd
     * @param data
     * @param skipLoader
     * @return {*}
     */
    load(cmd, data, skipLoader) {
        if (!cmd) {
            throw new Error('Command is not defined');
        }

        return this.ajaxRequest({
            requestKey: cmd + JSON.stringify(data),
            method: 'POST',
            url: '/mobile/api/proxy.html?cmd=' + cmd,
            headers: {
                'Content-Type': 'application/x-www-form-urlencoded; charset=utf-8'
            },
            requestBody: Transport.toQueryString(data || {}),
            skipLoader: skipLoader,
            responseStatusValidation: true,
            skipParseResponseJSON: false
        });
    }

    /**
     * JEDI-specific load method
     * @param cmd
     * @param data
     * @param skipLoader
     * @return {*}
     */
    loadBlob(cmd, data, skipLoader) {
        if (!cmd) {
            throw new Error('Command is not defined');
        }

        return this.ajaxRequest({
            requestKey: cmd + JSON.stringify(data),
            method: 'POST',
            url: '/mobile/api/proxy.html?cmd=' + cmd,
            headers: {
                'Content-Type': 'application/x-www-form-urlencoded; charset=utf-8'
            },
            responseType: 'blob',
            requestBody: Transport.toQueryString(data || {}),
            skipLoader,
            responseStatusValidation: false,
            compactResponseFormat: false,
            skipParseResponseJSON: true
        });
    }

    /**
     * PAS-specific load method
     * @param requestType
     * @param url
     * @param data
     * @param skipLoader
     * @return {*}
     */
    loadFromPAS(requestType, url, data, skipLoader) {
        let contentType = 'application/x-www-form-urlencoded; charset=utf-8';

        if (['POST', 'PUT', 'DELETE'].indexOf(requestType) > -1) {
            contentType = 'application/json';
        }

        return this.ajaxRequest({
            requestKey: requestType + ':' + url + JSON.stringify(data),
            method: requestType,
            url: '/mobile/restapi/v1.0' + url,
            headers: {
                Accept: 'application/json',
                'Content-Type': contentType
            },
            requestBody: JSON.stringify(data) || null,
            skipLoader: skipLoader,
            responseStatusValidation: false,
            skipParseResponseJSON: false
        });
    }

    loadFromPASBatch(requestType, url, data, skipLoader) {
        if (!(data instanceof Array) || !data.length) {
            throw new Error('Invalid data, not empty array expected');
        }
        if (data.length === 1) {
            return this.loadFromPAS(requestType, url, data[0], skipLoader).then(response => [
                response
            ]);
        }

        let boundary = 'Boundary_' + Date.now() + '_' + String(Math.random()).substr(2);
        let prefix = '--' + boundary + '\nContent-Type: application/json\n\n';
        let suffix = '\n';
        let end = '--' + boundary + '--';

        let requestBody =
            prefix + data.map(data => JSON.stringify(data)).join(suffix + prefix) + suffix + end;

        return this.ajaxRequest({
            requestKey: requestType + ':' + url + requestBody,
            method: requestType,
            url: '/mobile/restapi/v1.0' + url,
            headers: {
                'Content-Type': 'multipart/mixed; boundary=' + boundary
            },
            requestBody,
            skipLoader: skipLoader,
            responseStatusValidation: false,
            skipParseResponseJSON: true
        }).then(parts => {
            // Remove first part with status info
            parts.shift();
            return parts;
        });
    }

    static addUrlParameter(url, parameterName, parameterValue) {
        url += url.indexOf('?') >= 0 ? '&' : '?';
        url += parameterName + '=' + parameterValue;
        return url;
    }

    static toQueryString(object) {
        let queryString = [];

        Object.keys(object).forEach(key => {
            queryString.push(key + '=' + encodeURIComponent(object[key]));
        });

        return queryString.join('&');
    }
}
