import {AxiosError, AxiosInstance, AxiosRequestConfig, AxiosResponse, Cancel, default as axios} from 'axios';
import {History} from 'history';
import AuthUtils from './auth-utils';
import * as _ from 'lodash';
import {getGlobal, setGlobal} from './window-utils';
import {makeId} from './string-utils';
import {parseHeadersToApiResponseHeaders} from '..';

export function createCancelToken(id: string, config = null) {
    let source = makeCancelToken();
    storeCancelToken(id, source, config);
    return source;
}

export function makeCancelToken() {
    let source: any = axios.CancelToken.source();
    source.id = makeId();
    return source;
}

export function storeCancelToken(id: string, source, config = null) {
    let storedCancelTokens = getGlobal('storedCancelTokens', []);
    storedCancelTokens.push({id, source, config});
    setGlobal('storedCancelTokens', storedCancelTokens);
}

export function cancelRequest(tokenOrId: string, message: string = null) {
    let storedCancelTokens = getGlobal('storedCancelTokens', []);
    let lastIndex = -1;
    let sourceIndex = storedCancelTokens.findIndex(
        s => s.source.id == tokenOrId || s.id == tokenOrId
    );
    while (sourceIndex >= 0) {
        lastIndex = sourceIndex;
        storedCancelTokens[sourceIndex].source.cancel(
            JSON.stringify({id: storedCancelTokens[sourceIndex].id, message}));
        sourceIndex = storedCancelTokens.findIndex(
            (s, index) => index > lastIndex && (s.source.id == tokenOrId || s.id == tokenOrId)
        );
    }
}

export interface AxiosRequestConfigExtended extends AxiosRequestConfig {
    onResponse?: { [code: number]: () => void };
    onExpired?: () => void;
    skipCancel?: boolean;
    cancelTokenId?: string;
    cancelMessage?: string;
    globalHandlers?: {
        onRequest?: (config: AxiosRequestConfigExtended, originalConfig: AxiosRequestConfigExtended) => void;
        onRequestError?: (error: AxiosError, originalConfig: AxiosRequestConfigExtended) => void;
        onResponse?: (response: AxiosResponse, originalConfig: AxiosRequestConfigExtended) => void;
        onResponseError?: (error: AxiosError | Cancel, originalConfig: AxiosRequestConfigExtended) => void;
    }
}

export function bypassGlobalResponseHandlers(bypassGlobalResponseHandlersOption: (status: number,
                                                                                  response: any,
                                                                                  expired: boolean) => boolean | boolean | Array<number>,
                                             status: number, response: any, expired: boolean) {
    if (_.isBoolean(bypassGlobalResponseHandlersOption)) {
        return bypassGlobalResponseHandlersOption;
    }
    else if (_.isArray(bypassGlobalResponseHandlersOption)) {
        return bypassGlobalResponseHandlersOption.findIndex(h => h == status) >= 0;
    }
    else {
        return bypassGlobalResponseHandlersOption(status, response, expired);
    }
}

