/**
 * 统一接口公共模块
 * @date 2022-03-04
 * @author 何华东
 */

import axios, { AxiosInstance } from 'axios';
import { logger } from "mslib"
import { CryptoUtils } from "./Security";
import Contants, { IS_DEBUG, IS_PROD, IS_TEST } from './Contants'
import DateUtils from './Date';
import { replaceAll, uuid } from './Utils';
import { ApiError, throwApiErrorFromAxiosError } from './Errors';
import { IocConfigProvider, TyApiConfig, BasicConfig } from './Config';
import { ConfigCenterConfigProvider } from './ConfigCenter';

/**
 * 统一接口请求基类
 */
export class ApiRequestBase {
    /**
     * 车场id
     */
    parkId?: any;

    /**
     * 接口业务代码
     */
    serviceCode?: string;
}

/**
 * 统一/非标接口响应
 */
export class ApiResponse<T = any> {
    constructor(resCode: any) {
        this.resCode = resCode;
    }

    /**
     * 响应码,0为成功
     */
    resCode: string;

    /**
     * 响应信息
     */
    resMsg?: string;

    /**
     * 响应数据
     */
    data?: T;

    /**
     * 判断响应码是否为成功
     */
    static resCodeIsSuccess(resCode: string) {
        return resCode?.toString() === '0';
    }

    /**
     * 判断响应是否成功
     */
    static isSuccess(apiResponse: ApiResponse) {
        return this.resCodeIsSuccess(apiResponse?.resCode);
    }

    /**
     * 成功响应实例,resCode=0
     */
    static readonly success: ApiResponse = new ApiResponse<void>('0');

    /**
     * 创建一个失败响应，默认响应码为-1
     * @param resMsg 
     * @returns 
     */
    static failed(resMsg?: string) {
        let apiResponse = new ApiResponse('-1');
        apiResponse.resMsg = resMsg;
        return apiResponse;
    }

    /**
     * 重推响应实例,resCode=999
     */
    static readonly repush: ApiResponse = new ApiResponse<void>('999');

    static successWithData(data: any) {
        let apiResponse = new ApiResponse('0');
        apiResponse.data = data;
        return apiResponse;
    }

    static successWithMsg(resMsg: string) {
        let apiResponse = new ApiResponse('0');
        apiResponse.resMsg = resMsg;
        return apiResponse;
    }
}

/**
 * 如果统一接口返回的响应码为非成功，则抛出异常
 * 
 * @throws {@link Error} if `!ApiResponse.resCodeIsSuccess(apiResponse.resCode)`
 */
export function throwIfResCodeNoSuccess(res: ApiResponse) {
    if (!ApiResponse.resCodeIsSuccess(res.resCode)) {
        throw new Error(res.resMsg || `统一接口返回错误,返回值:${JSON.stringify(res)}`);
    }
}

/**
 * 签名工具
 */
export class SignUtils {
    /**
     * 获取签名
     */
    static getSign(data: object, appSecret: string) {
        let signStr = Object.keys(data).sort().filter(s => {
            if (data[s] === undefined || data[s] === null || data[s] === '') return false;
            if (['appId', 'key'].includes(s) || typeof data[s] === 'object' || Array.isArray(data[s])) return false;

            return true;
        }).map(k => `${k}=${data[k]}`).join('&') + `&${appSecret}`;

        let sign = CryptoUtils.md5(signStr).toUpperCase();

        return sign;
    }
}

let tyapi_config: TyApiConfig = IocConfigProvider.getTyApiConfig();

/**
 * 统一上行接口装饰器
 * 
 * 对请求进行验签(仅`PROD`环境), 异常捕获, 记录日志, 自动返回失败报文`ApiResponse.failed('xxx')`
 * 
 * ```ts
 * import { Singleton } from "typescript-ioc";
 * import { IService } from "mslib"
 * import { TyApi, TyExternalApi, ApiResponse } from "refs/nslib";
 * 
 * export class PostCarInInfo implements IService {
 *    //去掉注释
 *    //@TyApi.Service()
 *    async doService(input: TyExternalApi.PostCarInInfoInput): Promise<ApiResponse> {
 *        //todo...
 *          
 *        return ApiResponse.success;
 *    }
 * }
 * ```
 * @param description 服务描述，仅为日志输出
 */
