import {IApiHandler} from "./IApiHandler";
import {IApiParamsCollection} from "./IApiParamsCollection";
import {IApi} from "./IApi";
import {
    CacheData,
    ExceptionFunction,
    ExpectFunction,
    FailFunction,
    GeneralObject,
    Hanger,
    HangerIndex,
    ObjectMapping,
    ResolveParams,
    ResponseHandlerDefineFunction,
    ResponseHandlerFunction,
    SendHandlerResponse,
    SuccessFunction
} from "./TypeDefine";
import {ISendResponse} from "./ISendResponse";

abstract class ApiHandler<T extends IApi> implements IApiHandler<T['params']> {
    api: T;
    expect: ExpectFunction;
    success: SuccessFunction;
    exception: ExceptionFunction;
    fail: FailFunction;

    private cache: Map<String, CacheData> = new Map<String, CacheData>();
    private argsQueue: Array<(value: any) => void> = new Array<(value: any) => void>();
    private limitSet: number = 0;
    private beforeList: Hanger<T>['before'][] = new Array<Hanger<T>['before']>()
    private afterList: Hanger<T>['after'][] = new Array<Hanger<T>['after']>()
    private guardList: Hanger<T>['guard'][] = new Array<Hanger<T>['guard']>()
    private completeList: Hanger<T>['complete'][] = new Array<Hanger<T>['complete']>()

    public getURL(): string {
        if (!this.api.path) throw new Error("not setting url")
        return this.api.path;
    }

    public composeURL(args: ResolveParams<T['params']>): string {
        if (!this.api.path) throw new Error("not setting url")
        let path = this.api.path;
        if (Array.isArray(this.api.params)) {
            const params = ApiHandler.assemblyParameters(this.api.params, args, this.api);
            if (typeof params === 'string')
                path += params
        } else if (typeof this.api.params !== 'undefined') {
            const params = ApiHandler.assemblyParametersToUnion(this.api.params, args, this.api);
            path += params.path + params.query
        }
        return path;

    }

    protected constructor(api: T, responseHandlerDefineFunction: ResponseHandlerDefineFunction) {
        this.api = api;
        if (Array.isArray(this.api.hanger)) {
            this.api.hanger.forEach((v) => this.analysisHanger(v, api));
        } else if (typeof this.api.hanger !== 'undefined')
            this.analysisHanger(this.api.hanger, api)
        this.expect = responseHandlerDefineFunction.expectFunction;
        this.success = responseHandlerDefineFunction.successFunction;
        this.exception = responseHandlerDefineFunction.exceptionFunction;
        this.fail = responseHandlerDefineFunction.failFunction;
    }

    private analysisHanger(hangerIndex: HangerIndex<T>, api: T) {
        const {before, after, guard, complete} = hangerIndex(api)
        if (typeof before !== "undefined")
            this.beforeList.push(before)
        if (typeof after !== "undefined")
            this.afterList.push(after)
        if (typeof guard !== "undefined")
            this.guardList.push(guard)
        if (typeof complete !== "undefined")
            this.completeList.push(complete)
    }

    abstract send(args: ResolveParams<T['params']>): Promise<ISendResponse<any>> ;

    public clearCache() {
        for (const [key, cacheData] of this.cache.entries()) {
            localStorage.removeItem(`api-handler-${this.api.cacheKey ?? ''}-${key}`)
        }
        this.cache.clear();
        for (const key in localStorage) {
            if (key.startsWith(`api-handler-${this.api.cacheKey ?? ''}-${this.api.path}`))
                localStorage.removeItem(key);
        }

    }

    private saveCacheHandler(args: ResolveParams<T['params']>, data: ISendResponse<any>): void {
        if (typeof this.api.cachePolicy === "undefined" || this.api.cachePolicy === 'noCache') return;
        const key = this.api.path + JSON.stringify(args)
        if (this.cache.has(key)) {
            (<CacheData>this.cache.get(key)).lastTime = new Date();
        } else {
            const cacheData: CacheData = {
                data,
                lastTime: new Date()
            }
            this.cache.set(key, cacheData);
            if (this.api.cachePolicy === 'save')
                localStorage.setItem(`api-handler-${this.api.cacheKey ?? ''}-${key}`, JSON.stringify(data))
        }
    }

    private async cacheHandler(args: ResolveParams<T['params']>): Promise<ISendResponse<any>> {
        const key = this.api.path + JSON.stringify(args)
        if (!this.cache.has(key)) {
            if (this.api.cachePolicy === 'save') {
                const localStorageData = localStorage.getItem(`api-handler-${this.api.cacheKey ?? ''}-${key}`)
                if (localStorageData === null) return this.send(args);
                else return Promise.resolve().then(() => {
                    const cacheData = JSON.parse(localStorageData) as ISendResponse<any>;
                    this.cache.set(key, {
                        data: cacheData,
                        lastTime: new Date()
                    } as CacheData);
                    return cacheData;
                })
            }
            return this.send(args);
        }

        const cacheData: CacheData = this.cache.get(key) as CacheData
        if (this.api.cachePolicy === 'dynamic') {
            if (new Date().getTime() - cacheData.lastTime.getTime() > (this.api.expirationTime ?? 1000 * 60 * 30))
                return this.send(args);
        }
        return Promise.resolve().then(() => {
            return cacheData.data;
        })
    }

    public async sendNotSuccessTips(args: ResolveParams<T['params']>): Promise<SendHandlerResponse> {
        return this.sendHandler(args, {successFunction: null})
    }

