package com.cf.imes.module.report.service.datasource;

import cn.hutool.core.annotation.AnnotationUtil;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.ObjectUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.sql.SqlInjectionUtils;
import com.bstek.common.exception.ReportDesignException;
import com.bstek.common.utils.MultipleJdbcTemplate;
import com.bstek.datasource.bean.DataSourceInfo;
import com.bstek.datasource.bean.PreviewParams;
import com.bstek.ureport.definition.dataset.Field;
import com.bstek.ureport.definition.dataset.Parameter;
import com.bstek.ureport.definition.dataset.SqlDatasetDefinition;
import com.bstek.ureport.utils.ProcedureUtils;
import com.cf.imes.framework.common.exception.ServiceException;
import com.cf.imes.framework.common.exception.util.ServiceExceptionUtil;
import com.cf.imes.framework.common.util.object.BeanUtils;
import com.cf.imes.framework.mybatis.core.generator.SnowFlakeGenerator;
import com.cf.imes.framework.mybatis.core.query.LambdaQueryWrapperX;
import com.cf.imes.framework.organ.core.aop.OrganIgnore;
import com.cf.imes.framework.security.core.LoginUser;
import com.cf.imes.framework.security.core.util.SecurityFrameworkUtils;
import com.cf.imes.module.report.controller.admin.dataset.vo.ReportDatasetReqVO;
import com.cf.imes.module.report.controller.admin.datasource.vo.ReportBeanDatasourceRespVO;
import com.cf.imes.module.report.controller.admin.datasource.vo.ReportDatasourceSaveReqVO;
import com.cf.imes.module.report.dal.dataobject.dataset.ReportDatasetDO;
import com.cf.imes.module.report.dal.dataobject.datasource.ReportDatasourceDO;
import com.cf.imes.module.report.dal.mysql.dataset.ReportDatasetMapper;
import com.cf.imes.module.report.dal.mysql.datasource.ReportDatasourceMapper;
import com.cf.imes.module.report.enums.template.ReportTemplateTypeEnum;
import com.cf.imes.module.report.framework.ureport.annotation.CfReportSpringbeanDatasource;
import com.cf.imes.module.report.service.dataset.ReportDatasetService;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.jdbc.core.ResultSetExtractor;
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
import org.springframework.jdbc.datasource.SingleConnectionDataSource;
import org.springframework.jdbc.support.JdbcUtils;
import org.springframework.stereotype.Service;
import org.springframework.validation.annotation.Validated;

import jakarta.annotation.Resource;
import javax.sql.DataSource;
import java.beans.PropertyDescriptor;
import java.lang.reflect.Method;
import java.sql.Connection;
import java.sql.ResultSetMetaData;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import static com.cf.imes.framework.common.exception.util.ServiceExceptionUtil.exception;
import static com.cf.imes.module.report.enums.ErrorCodeConstants.DATASET_GET_FIELDS_ERROR;
import static com.cf.imes.module.report.enums.ErrorCodeConstants.DATASET_SQL_INJECTION_RISK;
import static com.cf.imes.module.report.enums.ErrorCodeConstants.DATASET_SQL_REQUIRED;
import static com.cf.imes.module.report.enums.ErrorCodeConstants.DATASOURCE_BUILDIN_OPERATION_PERMISSION_ERROR;
import static com.cf.imes.module.report.enums.ErrorCodeConstants.DATASOURCE_NOT_EXISTS;
import static com.cf.imes.module.report.enums.ErrorCodeConstants.DATASOURCE_SPRINGBEAN_GET_FAIL;
import static com.cf.imes.module.report.enums.ErrorCodeConstants.DATASOURCE_SPRINGBEAN_METHODS_GET_FAIL;

/**
 * 报表数据源 Service 实现类
 *
 * @author Gqr
 * @since 2024/7/8 11:01
 */
@Service
@Validated
@Slf4j
public class ReportDatasourceServiceImpl implements ReportDatasourceService {
    @Resource
    private ReportDatasourceMapper datasourceMapper;

    @Resource
    private ReportDatasetService datasetService;

    @Resource
    private ReportDatasetMapper datasetMapper;

    @Resource
    private SnowFlakeGenerator snowFlakeGenerator;

    private final ApplicationContext applicationContext;

    public ReportDatasourceServiceImpl(ApplicationContext applicationContext) {
        this.applicationContext = applicationContext;
    }

    @Override
    public Long createDatasource(ReportDatasourceSaveReqVO createReqVO) {
        // 校验内置数据源操作权限
        validateSystemDatasource(createReqVO.getBuildinType());
        // 插入
        ReportDatasourceDO datasource = BeanUtils.toBean(createReqVO, ReportDatasourceDO.class);
        // 超管创建的作为内置数据源
        if (SecurityFrameworkUtils.isSuperAdmin()) {
            datasource.setBuildinType(ReportTemplateTypeEnum.SYSTEM);
        } else {
            datasource.setBuildinType(ReportTemplateTypeEnum.CUSTOM);
        }
        // 生成id
        datasource.setId((Long) snowFlakeGenerator.nextId(null));
        datasourceMapper.insert(datasource);
        // 返回
        return datasource.getId();
    }

