import { batchCmdElement } from "../types/batchElement";
import { eventElement } from "../types/eventElement";

import axios, { AxiosError, AxiosRequestConfig, AxiosResponse } from 'axios';
import { CallResult } from "../callResult";
import {cloneDeep, get as __get} from 'lodash'
import { logger } from "../types/authBaseServe";
import { getAuth } from "../types/getAuth";

// проблемы декларирования методов HTMLElement
declare global{
    interface Document{
        attachEvent(event: string, listener: EventListener): boolean;
        detachEvent(event: string, listener: EventListener): void;
    }

    interface Window{
        attachEvent(event: string, listener: EventListener): boolean;
        detachEvent(event: string, listener: EventListener): void;
    }
}

interface elementCbArray{
    uid:string,
    cb:(args:any)=>void
}


export abstract class baseBX24{
    cbArray:elementCbArray[]=[];

    AUTH_CONNECTOR="";

    CLIENT_ID: string | undefined="";
    CLIENT_SECRET: string | undefined="";

    isInit=false;
    DOMAIN: string;
    PROTOCOL: number;
    APP_SID: string|boolean=false;
    PATH="/rest";
    LANG="";
    AUTH_ID="";
    REFRESH_ID="";
    MEMBER_ID="";
    PLACEMENT="";
    IS_ADMIN=false;
    AUTH_EXPIRES=0;
    USER_OPTIONS: any;
    APP_OPTIONS: any;
    PLACEMENT_OPTIONS: any;
    url='';
    timeoutCall=0;
    
    arrEvents:eventElement[]=[];

    logger:logger=console;

    isReadyVal=false; //check

    getHttpString(value:any, prefix=''):string{
        if (value instanceof Date){
            return prefix+'='+encodeURIComponent(value.toISOString());
        }
        else if (typeof value=='object'){
            const resultObj=[];
            for (const field in value){
                resultObj.push(this.getHttpString(value[field], prefix+`${prefix!=''?'[':""}${field}${prefix!=''?']':''}`));
            }
            return resultObj.join('&');
        }
        else if (prefix!=''){
            return encodeURIComponent(prefix)+'='+encodeURIComponent(value);
        }
        return encodeURIComponent(value);
    }

    setTimeoutCall(ms:number){
        this.timeoutCall=ms;
    }

    clearTimeoutCall(){
        this.timeoutCall=0;
    }

    isFunction(item:any){
        return item === null ? false : (typeof (item) == "function" || item instanceof Function);
    }

    addEvent(event:string, handler:(params:any)=>void){
        this.arrEvents.push({
            event:event,
            handler:handler
        });
    }

    emitEvent(event:string, params?:any){
        const arrHandler=this.arrEvents.filter(el=>{return el.event==event});
        for (const idx in arrHandler){
            setTimeout(()=>{
                arrHandler[idx].handler.call(this, params);
            }, 10);
        }
    }



    uniqid(){
        const charsList = '0123456789abcdefghijklmnopqrstuvwxyz';
        let s = '';
        for (let i = 0; i <32; i++)
            s += charsList[Math.round(Math.random()*(charsList.length-1))];
        return s;
    }

    setCallback(cb?:(args:any)=>void){
        const cbId = this.uniqid();
        if (cb){
            this.cbArray.push({uid:cbId, cb:cb});
        }

        return cbId;
    }

    abstract runCallback(e:MessageEvent):void

    doInit(){
        this.emitEvent('init', this);
    }

    abstract sendMessage(cmd:string, params:any, cb?:(params:any)=>void):void

    constructor(){
        this.DOMAIN="";
        this.PROTOCOL=1;
    }

    userOption={
        get:(name:string)=>{
            return this.USER_OPTIONS[name];
        },
        set:(name:string, value:any, cb:(params:any)=>void)=>{
            this.USER_OPTIONS[name] = value;
            this.sendMessage('setUserOption', {name:name,value:value}, cb);
        }
    }

    appOption={
		get: (name:string)=>{
			return this.APP_OPTIONS[name];
		},
		set:(name:string,value:any,cb:(params:any)=>void)=>{
            if (this.isAdmin()){
                this.APP_OPTIONS[name] = value;
                this.sendMessage('setAppOption', {name:name,value:value}, cb);
            }
			else{
                console.error('Access denied!');
            }
		}
	}

    
    utilReady(){
        if (document.readyState === "complete")
		{
			return this.runReady();
		}

        let __readyHandler:EventListener;
		if (document.addEventListener)
		{
			__readyHandler=()=>{
				document.removeEventListener("DOMContentLoaded", __readyHandler, false);
				this.runReady();
			}
			document.addEventListener("DOMContentLoaded", __readyHandler, false);
			window.addEventListener("load", ()=>{this.runReady()}, false);
		}
		else if (document.attachEvent)
		{
			__readyHandler =()=>{
				if (document.readyState === "complete")
				{
					document.detachEvent("onreadystatechange", __readyHandler);
					this.runReady();
				}
			}
			document.attachEvent("onreadystatechange", __readyHandler);
			window.attachEvent("onload", ()=>{this.runReady()});
		}

		this.utilReady = () => null;

		return null;
    }