export function setupAxiosInstance(instance: AxiosInstance, config: AxiosRequestConfigExtended = {}) {
    instance.interceptors.request.use(
        (_config: AxiosRequestConfigExtended) => {
            _config = {..._config, ...(config || {})};

            let source = makeCancelToken();
            let tokenId: string = _config.cancelToken ? _config.cancelToken as any : `${_config.method} ${_config.url}`.toLowerCase();
            _config.cancelToken = source.token;
            _config.cancelTokenId = source.id;

            if (!(_config as any).skipCancel) {
                cancelRequest(tokenId, _config.cancelMessage || 'Skipped because of ' + source.id);
            }

            if (AuthUtils.isLoggedIn()) {
                _config.headers = {..._config.headers, ...{token: AuthUtils.getToken()}};
            }

            for (let key in _config.headers) {
                let header = _config.headers[key];
                if (_.isFunction(header)) {
                    _config.headers[key] = header();
                }
            }

            storeCancelToken(tokenId, source, _config);

            if (config.globalHandlers && config.globalHandlers.onRequest) {
                config.globalHandlers.onRequest(_config, config);
            }

            return Promise.resolve(_config);
        },
        (error: AxiosError) => {
            console.error(error);

            if (config.globalHandlers && config.globalHandlers.onRequest) {
                config.globalHandlers.onRequestError(error, config);
            }

            return Promise.reject(error);
        }
    );
    instance.interceptors.response.use(
        (response: AxiosResponse) => {
            if (response.config.cancelToken) {
                let storedCancelTokens = getGlobal('storedCancelTokens', []);
                let index = storedCancelTokens.findIndex(s => s.source.id == (response.config as any).cancelTokenId);
                if (index >= 0) {
                    storedCancelTokens.splice(index, 1);
                    setGlobal('storedCancelTokens', storedCancelTokens);
                }
            }

            let config: any = (response.config as AxiosRequestConfigExtended);
            let headers = parseHeadersToApiResponseHeaders(response.headers);
            let bypass = 'bypassGlobalResponseHandlers' in config &&
                bypassGlobalResponseHandlers(config.bypassGlobalResponseHandlers, response.status, response.data,
                    !!headers.expired);

            if (!bypass) {
                if (config.onResponse && response.status in config.onResponse) {
                    config.onResponse[response.status]();
                }
                if (headers.expired && config.onExpired) {
                    config.onExpired();
                }
            }

            if (config.globalHandlers && config.globalHandlers.onResponse) {
                config.globalHandlers.onResponse(response, config);
            }

            return response;
        },
        (error: AxiosError | Cancel) => {
            if (axios.isCancel(error)) {
                try {
                    let cancelPayload = JSON.parse((error as Cancel).message);
                    let storedCancelTokens = getGlobal('storedCancelTokens', []);
                    let sourceIndex = storedCancelTokens.findIndex(s => s.id == cancelPayload.id);
                    if (sourceIndex >= 0) {
                        console.warn(cancelPayload.message, storedCancelTokens[sourceIndex]);
                        if (config.globalHandlers && config.globalHandlers.onResponseError) {
                            config.globalHandlers.onResponseError({
                                message: cancelPayload.message,
                                config: storedCancelTokens[sourceIndex]
                            }, config);
                        }
                        storedCancelTokens.splice(sourceIndex, 1);
                    }
                    else {
                        console.warn(error);
                        if (config.globalHandlers && config.globalHandlers.onResponseError) {
                            config.globalHandlers.onResponseError(error as Cancel, config);
                        }
                    }
                }
                catch {
                    console.warn(error);
                    if (config.globalHandlers && config.globalHandlers.onResponseError) {
                        config.globalHandlers.onResponseError(error as Cancel, config);
                    }
                }
            }
            else {
                if ((error as AxiosError).config.cancelToken) {
                    let storedCancelTokens = getGlobal('storedCancelTokens', []);
                    let index = storedCancelTokens.findIndex(s => s.source.id == (error as any).config.cancelTokenId);
                    if (index >= 0) {
                        storedCancelTokens.splice(index, 1);
                        setGlobal('storedCancelTokens', storedCancelTokens);
                    }
                }
                console.log('ERROR RESPONSE', error);

                let config: any = ((error as AxiosError).config as AxiosRequestConfigExtended);
                let response = (error as AxiosError).response;
                let headers = parseHeadersToApiResponseHeaders(response.headers);
                let bypass = 'bypassGlobalResponseHandlers' in config &&
                    bypassGlobalResponseHandlers(config.bypassGlobalResponseHandlers, response.status, response.data,
                        !!headers.expired);

                if (!bypass) {
                    if (config.onResponse && response.status in config.onResponse) {
                        config.onResponse[response.status]();
                    }

                    if (headers.expired && config.onExpired) {
                        config.onExpired();
                    }
                }

                if (config.globalHandlers && config.globalHandlers.onResponseError) {
                    config.globalHandlers.onResponseError(error as AxiosError, config);
                }
            }

            return Promise.reject(error);
        }
    );
}

export function redirectOnFailConnect(history: History, redirects = {
    401: (history: History) => history.push('/login'),
    403: (history: History) => history.push('/login'),
    404: (history: History) => history.push('/notfound'),
}): { catch: (e: any) => any } {
    return {
        catch: redirectOnFail(history, redirects),
    };
}

export function redirectOnFail(history: History, redirects = {
    401: (history: History) => history.push('/login'),
    403: (history: History) => history.push('/login'),
    404: (history: History) => history.push('/notfound'),
}): (e: any) => any {
    return e => {
        if (e.response && e.response.status && redirects[e.response.status]) {
            if (e.response.status == 401) {
                AuthUtils.logout();
                setTimeout(() => window.document.location.reload(true), 500);
            }
            redirects[e.response.status](history);
        }
    };
}

export function backOnSuccessConnect(history: History, to = null): { then: (r: any) => any } {
    return {
        then: backOnSuccess(history, to),
    };
}

export function backOnSuccess(history: History, to = null): (r: any) => any {
    return response => {
        if (to) {
            history.push(to);
        }
        else {
            history.goBack();
        }
    };
}