/*
 * Copyright [2020-2030] [https://www.stylefeng.cn]
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 * Guns采用APACHE LICENSE 2.0开源协议，您在使用过程中，需要注意以下几点：
 *
 * 1.请不要删除和修改根目录下的LICENSE文件。
 * 2.请不要删除和修改Guns源码头部的版权声明。
 * 3.请保留源码和相关描述文件的项目出处，作者声明等。
 * 4.分发源码时候，请注明软件出处 https://gitee.com/stylefeng/guns
 * 5.在修改包名，模块名称，项目代码等时，请注明软件出处 https://gitee.com/stylefeng/guns
 * 6.若您的项目无法满足以上几点，可申请商业授权
 */
package cn.stylefeng.roses.kernel.scanner;

import cn.hutool.core.exceptions.UtilException;
import cn.hutool.core.net.NetUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.RandomUtil;
import cn.hutool.core.util.StrUtil;
import cn.stylefeng.roses.kernel.rule.enums.ResBizTypeEnum;
import cn.stylefeng.roses.kernel.rule.util.AopTargetUtils;
import cn.stylefeng.roses.kernel.scanner.api.ResourceCollectorApi;
import cn.stylefeng.roses.kernel.scanner.api.annotation.ApiResource;
import cn.stylefeng.roses.kernel.scanner.api.annotation.GetResource;
import cn.stylefeng.roses.kernel.scanner.api.annotation.PostResource;
import cn.stylefeng.roses.kernel.scanner.api.constants.ScannerConstants;
import cn.stylefeng.roses.kernel.scanner.api.context.MetadataContext;
import cn.stylefeng.roses.kernel.scanner.api.exception.ScannerException;
import cn.stylefeng.roses.kernel.scanner.api.factory.ClassMetadataFactory;
import cn.stylefeng.roses.kernel.scanner.api.holder.IpAddrHolder;
import cn.stylefeng.roses.kernel.scanner.api.pojo.resource.FieldMetadata;
import cn.stylefeng.roses.kernel.scanner.api.pojo.resource.ParameterMetadata;
import cn.stylefeng.roses.kernel.scanner.api.pojo.resource.ResourceDefinition;
import cn.stylefeng.roses.kernel.scanner.api.pojo.scanner.ScannerProperties;
import cn.stylefeng.roses.kernel.scanner.api.util.MethodReflectUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;

import java.lang.annotation.Annotation;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Type;
import java.util.*;

import static cn.stylefeng.roses.kernel.scanner.api.exception.enums.ScannerExceptionEnum.ERROR_CONTROLLER_NAME;

/**
 * 资源扫描器，扫描控制器上的@ApiResource，@GetResource，@PostResource
 *
 * @author fengshuonan
 * @since 2020/10/19 22:31
 */
@Slf4j
public class ApiResourceScanner implements BeanPostProcessor {

    /**
     * 权限资源收集接口
     */
    private final ResourceCollectorApi resourceCollectorApi;

    /**
     * 项目的配置信息
     */
    private final ScannerProperties scannerProperties;

    public ApiResourceScanner(ResourceCollectorApi resourceCollectorApi, ScannerProperties scannerProperties) {
        this.resourceCollectorApi = resourceCollectorApi;
        this.scannerProperties = scannerProperties;
    }