    runReady():void{
		if (!this.isReadyVal)
		{
			if (!document.body){
				setTimeout(this.runReady, 15);
                return;
            }

			this.isReadyVal = true;

			this.ready = (handler)=>{
				if (typeof handler=='function')
				{
                    setTimeout(handler, 10);
				}
			};

			this.emitEvent('ready');
		}

		return;
	}

    abstract refreshAuth(cb?:(params:any)=>void):void;
    abstract refreshAuthAsync():Promise<getAuth>;

    protected callSuccess(xhr:AxiosResponse):boolean{
        return typeof xhr.status=='undefined' || (xhr.status >= 200 && xhr.status < 300) || xhr.status === 304 || xhr.status >= 400 && xhr.status < 500 || xhr.status === 1223 || xhr.status === 0;
    }

    protected call(url:string, config:{
        method:string,
        data:any,
        callback?:(params: any)=>void
    }):Promise<CallResult>{
        return new Promise((resolve, reject)=>{
            const params=cloneDeep(config.data);
            params.auth=this.AUTH_ID;
            if (this.AUTH_CONNECTOR&&!params.auth_connector){
                params.auth_connector=this.AUTH_CONNECTOR;
            }
            const options:AxiosRequestConfig = {
                method: 'POST',
                headers: { 'content-type': 'application/x-www-form-urlencoded' },
                data: this.getHttpString(params),
                url:url
            };

            if (this.timeoutCall){
                options.timeout=this.timeoutCall;
            }

            axios(options).then(res=>{
                const data = res.data;
                const result = new CallResult(data, config, this, res.status);
                resolve(result);
            })
            .catch((err:AxiosError)=>{
                if (!err?.response?.data){
                    reject(err);
                }
                else if (__get(err, ['response','data','error'], undefined)=='expired_token'&&!url.includes('oauth.bitrix.info/oauth/token/')){
                    try {
                        this.refreshAuth(()=>{
                            this.call(url, config).then(resolve).catch(reject);
                        });                        
                    } catch (error) {
                        reject(error);
                    }
                }
                else{
                    reject(__get(err, ['response','data'], err.message)); 
                }           
            });
        });
    }

    getAuth(){
        return (this.isInit && this.AUTH_EXPIRES > (new Date()).valueOf())
		? {access_token: this.AUTH_ID, refresh_token: this.REFRESH_ID, expires_in: this.AUTH_EXPIRES, domain: this.DOMAIN, member_id: this.MEMBER_ID}
		: false;
    }

