/**
 * 装饰器模块
 * @date 2022-03
 * @author 何华东
 */

 import { Container } from "typescript-ioc";
 import { logger } from "mslib"
 import * as schedule from 'node-schedule'
 import Contants from "./Contants";
 import { ApiError } from "./Errors";
 import dayjs = require("dayjs");
 
 /**
  * Cron计划任务
  * 
  * @param cronExpression cron时间表达式
  * @param description 计划任务描述,仅为记录日志
  * @returns 
  */
 export function CronJob(cronExpression: string, description?: string): ClassDecorator {
     return function (target: Function) {
         logger.info(`${Contants.SS_SERVICE_BASE_URL}/${Contants.P_ID}.${target.name}/1.0 ${description}(${cronExpression})`);
 
         //支持开关,一般用于测试环境下禁用计划任务
         if(!Container.getValue('config.cron.enabled')) return;
 
         schedule.scheduleJob(cronExpression, async () => {
             try {
                 let service = Container.get(target);
                 await service.doService();
             } catch (error) {
                 logger.error(`${description || '定时任务'} ${target.name} 出现异常 \n异常信息 >>>>>`, error instanceof ApiError && !Contants.IS_DEBUG ? error.toString() : error);
             }
         });
     }
 }
 
 /**
  * 重试器
  * 
  * 重试条件:
  * 
  * 1.如果可重试目标方法出现异常,会自动进行重试
  * 
  * 2.可重试目标方法未抛出异常,根据返回结果并传入重试条件retryCondition方法判断是否进行重试
  * @param func 可重试的目标方法
  * @param retryMaxCount 重试最大次数
  * @param retryDelayMs 每次重试延迟毫秒数
  * @param errorHandler 可重试的目标方法出现异常时的异常处理,不会打断重试逻辑，一般用于记录日志
  * @param retryCondition 重试条件,传入可重试的目标方法返回的结果判断是否重试
  */
 export function retry(func: () => Promise<any>,
     retryMaxCount: number,
     retryDelayMs: number,
     config?: {
         errorHandler?: (error: any, retryCountLeft: number) => any,
         retryCondition?: (res: any) => boolean
     }
 ) {
     return new Promise<any>(async (resolve, reject) => {
         try {
             let res = await func();
             if (typeof config?.retryCondition !== 'function' || !config.retryCondition(res)) {
                 resolve(res);
                 return;
             }
         } catch (error) {
             if (typeof config?.errorHandler === 'function') config.errorHandler(error, retryMaxCount);
         }
 
         if (retryMaxCount <= 0) {
             reject('已用尽所有尝试次数');
         } else {
             setTimeout(() => {
                 resolve(retry(func, --retryMaxCount, retryDelayMs, config));
             }, retryDelayMs);
         }
     });
 }
 
 /**
  * 重试器
  * 
  * 重试条件:
  * 
  * 1.如果可重试目标方法出现异常,会自动进行重试
  * 
  * 2.可重试目标方法未抛出异常,根据返回结果并传入重试条件retryCondition方法判断是否进行重试
  * @param methodDesc 方法描述,仅为记录日志
  * @param retryMaxCount 重试最大次数
  * @param retryDelayMs 每次重试延迟毫秒数
  * @param errorHandler 可重试的目标方法出现异常时的异常处理,不会打断重试逻辑，一般用于记录日志
  * @param retryCondition 重试条件,传入可重试的目标方法返回的结果判断是否重试
  */
 export function Retryable(methodDesc?: string, retryMaxCount?: number, retryDelayMs?: number, config?: {
     errorHandler?: (error: any, retryCountLeft: number) => any,
     retryCondition?: (res: any) => boolean
 }): MethodDecorator {
     return function (target: Object, key: string, descriptor: PropertyDescriptor) {
         let method = descriptor.value;
 
         descriptor.value = async function (...args) {
             try {
                 let result = await retry(async () => {
                     return await method.apply(this, args);
                 },
                     retryMaxCount || 1,
                     retryDelayMs || 1000 * 60,
                     {
                         errorHandler: (error, retryCountLeft) => {
                             logger.error(`${methodDesc || ''} ${target.constructor.name}/${key} 出现异常(已重试${retryMaxCount - retryCountLeft}次}),方法参数:${JSON.stringify(args)} \n异常信息 >>>>>`, error instanceof ApiError && !Contants.IS_DEBUG ? error.toString() : error);
                         },
                         retryCondition: config?.retryCondition
                     });
 
                 return result;
             } catch (error) {
                 logger.error(`${methodDesc || ''} ${target.constructor.name}/${key} 出现异常,方法参数:${JSON.stringify(args)} \n异常信息 >>>>>`, error);
                 return null;
             }
         }
     }
 }
 
 /**
  * 异步锁
  */
 class AsyncLock {
     disable: any;
     promise: Promise<void> = Promise.resolve();
 
     enable() {
         this.promise = new Promise(resolve => this.disable = resolve);
     }
 }
 
 /**
  * 异步方法同步执行器
  * 
  * 基于Promise实现
  */
 export function Sync(): MethodDecorator {
     return function (target: Object, key: string, descriptor: PropertyDescriptor) {
         let method = descriptor.value;
         let lock = new AsyncLock();
 
         descriptor.value = async function (...args) {
             await lock.promise;
             lock.enable();
 
             try {
                 return method.apply(this, args);
             } catch (error) {
                 throw error;
             } finally {
                 lock.disable();
             }
         }
     }
 }
 
 /**
  * 节流器
  * @param milliseconds 统计周期
  * @param count 最大执行次数
  * @param filter 过滤器,返回true表示不参与节流(包括计数)
  */
export function Throttle(milliseconds: number, count: number, filter?: (args) => boolean): MethodDecorator {
    return function (target: Object, key: string, descriptor: PropertyDescriptor) {
        let method = descriptor.value;
        let count_time = dayjs();
        let counter = 0;

        descriptor.value = async function (...args) {
            if (typeof filter === 'function' && filter.apply(this, args) === true) { 
                return await method.apply(this, args);
            }

            let now = dayjs();

            //新的统计周期,重置统计时间和计数器
            if (dayjs(now).diff(count_time, 'millisecond') > milliseconds) {
                count_time = now;
                counter = 1;
            } else {
                counter++;

                //节流
                if (counter > count) {
                    logger.warn(`${target.constructor.name}触发节流${counter}>${count}(${milliseconds}ms)},args:${JSON.stringify(args)}`);
                    return;
                }
            }

            return await method.apply(this, args);
        }
    }
}