    @Override
    public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
        return bean;
    }

    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {

        // 如果controller是代理对象,则需要获取原始类的信息
        Object aopTarget = AopTargetUtils.getTarget(bean);

        if (aopTarget == null) {
            aopTarget = bean;
        }

        Class<?> clazz = aopTarget.getClass();

        // 判断是不是控制器,不是控制器就略过
        boolean controllerFlag = getControllerFlag(clazz);
        if (!controllerFlag) {
            return bean;
        }

        // 扫描控制器的所有带ApiResource注解的方法
        List<ResourceDefinition> apiResources = doScan(clazz);

        // 将扫描到的注解转化为资源实体存储到缓存
        persistApiResources(apiResources);

        return bean;
    }

    /**
     * 判断一个类是否是控制器
     *
     * @author fengshuonan
     * @since 2020/12/9 11:21
     */
    private boolean getControllerFlag(Class<?> clazz) {
        Annotation[] annotations = clazz.getAnnotations();
        for (Annotation annotation : annotations) {
            if (RestController.class.equals(annotation.annotationType()) || Controller.class.equals(annotation.annotationType())) {
                return true;
            }
        }
        return false;
    }

    /**
     * 扫描整个类中包含的所有@ApiResource资源
     *
     * @author fengshuonan
     * @since 2020/12/9 11:21
     */
    private List<ResourceDefinition> doScan(Class<?> clazz) {
        // 绑定类的code-中文名称映射
        ApiResource classApiAnnotation = clazz.getAnnotation(ApiResource.class);
        if (classApiAnnotation != null) {
            if (StrUtil.isEmpty(classApiAnnotation.code())) {
                String className = clazz.getSimpleName();
                int controllerIndex = className.indexOf("Controller");
                if (controllerIndex == -1) {
                    throw new IllegalArgumentException("controller class name is not illegal, it should end with Controller!");
                }
                String code = className.substring(0, controllerIndex);
                this.resourceCollectorApi.bindResourceName(StrUtil.toUnderlineCase(code), classApiAnnotation.name());
            } else {
                this.resourceCollectorApi.bindResourceName(StrUtil.toUnderlineCase(classApiAnnotation.code()), classApiAnnotation.name());
            }
        }

        ArrayList<ResourceDefinition> apiResources = new ArrayList<>();
        Method[] declaredMethods = clazz.getDeclaredMethods();
        if (declaredMethods.length > 0) {
            for (Method declaredMethod : declaredMethods) {
                ApiResource apiResource = declaredMethod.getAnnotation(ApiResource.class);
                GetResource getResource = declaredMethod.getAnnotation(GetResource.class);
                PostResource postResource = declaredMethod.getAnnotation(PostResource.class);

                Annotation annotation = null;
                if (apiResource != null) {
                    annotation = apiResource;
                }

                if (getResource != null) {
                    annotation = getResource;
                }

                if (postResource != null) {
                    annotation = postResource;
                }

                if (annotation != null) {
                    ResourceDefinition definition = createDefinition(clazz, declaredMethod, annotation);
                    apiResources.add(definition);
                    log.debug("扫描到资源: " + definition);
                }
            }
        }
        return apiResources;
    }

    /**
     * 缓存扫描到的api资源
     *
     * @author fengshuonan
     * @since 2020/12/9 11:22
     */
    private void persistApiResources(List<ResourceDefinition> apiResources) {
        resourceCollectorApi.collectResources(apiResources);
    }

    /**
     * 根据类信息，方法信息，注解信息创建ResourceDefinition对象
     *
     * @author fengshuonan
     * @since 2020/12/9 11:22
     */
    private ResourceDefinition createDefinition(Class<?> controllerClass, Method method, Annotation apiResource) {
        ResourceDefinition resourceDefinition = new ResourceDefinition();

        // 填充控制器类的名称
        resourceDefinition.setClassName(controllerClass.getSimpleName());

        // 填充方法名称
        resourceDefinition.setMethodName(method.getName());

        // 填充模块编码，模块编码就是控制器名称截取Controller关键字前边的字符串
        String className = resourceDefinition.getClassName();

        int controllerIndex = className.indexOf("Controller");
        if (controllerIndex == -1) {
            throw new ScannerException(ERROR_CONTROLLER_NAME, controllerClass.getName());
        }
        String modular = className.substring(0, controllerIndex);
        resourceDefinition.setModularCode(modular);

        // 填充模块的中文名称
        ApiResource classApiAnnotation = controllerClass.getAnnotation(ApiResource.class);
        resourceDefinition.setModularName(classApiAnnotation.name());

        // 如果控制器类上标识了appCode则应用标识上的appCode,如果控制器上没标识则用配置文件中的appCode
        if (StrUtil.isNotBlank(classApiAnnotation.appCode())) {
            resourceDefinition.setAppCode(classApiAnnotation.appCode());
        } else {
            resourceDefinition.setAppCode(scannerProperties.getAppCode());
        }

        // 如果没有填写code则用"模块名称_方法名称"为默认的标识
        String code = invokeAnnotationMethod(apiResource, "code", String.class);
        if (StrUtil.isEmpty(code)) {
            resourceDefinition.setResourceCode(
                    resourceDefinition.getAppCode() + scannerProperties.getLinkSymbol() + StrUtil.toUnderlineCase(
                            modular) + scannerProperties.getLinkSymbol() + StrUtil.toUnderlineCase(method.getName()));
        } else {
            resourceDefinition.setResourceCode(
                    resourceDefinition.getAppCode() + scannerProperties.getLinkSymbol() + StrUtil.toUnderlineCase(
                            modular) + scannerProperties.getLinkSymbol() + StrUtil.toUnderlineCase(code));
        }

        // 填充其他属性
        String name = invokeAnnotationMethod(apiResource, "name", String.class);
        String[] methodPath = invokeAnnotationMethod(apiResource, "path", String[].class);
        RequestMethod[] requestMethods = invokeAnnotationMethod(apiResource, "method", RequestMethod[].class);
        Boolean viewFlag = invokeAnnotationMethod(apiResource, "viewFlag", Boolean.class);
        ResBizTypeEnum methodResBizType = invokeAnnotationMethod(apiResource, "resBizType", ResBizTypeEnum.class);

        resourceDefinition.setResourceName(name);

        // 填充权限校验标识
        this.processPermissionWithParentCondition(classApiAnnotation, apiResource, resourceDefinition);

        // 填充资源的业务类型
        if (!methodResBizType.equals(ResBizTypeEnum.DEFAULT)) {
            resourceDefinition.setResourceBizType(methodResBizType.getCode());
        } else {
            resourceDefinition.setResourceBizType(classApiAnnotation.resBizType().getCode());
        }

        // 根据控制器和控制器方法的path组装最后的url
        String controllerMethodPath = createControllerPath(controllerClass, methodPath[0]);
        resourceDefinition.setUrl(createFinalUrl(controllerMethodPath));

        // 如果注解标识是视图类型，则判断该资源是视图类型（优先级最高）
        if (viewFlag) {
            resourceDefinition.setViewFlag(true);
        }
        // 如果资源url是以/view开头，则是视图类型
        else if (StrUtil.isNotBlank(controllerMethodPath)) {
            resourceDefinition.setViewFlag(controllerMethodPath.toLowerCase().startsWith(ScannerConstants.VIEW_CONTROLLER_PATH_START_WITH));
        }
        // 其他都是非视图类型
        else {
            resourceDefinition.setViewFlag(false);
        }

        StringBuilder methodNames = new StringBuilder();
        for (RequestMethod requestMethod : requestMethods) {
            methodNames.append(requestMethod.name()).append(",");
        }
        resourceDefinition.setHttpMethod(StrUtil.removeSuffix(methodNames.toString(), ","));

        // 填充ip地址
        String localMacAddress = IpAddrHolder.get();
        if (localMacAddress == null) {
            try {
                localMacAddress = NetUtil.getLocalhostStr();
                IpAddrHolder.set(localMacAddress);
            } catch (UtilException e) {
                log.error("获取当前机器ip地址错误！");
            }
        }

        resourceDefinition.setIpAddress(localMacAddress == null ? "" : localMacAddress);
        resourceDefinition.setCreateTime(new Date());

        // 填充项目编码
        resourceDefinition.setProjectCode(scannerProperties.getAppCode());

        // 填充方法的校验分组
        Set<String> methodValidateGroup = MethodReflectUtil.getMethodValidateGroup(method);
        if (methodValidateGroup != null) {
            resourceDefinition.setValidateGroups(methodValidateGroup);
        }

        // 填充方法返回结果字段的详细信息
        Type returnType = MethodReflectUtil.getMethodReturnType(method);
        String processReturnTypeUuid = RandomUtil.randomString(32);
        resourceDefinition.setResponseFieldDescriptions(ClassMetadataFactory.beginCreateFieldMetadata(returnType, processReturnTypeUuid));
        MetadataContext.cleanContext(processReturnTypeUuid);

        // 填充方法的请求参数字段的详细信息
        List<ParameterMetadata> methodParameterInfos = MethodReflectUtil.getMethodParameterInfos(method);
        if (methodParameterInfos.size() > 0) {
            LinkedHashSet<FieldMetadata> fieldMetadataLinkedHashSet = new LinkedHashSet<>();
            for (ParameterMetadata parameterMetadata : methodParameterInfos) {
                String parameterContextUuid = RandomUtil.randomString(32);
                // 将当前参数的类型加到context中，后边会用到
                MetadataContext.addParamTypeMetadata(parameterContextUuid, parameterMetadata.getParamTypeEnum());
                MetadataContext.addParameterName(parameterContextUuid, parameterMetadata.getParameterName());
                fieldMetadataLinkedHashSet.add(
                        ClassMetadataFactory.beginCreateFieldMetadata(parameterMetadata.getParameterizedType(), parameterContextUuid));
                MetadataContext.cleanContext(parameterContextUuid);
            }
            resourceDefinition.setParamFieldDescriptions(fieldMetadataLinkedHashSet);
        }

        return resourceDefinition;
    }

    /**
     * 根据控制器类上的RequestMapping注解的映射路径，以及方法上的路径，拼出整个接口的路径
     *
     * @param clazz 控制器的类
     * @param path  控制器方法注解上的路径
     * @author fengshuonan
     * @since 2021/1/5 14:43
     */
    private String createControllerPath(Class<?> clazz, String path) {
        String controllerPath;

        ApiResource controllerRequestMapping = clazz.getDeclaredAnnotation(ApiResource.class);
        if (controllerRequestMapping == null) {
            controllerPath = "";
        } else {
            String[] paths = controllerRequestMapping.path();
            if (paths.length > 0) {
                controllerPath = paths[0];
            } else {
                controllerPath = "";
            }
        }

        // 控制器上的path要以/开头
        if (!controllerPath.startsWith("/")) {
            controllerPath = "/" + controllerPath;
        }

        // 前缀多个左斜杠替换为一个
        return (controllerPath + path).replaceAll("/+", "/");
    }

    /**
     * 根据appCode和contextPath等，拼出整个接口的路径
     *
     * @param controllerMethodPath 控制器和控制器方法的path的组合
     * @author fengshuonan
     * @since 2020/12/14 22:17
     */
    private String createFinalUrl(String controllerMethodPath) {

        // 拼接最终url的时候，依据如下规则拼接：/appCode/contextPath/xxx
        // 第一部分是appCode
        String appCode = "";
        if (scannerProperties.getUrlWithAppCode()) {
            appCode = "/" + StrUtil.removePrefix(scannerProperties.getAppCode(), "/");
        }

        // 第二部分是context-path
        String contextPath = "";
        if (scannerProperties.getUrlWithContextPath()) {
            contextPath = "/" + StrUtil.removePrefix(scannerProperties.getContextPath(), "/");
        }

        // 依据如下规则拼接：/appCode/contextPath/xxx
        String resultPath = appCode + contextPath + controllerMethodPath;

        // 前缀多个左斜杠替换为一个
        resultPath = resultPath.replaceAll("/+", "/");

        return resultPath;
    }

    /**
     * 调用注解上的某个方法，并获取结果
     *
     * @author fengshuonan
     * @since 2020/12/8 17:13
     */
    private <T> T invokeAnnotationMethod(Annotation apiResource, String methodName, Class<T> resultType) {
        try {
            Class<? extends Annotation> annotationType = apiResource.annotationType();
            Method method = annotationType.getMethod(methodName);
            return (T) method.invoke(apiResource);
        } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
            log.error("扫描api资源时出错!", e);
        }
        throw new RuntimeException("扫描api资源时出错!");
    }

    /**
     * 【7月7日新增逻辑】处理当前接口的认证和鉴权标识
     * <p>
     * 如果class类上有标识，但是本接口没有，则以class类上为准
     * 如果本接口有权限标识，则以本接口为准
     *
     * @param classApiAnnotation 类上的@ApiResource注解的配置
     * @param methodApiResource  方法上的api注解
     * @param resourceDefinition 把最终信息汇总到此资源定义
     * @author fengshuonan
     * @since 2023/7/7 17:57
     */
    private void processPermissionWithParentCondition(ApiResource classApiAnnotation, Annotation methodApiResource,
                                                      ResourceDefinition resourceDefinition) {

        // 设置是否认证的开关，这个开关只以方法上的注解为准
        Boolean requiredLogin = invokeAnnotationMethod(methodApiResource, "requiredLogin", Boolean.class);
        resourceDefinition.setRequiredLoginFlag(requiredLogin);

        // 设置是否鉴权的开关，如果方法没有则判断控制器上，如果控制器有权限校验编码设置，则以控制器为准
        Boolean requiredPermission = invokeAnnotationMethod(methodApiResource, "requiredPermission", Boolean.class);
        String requirePermissionCode = invokeAnnotationMethod(methodApiResource, "requirePermissionCode", String.class);

        if ((requiredPermission == null || !requiredPermission)) {

            // 鉴权开关为空，但是方法上的鉴权编码不为空，则直接设置鉴权开关为true并返回
            if (ObjectUtil.isNotEmpty(requirePermissionCode)) {
                requiredPermission = true;
            }

            // 鉴权开关为空，鉴权编码也为空，则此时去查询类上是否配置了鉴权编码
            else {
                if (ObjectUtil.isNotEmpty(classApiAnnotation.requirePermissionCode())) {
                    requiredPermission = true;
                    requirePermissionCode = classApiAnnotation.requirePermissionCode();
                }
            }
        }

        // 设置权限校验标识
        resourceDefinition.setRequiredPermissionFlag(requiredPermission);
        resourceDefinition.setPermissionCode(requirePermissionCode);
    }

}
