import _ from 'lodash';
import cookie from 'js-cookie';
import { Promise } from 'rsvp';
import { StatusCodes } from 'constants/Http';
import { sendRequestErrorNotification } from 'services/notify';
import log from 'services/log';
import ErrorActions from 'actions/App/Error';

import AuthService from 'services/auth';
import AuthStore from 'stores/Auth';
import ConfigStore from 'stores/Config';

// TODO: Перенести токен из дома в конфиг
// CSRF токен полученный из метатега страницы
const token = document.querySelector('meta[name="csrf_token"]').content;

// Регулярное выражение для проверки разрешенного типа запроса
const SAFE_REQUEST_PATTERN = /^(GET|HEAD|OPTIONS|TRACE)$/;

function isFirstLoad() {
    return Boolean(cookie.get('connect_first_load'));
}

function getDebugId(url) {
    return (url.match(/(\?|&)debug=([^&]+)/) || [])[2];
}

function getFetchOptions(method, url, options) {
    const params = {
        method,
        mode: 'cors',
        cache: 'no-cache',
        credentials: 'include',
        headers: {
            Accept: 'application/json',
            'Cache-Control': 'no-cache',
        },
    };
    const { requestId } = window.ya.connect.initial;

    if (options.data instanceof FormData) {
        params.body = options.data;
    } else if (params.method !== 'GET') {
        params.headers['Content-Type'] = 'application/json';
        params.body = JSON.stringify(options.data);
    }

    url = applyQuery(url, options.query, requestId);

    const orgId = AuthStore.getOrganizationId();

    // при параметре noOrgId не проставляем заголовок с OrgId
    // нужно например для дорегистрации новой организации
    if (orgId && !options.noOrgId) {
        params.headers['x-org-id'] = orgId;
    }

    if (!SAFE_REQUEST_PATTERN.test(params.method)) {
        params.headers['x-csrf-token'] = token;
    }

    params.headers['x-request-id'] = requestId;

    const debugId = getDebugId(location.search) || getDebugId(url);

    if (debugId) {
        params.headers['x-debug'] = debugId;
    }

    if (typeof options.timeout === 'number') {
        params.headers['x-timeout'] = options.timeout;
    }

    if (isFirstLoad()) {
        params.headers['x-first-visit'] = 'yes';
    }

    return { url, params, requestId };
}

function succeed(requestId, response, callback) {
    if (callback) {
        callback(response);
    }

    log.info('[api:%s] response: %O', requestId, response);
}

function fail(requestId, response, callback, isServiceApi) {
    ErrorActions.pushErrorId(requestId);

    if (response.status === StatusCodes.UNAUTHORIZED) {
        log.info('[api:%s] error NO_AUTH: %O', requestId, response);
        AuthService.requireAuthorization();
    }

    if (response.status !== StatusCodes.NOT_FOUND && !isServiceApi) {
        handleApiError(response);
    }

    log.info('[api:%s] error: %O', requestId, response);

    if (callback) {
        callback(response);
    }
}

function handleApiError(response) {
    if (['registration', 'widget', 'onboarding', 'invites', 'services'].indexOf(AuthStore.getViewMode()) !== -1) {
        return;
    }

    if (['admin', 'staff', 'forms', 'home'].indexOf(AuthStore.getViewMode()) !== -1) {
        sendRequestErrorNotification(response);

        return;
    }

    ErrorActions.showError();
}

function getUrl(prefix, path) {
    const appRoot = ConfigStore.get('app.root');

    if (prefix.indexOf(appRoot) !== 0) {
        prefix = appRoot + prefix.substring(1);
    }

    return `${prefix}/${path}`;
}

/**
 * Запрос к API
 * @param {String}   method   название метода
 * @param {String}   path     параметры запроса
 * @param {?Object}  options  опциональные настройки запроса
 * @returns {Promise}
 */