    // callBatch(cmd:batchCmdElement, haltOnError?:boolean):Promise<{[key:string|number]:CallResult}>;
    // callBatch(cmd:batchCmdElement, cb:(params:{[key:string|number]:CallResult})=>void|boolean, haltOnError?:boolean):void;
    // callBatch(cmd:batchCmdElement, haltOnError?:boolean):Promise<{[key:string|number]:CallResult}>;
    // callBatch(cmd:batchCmdElement, cb:(params:{[key:string|number]:CallResult})=>void|boolean, haltOnError?:boolean):void;
    // callBatch(
    //         cmd:batchCmdElement, 
    //         cb?:((params:{[key:string|number]:CallResult})=>void)|boolean, 
    //         haltOnError=false):void|Promise<{[key:string|number]:CallResult
    //         }>
    callBatch<T extends batchCmdElement>(cmd:T, haltOnError?:boolean):Promise<{[key in keyof T]:CallResult}>
    callBatch<T extends batchCmdElement>(cmd:T, cb:(params:{[key in keyof T]:CallResult})=>void):void
    callBatch<T extends batchCmdElement>(cmd:T, cb:(params:{[key in keyof T]:CallResult})=>void, haltOnError:boolean):void
    callBatch<T extends batchCmdElement>(
        cmd:T,
        cb?:((params:{[key in keyof T]:CallResult})=>void)|boolean,
        haltOnError=false
    ):void|Promise<{[key in keyof T]:CallResult}>
            {
                const startCb=cb;
                const startHaltOnError=haltOnError;
                if (typeof cb == 'boolean'){
                    haltOnError=cb;
                    cb=undefined;
                }
                else{
                    cb=cb as (params:{[key in keyof T]:CallResult})=>void;
                }
        
                const comands:Partial<{
                    [key in keyof T]:string
                }>={};
                let cnt=0;
                
                for(const idx in cmd){
                    const row=cmd[idx];
                    const method=Array.isArray(row)?row[0]:row.method;
                    const params=Array.isArray(row)?row[1]:row.params;

                    if(method)
                    {
                        cnt++;
                        comands[idx] = `${method}?${this.getHttpString(params)}`;
                    }
                }

                if (cnt>0){
                    const url=this.url?`${this.url}batch.json`:`http${this.PROTOCOL?'s':''}://${this.DOMAIN}${this.PATH}/batch.json`;
                    const params={
                        cmd:comands as {
                            [key in keyof T]:string
                        },
                        halt:haltOnError?1:0
                    };

                    if (!startCb){
                        if (this.AUTH_EXPIRES<(new Date()).valueOf()){
                            return new Promise((resolve, reject)=>{
                                this.refreshAuthAsync().then(()=>{
                                    this.callBatch(cmd, startHaltOnError).then(res=>{                            
                                        resolve(res);
                                    })
                                    .catch(err=>{
                                        reject(err);
                                    })
                                })
                                .catch((error:Error)=>{
                                    reject(error);
                                });
                            });
                        }
                        else{
                            return new Promise((resolve, reject)=>{
                                this.call(url, {
                                    method:'batch',
                                    data: params,
                                }).then(res=>{
                                    resolve(this.formatResultForBatch(res, cmd));
                                })
                                .catch(reject)
                            });
                        }
                    }
                    else if (typeof startCb=='function'){
                        if (this.AUTH_EXPIRES<(new Date()).valueOf()){
                            this.refreshAuthAsync().then(()=>{
                                this.callBatch(cmd, startCb, startHaltOnError);
                            })
                            .catch(error=>{
                                console.error(error);
                            });
                        }
                        else{
                            this.call(url, {
                                method:'batch',
                                data:params,
                                callback: startCb
                            }).then(res=>{
                                if (typeof cb =='function')
                                    cb(this.formatResultForBatch(res, cmd, cb));
                            })
                            .catch(err=>{
                                if (err instanceof Error){
                                    throw err;
                                }
                                const result=this.formatResultForBatch(
                                    new CallResult(err, {
                                        method:'batch',
                                        data:params
                                    }, this, 500),
                                    cmd,
                                    cb as (params:any)=>void
                                );
                                if (typeof cb ==='function')
                                    cb(result);
                                return false;
                            })
                        }
                    }
                }
                else{
                    return;
                }
    }

    formatResultForBatch<T extends CallResult, R extends batchCmdElement>(
        res:T, 
        calls:R,
        callback?:(params:any)=>void)
        :{[key in keyof R]:CallResult}
    {
        const data = res.data();

        const result:Partial<{[key in keyof R]:CallResult}>={};
        for(const idx in calls){
            const cmd=calls[idx];
            if (data?.result?.[idx]!==undefined|| data?.result_error?.[idx]!==undefined){
                result[idx]=new CallResult({
                    result: data.result?.[idx]||{},
                    error:data.result_error[idx]||undefined,
                    total:data.result_total[idx],
                    time:data.result_time[idx],
                    next: data.result_next[idx]
                }, {
                    method:Array.isArray(cmd)?cmd[0]:cmd?.method,
                    data: Array.isArray(cmd)?cmd[1]:cmd?.params,
                    callback:callback
                }, this, res.status)
            }
            else{
                result[idx]={
                    data:()=>({}),
                    total:()=>0,
                    error_description:()=>JSON.stringify(res),
                    answer:data?.result?.[idx],
                    query: {
                        method:Array.isArray(cmd)?cmd[0]:cmd?.method,
                        data: Array.isArray(cmd)?cmd[1]:cmd?.params,
                        callback:callback
                    },
                    next:()=>false,
                    bx24:this,
                    time:()=>({}),
                    status:res.status,
                    more:()=>false,
                    error:()=>JSON.stringify(res)
                };
            }
        }
        
        return result as { [key in keyof R]: CallResult; };
    }

    
    callMethod(method:string, params:any):Promise<CallResult>;
    callMethod(method:string, params:any, cb?:(params:CallResult)=>void):void;
    callMethod(method:string, params:any, cb?:(params:CallResult)=>void):void|Promise<CallResult>{
        const url=this.url?`${this.url}${method}.json`:`http${this.PROTOCOL?'s':''}://${this.DOMAIN}${this.PATH}/${method}.json`;

        if (!cb){
            if (this.AUTH_EXPIRES<(new Date()).valueOf()){
                return new Promise((resolve, reject)=>{
                    try {
                        this.refreshAuth(()=>{
                            this.callMethod(method, params)?.then(res=>{                            
                                resolve(res);
                            })
                            .catch(err=>{
                                reject(err);
                            })
                        });
                    } catch (error) {
                        reject(error);
                    }
                });                
            }
            else{
                return new Promise((resolve)=>{
                    this.call(url, {
                        method,
                        data:params
                    }).then(res=>{
                        resolve(res);
                    })
                    .catch(err=>{
                        resolve(new CallResult(err, {
                            data:params,
                            method,
                            callback:cb
                        }, this, 500));
                    })
                });
            }
        }
        else {
            if (this.AUTH_EXPIRES<(new Date()).valueOf()){
                this.refreshAuth(()=>{
                    this.callMethod(method, params, cb);
                });
            }
            else{
                this.call(url, {
                    method,
                    data:params,
                    callback:cb
                }).then(res=>{
                    return cb(res);
                })
                .catch(err=>{
                    cb(new CallResult({error_description:String(err)}, {
                        data:params,
                        method,
                        callback:cb
                    },this, 500));
                })
            }
        }
    }