    @Override
    public void updateDatasource(ReportDatasourceSaveReqVO updateReqVO) {
        // 校验内置数据源操作权限
        validateSystemDatasource(updateReqVO.getBuildinType());
        // 校验存在
        validateDatasourceExists(updateReqVO.getId());
        ReportDatasourceDO updateObj = BeanUtils.toBean(updateReqVO, ReportDatasourceDO.class);
        // 更新
        datasourceMapper.updateById(updateObj);
    }


    @Override
    public void deleteDatasource(Long id) {
        // 校验存在
        ReportDatasourceDO reportDatasourceDO = validateDatasourceExists(id);
        // 校验内置数据源操作权限
        validateSystemDatasource(reportDatasourceDO.getBuildinType().getType());
        // 删除
        datasourceMapper.deleteById(id);
        // 删除关联数据集
        datasetMapper.delete(new LambdaQueryWrapperX<ReportDatasetDO>().eq(ReportDatasetDO::getDatasourceId, id));
    }

    @Override
    public ReportDatasourceDO getDatasource(Long id) {
        ReportDatasourceDO reportDatasourceDO = datasourceMapper.selectNormalDatasourceById(id, SecurityFrameworkUtils.getUserOrganId());
        // 查询数据集
        queryDatasetInSource(reportDatasourceDO);
        return reportDatasourceDO;
    }

    @Override
    @OrganIgnore
    public List<ReportDatasourceDO> getDatasourceList(Long organId) {
        LoginUser loginUser = SecurityFrameworkUtils.getLoginUser();
        LambdaQueryWrapper<ReportDatasourceDO> queryWrapper = new LambdaQueryWrapperX<ReportDatasourceDO>().orderByDesc(ReportDatasourceDO::getCreateTime);
        // 当前登录人机构id
        Long currentOrganId = loginUser.getOrganId();
        // 超管查看不同组织数据源
        if (ObjectUtil.isNotNull(loginUser) && Boolean.TRUE.equals(loginUser.getIsSupAdmin()) && ObjectUtil.isNotNull(organId)) {
            currentOrganId = organId;
        }
        Long finalCurrentOrganId = currentOrganId;
        queryWrapper.or(wrapper -> wrapper
                        .eq(ReportDatasourceDO::getOrganId, finalCurrentOrganId)
                        .eq(ReportDatasourceDO::getBuildinType, ReportTemplateTypeEnum.CUSTOM))
                .or(wrapper -> wrapper.eq(ReportDatasourceDO::getBuildinType, ReportTemplateTypeEnum.SYSTEM));

        List<ReportDatasourceDO> reportDatasourceDOS = datasourceMapper.selectList(queryWrapper);
        // 查询数据集
        if (CollUtil.isNotEmpty(reportDatasourceDOS)) {
            reportDatasourceDOS.forEach(reportDatasourceDO -> {
                queryDatasetInSource(reportDatasourceDO);
            });
        }
        return reportDatasourceDOS;
    }

    @Override
    public List<ReportBeanDatasourceRespVO> getBeanDatasourceList() {
        Map<String, Object> beansWithAnnotation = applicationContext.getBeansWithAnnotation(CfReportSpringbeanDatasource.class);
        List<ReportBeanDatasourceRespVO> beanDatasourceRespVOList = new ArrayList<>();
        for (Map.Entry<String, Object> entry : beansWithAnnotation.entrySet()) {
            Class<?> beanClass = entry.getValue().getClass();
            ReportBeanDatasourceRespVO respVO = ReportBeanDatasourceRespVO.builder()
                    .value(AnnotationUtil.getAnnotationValue(beanClass, CfReportSpringbeanDatasource.class, "value").toString())
                    .name(AnnotationUtil.getAnnotationValue(beanClass, CfReportSpringbeanDatasource.class, "name").toString()).build();
            beanDatasourceRespVOList.add(respVO);
        }
        return beanDatasourceRespVOList;
    }

    @Override
    public List<String> loadBeanMethods(String beanId) {
        Object obj;
        List<String> result = new ArrayList<String>();
        try {
            obj = applicationContext.getBean(beanId);
            Class<?> clazz = obj.getClass();
            Method[] methods = clazz.getMethods();

            // ureport的springbean方法必须满足（String dsName, String datasetName, Map<String, Object> parameters）
            for (Method method : methods) {
                Class<?>[] types = method.getParameterTypes();
                if (types.length != 3) {
                    continue;
                }
                Class<?> typeClass1 = types[0];
                Class<?> typeClass2 = types[1];
                Class<?> typeClass3 = types[2];
                if (!String.class.isAssignableFrom(typeClass1)) {
                    continue;
                }
                if (!String.class.isAssignableFrom(typeClass2)) {
                    continue;
                }
                if (!Map.class.isAssignableFrom(typeClass3)) {
                    continue;
                }
                result.add(method.getName());
            }
        } catch (BeansException e) {
            log.error("[ReportDatasourceService][loadBeanMethods]获取bean失败", e);
            throw ServiceExceptionUtil.exception(DATASOURCE_SPRINGBEAN_GET_FAIL, beanId);
        } catch (Exception e) {
            log.error("[ReportDatasourceService][loadBeanMethods]方法列表获取异常", e);
            throw ServiceExceptionUtil.exception(DATASOURCE_SPRINGBEAN_METHODS_GET_FAIL);
        }
        return result;
    }