export function requestApi(method, path, options = {}) {
    const url = getUrl(ConfigStore.get('api.app.default'), path);
    const fetchOptions = getFetchOptions(method, url, options);

    return new Promise((resolve, reject) => {
        const t0 = Date.now();
        let requestId = fetchOptions.requestId;

        log.info('[api:%s] method: %s, params: %O', requestId, path, options);

        fetch(fetchOptions.url, fetchOptions.params)
            .then(response => {
                // Логирование id-шников долгих запросов для дебага директории
                // @TODO выпилить до релиза наружу
                const dt = Date.now() - t0;

                requestId = response.headers.get('x-request-id');

                if (dt > 1000) {
                    log.info(
                        `%c${requestId}`,
                        'background: #000; color: #f00; font-size: 24px; border: 1px solid red; padding: 4px 10px;'
                    );
                    log.info(`Время запроса #${requestId}: ${dt} мс`);
                }

                return response;
            })
            .then(checkStatus)
            .then(checkUserChange)
            .then(parseJSON)
            .then(response => setTag(response, options))
            .then(response => succeed(requestId, response, resolve))
            .catch(response => fail(requestId, response, reject));
    });
}

/**
 * Запрос к сервисному API
 * @param {String}   method   название метода
 * @param {String}   path     параметры запроса
 * @param {?Object}  options  опциональные настройки запроса
 * @returns {Promise}
 */
export function requestServiceApi(method, path, options = {}) {
    const url = getUrl(ConfigStore.get('api.app.service'), path);

    const fetchOptions = getFetchOptions(method, url, _.extend({}, options, { noOrgId: true }));

    return new Promise((resolve, reject) => {
        const requestId = fetchOptions.requestId;

        log.info('[api:%s] method: %s, params: %O', requestId, path, options);

        fetch(fetchOptions.url, fetchOptions.params)
            .then(checkStatus)
            .then(checkUserChange)
            .then(parseJSON)
            .then(response => succeed(requestId, response, resolve))
            .catch(response => fail(requestId, response, reject, true));
    });
}

/**
 * Проверка изменился ли текущий авторизованный пользователь
 * @param {Response} response объект ответа fetch
 * @returns {Response}
 * @private
 */
function checkUserChange(response) {
    const uid = response.headers.get('x-uid');

    if (uid && !AuthService.isCurrentSession(uid)) {
        AuthService.startSession();

        return;
    }

    return response;
}

const HANDLEABLE_ERRORS = [
    StatusCodes.PAYMENT_REQUIRED,
    StatusCodes.CONFLICT,
    StatusCodes.UNPROCESSABLE_ENTITY,
];

/**
 * Проверка статуса ответа
 * @param {Response} response объект ответа fetch
 * @returns {Object}
 * @private
 */
function checkStatus(response) {
    const normalResponse = response.ok &&
        response.status >= StatusCodes.OK &&
        response.status < StatusCodes.MULTIPLE_CHOICES;

    const handleableError = HANDLEABLE_ERRORS.indexOf(response.status) !== -1;

    if (normalResponse || handleableError) {
        return response;
    }

    const error = new Error(`Failed to execute 'fetch': ${response.statusText}`);

    error.status = response.status;

    return Promise.resolve(response)
        .then(res => res.json())
        .then(data => {
            if (data) {
                error.code = data.code;
                error.response = data;
            }
            throw error;
        })
        .catch(() => {
            throw error;
        });
}

/**
 * Преобразование ответа к JSON виду
 * @param {Response} response объект ответа fetch
 * @returns {Object}
 * @private
 */
function parseJSON(response) {
    return response ? response.json() : {};
}

function setTag(response, options) {
    if (response && options && options.tag) {
        response.tag = options.tag;
    }

    return response;
}

/**
 * Генерация query-пареметров для запроса
 * @param {String} url
 * @param {Object} queryOptions
 * @param {String} requestId
 * @returns {String}
 */
function applyQuery(url, queryOptions, requestId) {
    const query = [`x-request-id=${requestId}`];

    if (url && queryOptions) {
        _.forEach(queryOptions, (value, key) => {
            if (value !== undefined && value !== null) {
                query.push(`${encodeURIComponent(key)}=${encodeURIComponent(value)}`);
            }
        });
    }

    url += (url.indexOf('?') === -1 ? '?' : '&') + query.join('&');

    return url;
}

_.extend(window.ya.connect, { requestApi, requestServiceApi });