    //Getters
    isAdmin(){
        return this.IS_ADMIN;
    }

    getLang(){
        return this.LANG;
    }

    getDomain(){
        return this.DOMAIN;
    }

    isReady(){
        return this.isReadyVal;
    }

    ready(handler:(params:any)=>void){
        this.addEvent('ready', handler);
    }

    getScrollSize(){
        return {
            scrollWidth: Math.max(document.documentElement.scrollWidth, document.documentElement.offsetWidth),
            scrollHeight: Math.max(document.documentElement.scrollHeight, document.documentElement.offsetHeight)
        };
    }



    ///////////////////////////////////////////////////////////////////////////////////////

    init(callback?:(params:any)=>void){
        if(callback)
        {
            this.addEvent('init', callback);
        }
    }

    install(callback:(params:any)=>void){
        if(callback)
        {
            this.addEvent('install', callback);
        }
    }

    callBind(event:string, handler:string, auth_type?:number, callback?:(params: CallResult) => void){
        if(!this.isInit){
            // var _a = arguments;
            this.init(()=>{
                this.callBind(event, handler, auth_type, callback);
            });
        }
        else if(this.isAdmin())
        {
            const params = {
                event: event||'',
                handler: handler||'',
                auth_type: (typeof auth_type == 'undefined') ? 0 : auth_type
            };

            return this.callMethod('event.bind', params, callback);
        }

        return false;
    }

    //select dialog
    
    selectAccess = (title:string, value:string[], cb:(params:any)=>void)=>{
        if(typeof(value)==='function')
        {
            cb = value; value = [];
        }

        this.sendMessage('selectAccess', {value, title}, cb);
    };

    selectUser = (title:string, cb:(params:any)=>void)=>{
        if(typeof(title)==='function'){
            cb = title; title = '';
        }

        this.sendMessage('selectUser', {title: title, mult:false}, cb);
    };

    selectUsers = (title:string, cb:(params:any)=>void)=>{
        if(typeof (title)!=='string'){
            cb = title; title = '';
        }
        this.sendMessage('selectUser', {title: title, mult:true}, cb);
    };

    selectCRM = (params:{
        entityType:string[],
        multiple?:boolean,
        value?:string[]
    }, cb:(params:any)=>void)=>{
        this.sendMessage('selectCRM', {
            entityType: params.entityType,
            multiple: params.multiple,
            value: params.value,
        }, cb);
    };

    //Methods sendMessage
    installFinish(){
        this.sendMessage('setInstallFinish',{});
    }

    resizeWindow(width:number, height:number, cb:(params:any)=>void){
        if(width > 0 && height > 0){
            this.sendMessage('resizeWindow', {width:width,height:height}, cb);
        }
    }

    fitWindow(cb?:(params:any)=>void){
        this.sendMessage('resizeWindow', {
            width:'100%', height:this.getScrollSize().scrollHeight
        }, cb);
    }

    closeApplication(cb:(params:any)=>void){
        this.sendMessage('closeApplication', {}, cb);
    }

    reloadWindow(cb:(params:any)=>void){
        this.sendMessage('reloadWindow', {}, cb);
    }

    setTitle(title:string, cb:(params:any)=>void){
        this.sendMessage('setTitle', {title:title}, cb);
    }

    scrollParentWindow(scroll:number, cb:(params:any)=>void){
        if(scroll>0)
        {
            this.sendMessage('setScroll', {scroll:scroll}, cb);
        }
    }

    //placement
    placement={
        info:()=>({
            placement:this.PLACEMENT,
            options:this.PLACEMENT_OPTIONS
        }),

        getInterface:(cb?:(params: any) => void)=>{
            this.sendMessage('getInterface', {}, cb);
        },

        // call:(method:string, params:any, cb?:(params:CallResult)=>void):void,
        call:(cmd:string, params?:any, cb?:(params:any)=>void):void=>{
            this.sendMessage(cmd, params, cb);
        },

        bindEvent:(eventName:string, cb:(params:any)=>void)=>{
            this.sendMessage('placementBindEvent', {event:eventName}, cb);
        }
    }
}