    @Override
    public List<Field> getTableFields(PreviewParams previewParams) {
        Connection conn = null;
        String sql = previewParams.getSql();
        List<Parameter> parameters = previewParams.getParameters();
        DataSourceInfo info = previewParams.getInfo();
        try {
            if (StringUtils.isEmpty(sql)) {
                throw exception(DATASET_SQL_REQUIRED);
            }
            // 获取数据库连接
            conn = MultipleJdbcTemplate.buildConnection(info);
            // 检查参数sql注入
            for (Parameter parameter : parameters) {
                // mybatis-plus util检查参数
                if (SqlInjectionUtils.check(parameter.getDefaultValue())) {
                    throw exception(DATASET_SQL_INJECTION_RISK);
                }
            }
            // 准备sql执行参数
            SqlDatasetDefinition sqlDatasetDefinition = new SqlDatasetDefinition();
            sqlDatasetDefinition.setSql(sql);
            sqlDatasetDefinition.setParameters(BeanUtils.toBean(parameters, Parameter.class));

            Map<String, Object> params = new HashMap<>();
            Map<String, Object> pmap = sqlDatasetDefinition.buildParameters(params);
            String sqlForUse = sqlDatasetDefinition.parseSql(pmap, previewParams.getTenantParams());
            if (ProcedureUtils.isProcedure(sqlForUse)) {
                return ProcedureUtils.procedureColumnsQuery(sqlForUse, pmap, conn);
            }
            DataSource dataSource = new SingleConnectionDataSource(conn, false);
            NamedParameterJdbcTemplate jdbcTemplate = new NamedParameterJdbcTemplate(dataSource);
            final List<Field> fields = new ArrayList<>();
            jdbcTemplate.query(sqlForUse, pmap, (ResultSetExtractor<Integer>) rs -> {
                ResultSetMetaData metadata = rs.getMetaData();
                List<String> metadataList = new ArrayList<>();
                for (int i = 1; i <= metadata.getColumnCount(); i++) {
                    String columnName = metadata.getColumnLabel(i);
                    metadataList.add(columnName);
                    fields.add(new Field(columnName));
                }
                return null;
            });
            return fields;
        } catch (ServiceException sex) {
            throw sex;
        } catch (Exception ex) {
            log.error("[getTableFields][报表数据源获取表字段异常]", ex);
            throw new ServiceException(DATASET_GET_FIELDS_ERROR);
        } finally {
            JdbcUtils.closeConnection(conn);
        }
    }

    @Override
    public List<Field> getSpringBeanResultFieldList(String clazz) {
        List<Field> result = new ArrayList<Field>();
        try {
            Class<?> targetClass = Class.forName(clazz);
            PropertyDescriptor[] propertyDescriptors = BeanUtil.getPropertyDescriptors(targetClass);
            for (PropertyDescriptor pd : propertyDescriptors) {
                String name = pd.getName();
                if ("class".equals(name)) {
                    continue;
                }
                result.add(new Field(name));
            }
            return result;

        } catch (Exception ex) {
            throw new ReportDesignException(ex);
        }
    }

    /**
     * 查询数据源下的数据集
     *
     * @param reportDatasourceDO
     */
    private void queryDatasetInSource(ReportDatasourceDO reportDatasourceDO) {
        if (ObjectUtil.isNotNull(reportDatasourceDO)) {
            // 查询数据集
            List<ReportDatasetDO> datasetList = datasetService.getDatasetList(ReportDatasetReqVO.builder().datasourceId(reportDatasourceDO.getId()).build());
            reportDatasourceDO.setDatasets(datasetList);
        }
    }

    /**
     * 校验数据源是否存在
     *
     * @param id 数据源id
     */
    private ReportDatasourceDO validateDatasourceExists(Long id) {
        ReportDatasourceDO reportDatasourceDO = datasourceMapper.selectNormalDatasourceById(id, SecurityFrameworkUtils.getUserOrganId());
        if (reportDatasourceDO == null) {
            throw exception(DATASOURCE_NOT_EXISTS);
        } else {
            return reportDatasourceDO;
        }
    }

    /**
     * 校验只有超管可以操作内置数据源
     *
     * @param buildinType 内置数据源类型
     */
    private void validateSystemDatasource(Integer buildinType) {
        // 非超管不能操作内置模板
        if (ReportTemplateTypeEnum.SYSTEM.getType().equals(buildinType) && Boolean.FALSE.equals(SecurityFrameworkUtils.isSuperAdmin())) {
            throw exception(DATASOURCE_BUILDIN_OPERATION_PERMISSION_ERROR);
        }
    }
}