    private startNext() {
        if (this.argsQueue.length == 0) return;
        if (typeof this.api.limit === 'undefined' || this.api.limit <= 0) return;
        while (this.limitSet < this.api.limit) {
            this.limitSet += 1;
            const resolve = this.argsQueue.shift() as ((value: any) => void);
            resolve(1);
        }

    }

    private async sendLimit(args: ResolveParams<T['params']>): Promise<ISendResponse<any>> {
        await new Promise((resolve) => {
            if (typeof this.api.limit === 'undefined' || this.api.limit <= 0)
                return resolve(1);
            if (this.limitSet < this.api.limit) {
                this.limitSet += 1;
                return resolve(1);
            }
            this.argsQueue.push(resolve);
        });
        try {
            return await this.cacheHandler(args);
        } finally {
            this.limitSet -= 1;
            this.startNext();
        }
    }

    private hangerBefore(args: ResolveParams<T['params']>) {
        for (const beforeListElement of this.beforeList) {
            if (beforeListElement) {
                args = beforeListElement(args) as ResolveParams<T['params']>;
            }
        }
        return args;
    }

    private hangerComplete(res: Promise<ISendResponse<any>>): Promise<ISendResponse<any>> {
        return this.completeList.reduce((c, p) => {
            if (p)
                return p(c);
            return c;
        }, res)
    }

    private hangerAfter(res: Promise<SendHandlerResponse>): Promise<SendHandlerResponse> {
        return this.afterList.reduce((c, p) => {
            if (p)
                return p(c);
            return c;
        }, res)
    }

    private hangerGuard(args: ResolveParams<T['params']>, info: object): Promise<ISendResponse<any>> {
        const start = this.guardList.reduceRight((next, c) => {
            if (c)
                return (a) => c(a, info, next);
            return next;
        }, ((args) => this.sendLimit(args)) as Hanger<T>['guard'])
        if (start)
            return start(args)
        return this.sendLimit(args);
    }


    public async sendHandler(args: ResolveParams<T['params']>, responseHandlerFunction?: ResponseHandlerFunction): Promise<SendHandlerResponse> {
        const requestTime = new Date();
        const info = {
            requestTime,
            completeTime: new Date()
        }
        args = this.hangerBefore(args)
        const resp = this.hangerComplete(this.hangerGuard(args, info));

        return this.hangerAfter(
            resp
                .catch(e => {
                    if (e instanceof Error)
                        this.processResponse(this.fail, responseHandlerFunction?.failFunction, e)
                    throw e
                })
                .then(sendResponse => {
                    info['completeTime'] = new Date()
                    const isRight = this.expect(sendResponse);
                    if (isRight) {
                        this.processResponse(this.success, responseHandlerFunction?.successFunction, sendResponse)
                        this.saveCacheHandler(args, sendResponse);
                        return {data: sendResponse.data, info} as SendHandlerResponse
                    } else {
                        this.processResponse(this.exception, responseHandlerFunction?.exceptionFunction, sendResponse)
                        throw new Error(sendResponse.message);
                    }
                })
        )

    }


    protected processResponse(define: Function, custom: Function | undefined | null, ...args: any[]) {
        if (typeof custom !== "undefined") {
            if (custom != null)
                custom.apply(this, args)
        } else
            define.apply(this, args);
    }

    private static filteredData(data: GeneralObject): GeneralObject {
        return Object.fromEntries(
            Object.entries(data).filter(([_, value]) => value !== undefined && value !== null)
        );
    }

    public static toQueryString(data: GeneralObject) {
        const queryString = new URLSearchParams(ApiHandler.filteredData(data)).toString();
        return queryString.length > 0 ? `?${queryString}` : '';
    }

    public static toPathString(data: GeneralObject) {
        const path = Object.entries(ApiHandler.filteredData(data)).map(([_, value]) => {
            return `${value}`
        }).join("/")
        return ("/" + path).substring(1);

    }

    public static paramsToObject(params: string[], args: ObjectMapping<string, any>) {
        return params.reduce((p, c) => {
            p[c] = args[c];
            return p;
        }, {} as ObjectMapping<string, any>)
    }

    public static assemblyParameters(params: string[], args: ObjectMapping<string, any> | undefined, api: IApi) {
        if (typeof args === 'undefined') return;
        const paramsType = api.paramsDefault ?? "query"
        const paramsObject = this.paramsToObject(params, args);
        if (paramsType === "body")
            return paramsObject;
        if (paramsType === 'query')
            return this.toQueryString(paramsObject);
        return this.toPathString(paramsObject);

    }

    public static assemblyParametersToUnion(params: IApiParamsCollection, args: ObjectMapping<string, any> | undefined, api: IApi) {
        if (typeof args === 'undefined') return {
            body: {},
            query: "",
            path: "",
        };
        const types = Object.entries(params).reduce((p, [k, v]) => {
            let path = k;
            let paramsType = api.paramsDefault ?? "query"
            if (typeof v === 'string') path = v;
            else {
                path = v.name ?? k;
                paramsType = v.type ?? paramsType
            }
            // @ts-ignore
            p[paramsType][path] = args[path];
            return p;
        }, {
            body: {},
            query: {},
            path: {},
        })
        return {
            body: types.body,
            query: this.toQueryString(types.query),
            path: this.toPathString(types.path)
        }
    }

}

export {ApiHandler}