export function Service(description?: string, config?: { isLog?: boolean }): MethodDecorator {
    return function (target: Object, propertyKey: string, descriptor: PropertyDescriptor) {
        logger.info(`${Contants.SS_SERVICE_BASE_URL}/${Contants.P_ID}.${target.constructor.name}/1.0`, description);

        let method = descriptor.value;

        descriptor.value = async function (...args) {
            logger.info(`统一上行接口 ${description || target.constructor.name} 请求参数:${JSON.stringify(args)}`);

            let response;

            try {
                if (IS_PROD) {
                    if (!tyapi_config?.appSecret || !args[0]?.key) return ApiResponse.failed('验签失败');
                    let new_sign = SignUtils.getSign(args[0], tyapi_config.appSecret);
                    if (new_sign !== args[0].key) return ApiResponse.failed('验签失败');
                }

                response = await method.apply(this, args);
            } catch (error) {
                logger.error(`统一上行接口 ${description || target.constructor.name} 出现异常`, error);
                response = error instanceof Error ? ApiResponse.failed(error.message) : ApiResponse.failed(error.toString());
            }

            if (!(config?.isLog === false)) logger.info(`统一上行接口 ${description || target.constructor.name} 接口响应:${JSON.stringify(response)}`);

            return response;
        }
    }
}

export function generateAxiosInstance(config: TyApiConfig): AxiosInstance {
    const _config = config;

    const axiosInstance = axios.create({
        baseURL: _config.baseUrl || Contants.TY_API_BASE_URL,
        timeout: _config.timeout || 1000 * 10,
        headers: {
            'Content-Type': 'application/json;charset=utf-8',
            'accept-language': _config.acceptLanguage || 'zh-CN',
            version: '1.0.0'
        }
    });

    axiosInstance.interceptors.request.use(config => {
        config.data.appId = _config.appId;
        config.data.reqId = uuid().replace(/-/g, '');
        config.data.ts = DateUtils.getUnixTimestamp();

        if (!config.data.serviceCode) {
            let serviceCode = config.url.split('/').pop();
            config.data.serviceCode = serviceCode.replace(serviceCode[0], serviceCode[0].toLowerCase());

            if (tyapi_config.serviceCode && !tyapi_config.serviceCode.split(',').includes(config.data.serviceCode)) {
                config.data.serviceCode = '接口未授权'
            }
        }

        config.data.key = SignUtils.getSign(config.data, _config.appSecret);

        return config;
    });

    axiosInstance.interceptors.response.use(response => {
        //过滤日志,仅正式环境&接口返回成功
        if (Contants.IS_PROD && ApiResponse.resCodeIsSuccess(response.data?.resCode)) {
            if (_config.isLog === false || response.config['isLog'] === false) return response;
            if (response.config.url.includes('/dbproxy/')) return response;
        }

        logger.info(`统一接口 ${response.config['apiDesc'] || response.config.url} 调用成功,请求地址:${response.config.baseURL + response.config.url},请求头:${JSON.stringify(response.config.headers)},请求参数:${response.config.data},返回值:${JSON.stringify(response.data)}`);

        return response;
    }, error => {
        if (error.isAxiosError) {
            logger.error(`统一接口 ${error.config['apiDesc'] || error.config.url} 调用失败,错误信息:${error.toString()},请求地址:${error.config.baseURL + error.config.url},请求头:${JSON.stringify(error.config.headers)},请求参数:${error.config.data},返回值:${JSON.stringify(error.response?.data)}`);
        }
    
        return Promise.reject(error);
    });

    return axiosInstance;
}

/**
 * 统一接口axios实例
 * 
 * `Ioc`注册`tyApi`配置将会自动创建该实例; @see {@link Config}
 * 
 * 如已引用配置依赖[NsConfig](https://me.keytop.cn/service/config), 那么该实例的根地址将会以配置中心的统一接口根地址为准
 * 
 * ```ts
 * import { axiosInstance } from "refs/nslib";
 * 
 * //直接使用axios实例调用未封装的接口
 * //公共参数不需要赋值，会自动填充
 * //serviceCode也可以不填, 会自动按照接口地址最后一个单词(首字母转小写)填充，如果servieceCode不遵循此规则，则需要手动赋值
 * let res = await axiosInstance.post('get', {
 *    data:1
 * });
 * ```
 */
export let axiosInstance: AxiosInstance;

init();

async function init() {
    try {
        let config = IocConfigProvider.getTyApiConfig();
        if (!config) return;

        if (config.baseUrl) {
            axiosInstance = generateAxiosInstance(config);
        }
        else {
            let remoteconfig = await ConfigCenterConfigProvider.getTyApiConfig();
            config.baseUrl = remoteconfig?.ty_url || Contants.TY_API_BASE_URL;
            axiosInstance = generateAxiosInstance(config);
        }

        logger.info(`统一接口axios实例创建成功,baseUrl:${config.baseUrl},appId:${config.appId}`);
    } catch (error) {
        logger.error(error);
    }
}