package com.face.global.service.impl;

import com.baomidou.mybatisplus.annotation.FieldStrategy;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.baomidou.mybatisplus.core.conditions.Wrapper;
import com.baomidou.mybatisplus.core.enums.SqlMethod;
import com.baomidou.mybatisplus.core.incrementer.IdentifierGenerator;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baomidou.mybatisplus.core.metadata.TableInfo;
import com.baomidou.mybatisplus.core.metadata.TableInfoHelper;
import com.baomidou.mybatisplus.core.override.MybatisMapperProxy;
import com.baomidou.mybatisplus.core.toolkit.*;
import com.baomidou.mybatisplus.core.toolkit.reflect.GenericTypeUtils;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.baomidou.mybatisplus.extension.toolkit.SqlHelper;

import com.google.common.base.CaseFormat;
import com.face.global.consts.GlobalConstant;
import com.face.global.enums.DeleteType;
import com.face.global.enums.UserTypeEnum;
import com.face.global.mapper.GlobalBaseMapper;
import com.face.global.service.IGlobalBaseService;
import com.face.global.user.UserStore;
import com.face.global.vo.GlobalUser;
import com.face.global.vo.Operate;
import com.face.util.JsonUtils;
import com.face.util.SystemExceptionUtils;
import org.apache.commons.lang3.reflect.FieldUtils;
import org.apache.ibatis.binding.MapperMethod;
import org.apache.ibatis.logging.Log;
import org.apache.ibatis.logging.LogFactory;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionTemplate;
import org.springframework.aop.framework.AopProxyUtils;
import org.springframework.aop.support.AopUtils;
import org.springframework.cglib.beans.BeanMap;
import org.springframework.transaction.annotation.Transactional;

import java.io.Serializable;
import java.lang.reflect.Field;
import java.lang.reflect.Proxy;
import java.util.*;
import java.util.function.BiConsumer;
import java.util.function.Function;

import static com.baomidou.mybatisplus.core.toolkit.StringPool.*;

public class GlobalBaseService<T, ID extends Serializable> implements IGlobalBaseService<T, ID> {

    private GlobalBaseMapper<T, ID> globalBaseMapper;

    private IdentifierGenerator identifierGenerator;

    private volatile SqlSessionFactory sqlSessionFactory;

    protected final Log log = LogFactory.getLog(getClass());

    private Class<GlobalBaseMapper<T, ID>> mapperClass;

    public GlobalBaseService(GlobalBaseMapper<T, ID> globalBaseMapper, IdentifierGenerator identifierGenerator) {
        this.globalBaseMapper = globalBaseMapper;
        this.identifierGenerator = identifierGenerator;
        this.mapperClass = (Class<GlobalBaseMapper<T, ID>>) globalBaseMapper.getClass().getInterfaces()[0];
    }

    @Transactional
    @Override
    public int insert(T entity) {
        return this.globalBaseMapper.insert(entity);
    }

    @Transactional
    @Override
    public int deleteById(ID id) {
        return this.globalBaseMapper.deleteById(id);
    }

    @Transactional
    @Override
    public int deleteById(T entity) {
        return this.globalBaseMapper.deleteById(entity);
    }

    @Transactional
    @Override
    public int deleteByMap(Map columnMap) {
        return this.globalBaseMapper.deleteByMap(columnMap);
    }

    @Transactional
    @Override
    public int delete(Wrapper<T> queryWrapper) {
        return this.globalBaseMapper.delete(queryWrapper);
    }

    @Transactional
    @Override
    public int updateByIdForInt(T entity) {
        return this.globalBaseMapper.updateById(entity);
    }

    @Transactional
    @Override
    public int updateForInt(T entity, Wrapper<T> updateWrapper) {
        return this.globalBaseMapper.update(entity, updateWrapper);
    }

    @Transactional
    @Override
    public int updateForInt(Wrapper<T> updateWrapper) {
        return this.globalBaseMapper.update(updateWrapper);
    }

    @Override
    public T selectById(ID id) {
        return this.globalBaseMapper.selectById(id);
    }

    @Override
    public T selectOne(Wrapper<T> queryWrapper) {
        return this.globalBaseMapper.selectOne(queryWrapper);
    }

    @Override
    public T selectOne(Wrapper<T> queryWrapper, boolean throwEx) {
        return this.globalBaseMapper.selectOne(queryWrapper, throwEx);
    }

    @Override
    public boolean exists(Wrapper<T> queryWrapper) {
        return this.globalBaseMapper.exists(queryWrapper);
    }

    @Override
    public BaseMapper<T> getBaseMapper() {
        return this.globalBaseMapper;
    }

    @Override
    public List<T> selectByMap(Map columnMap) {
        return this.globalBaseMapper.selectByMap(columnMap);
    }

    @Override
    public Long selectCount(Wrapper<T> queryWrapper) {
        return this.globalBaseMapper.selectCount(queryWrapper);
    }

    @Override
    public List<T> selectList(Wrapper<T> queryWrapper) {
        return this.globalBaseMapper.selectList(queryWrapper);
    }

    @Override
    public List<T> selectBatchIds(Collection<ID> idList) {
        return this.globalBaseMapper.selectBatchIds(idList);
    }

    @Transactional
    @Override
    public int deleteBatchIds(Collection<ID> idList) {
        return this.globalBaseMapper.deleteBatchIds(idList);
    }

    @Override
    public String generatorStringId() {
        return String.valueOf(identifierGenerator.nextId(null));
    }

    @Override
    public long generatorLongId() {
        return identifierGenerator.nextId(null).longValue();
    }

    @Transactional
    @Override
    public int insertH(ID id, Function<ID, Map<String, Object>> content, Operate operate) {
        Map<Object, Object> data = new HashMap<>();
        data.putAll(BeanMap.create(operate));
        if (operate.getExtendsData() != null) {
            data.putAll(operate.getExtendsData());
        }
        data.put("createDate", operate.getModifyDate());
        data.put("isDelete", DeleteType.NO_DELETE.getCode());
        data.put("createUser", operate.getModifyUser());
        data.put("createUserType", operate.getModifyUserType());
        if (operate.isGenerateId()) {
            operate.setId(this.generatorStringId());
        }
        data.put("id", operate.getId());
        data.put("mainId", id);
        Object contentObject = content.apply(id);
        if (contentObject != null) {
            data.put("content", JsonUtils.toJsonString(contentObject));
        }
        return this.globalBaseMapper.insertH(data);
    }


    @Override
    public Operate getUpdateOperate(boolean generateSeq) {
        Operate operate = Operate.inst()
                .modifyUser(this.getOperateUser().getUserId())
                .modifyUserType(this.getOperateUser().getUserType()).update();
        if (generateSeq) {
            operate.setSeqno(this.generatorLongId());
        }
        return operate;
    }

    @Override
    public Operate getInsertOperate(boolean generateSeq) {
        Operate operate = Operate.inst()
                .modifyUser(this.getOperateUser().getUserId())
                .modifyUserType(this.getOperateUser().getUserType()).insert();
        if (generateSeq) {
            operate.setSeqno(this.generatorLongId());
        }
        return operate;
    }

    @Override
    public Operate getDeleteOperate(boolean generateSeq) {
        Operate operate = Operate.inst()
                .modifyUser(this.getOperateUser().getUserId())
                .modifyUserType(this.getOperateUser().getUserType())
                .delete();
        if (generateSeq) {
            operate.setSeqno(this.generatorLongId());
        }
        return operate;
    }

    @Transactional
    @Override
    public boolean updateSomeColumnById(List list) {
        Class modelClass = GenericTypeUtils.resolveTypeArguments(getClass(), GlobalBaseService.class)[0];
        TableName tableNameAnnotation = (TableName) modelClass.getAnnotation(TableName.class);
        String tableName = tableNameAnnotation.value();
        Field[] baseFields = FieldUtils.getAllFields(modelClass);
        List<String> keys = new ArrayList<>();
        for (Field field : baseFields) {
            if (field.getAnnotation(TableId.class) != null) {
                keys.add(field.getName());
            }
        }
        if (CollectionUtils.isEmpty(keys)) {
            throw SystemExceptionUtils.buildBusinessException("noKeys");
        }
        String sqlStatement = GlobalBaseMapper.class.getName() + StringPool.DOT + "updateSomeColumnById";
        Field[] updateFields = FieldUtils.getAllFields(list.get(0).getClass());
        List<String> allUpdateFields = Arrays.stream(updateFields).filter(field -> {
            if (keys.contains(field.getName())) {
                return false;
            }
            TableField tableField = field.getAnnotation(TableField.class);
            if (tableField == null) {
                return true;
            }
            return tableField.updateStrategy() != FieldStrategy.NEVER;
        }).map(field -> field.getName()).toList();
        if (CollectionUtils.isEmpty(allUpdateFields)) {
            throw SystemExceptionUtils.buildBusinessException("noUpdateColumn");
        }
        StringBuffer updateSql = new StringBuffer();
        StringBuffer whereSql = new StringBuffer();
        List<String> wheres = new ArrayList<>();
        List<String> updates = new ArrayList<>();
        for (String key : keys) {
            wheres.add(CaseFormat.LOWER_CAMEL.to(CaseFormat.LOWER_UNDERSCORE, key).concat(EQUALS)
                    .concat(HASH_LEFT_BRACE).concat("entity.").concat(key).concat(RIGHT_BRACE));
        }
        whereSql.append(String.join(" and ", wheres));
        for (String key : allUpdateFields) {
            updates.add(CaseFormat.LOWER_CAMEL.to(CaseFormat.LOWER_UNDERSCORE, key).concat(EQUALS)
                    .concat(HASH_LEFT_BRACE).concat("entity.").concat(key).concat(RIGHT_BRACE));
        }
        updateSql.append(String.join(" and ", updates));
        return SqlHelper.executeBatch(getSqlSessionFactory(), this.log, list, DEFAULT_BATCH_SIZE,
                (sqlSession, data) -> {
                    sqlSession.update(sqlStatement, Map.of("tableName", tableName, "updateSql", updateSql.toString(), "whereSql", whereSql.toString(), "entity", data));
                });
    }

    @Override
    public GlobalUser getOperateUser() {
        GlobalUser globalUser = new GlobalUser();
        if (UserStore.getCurrentUser() == null) {
            globalUser.setUserName(GlobalConstant.SYSTEM_OPERATE_USER_ID);
            globalUser.setUserType(UserTypeEnum.SYS_USER.getCode());
            globalUser.setUserId(GlobalConstant.SYSTEM_OPERATE_USER_ID);
            return globalUser;
        }
        return UserStore.getCurrentUser();
    }

    protected GlobalUser getOperateUser(String userId, UserTypeEnum userTypeEnum) {
        GlobalUser globalUser = new GlobalUser();
        if (UserStore.getCurrentUser() == null) {
            globalUser.setUserType(userTypeEnum.getCode());
            globalUser.setUserId(userId);
            return globalUser;
        }
        return UserStore.getCurrentUser();
    }

    protected final Class<?>[] typeArguments = GenericTypeUtils.resolveTypeArguments(getClass(), ServiceImpl.class);

    @Override
    public Class<T> getEntityClass() {
        return (Class<T>) this.typeArguments[1];
    }

    protected SqlSessionFactory getSqlSessionFactory() {
        if (this.sqlSessionFactory == null) {
            synchronized (this) {
                if (this.sqlSessionFactory == null) {
                    Object target = this.globalBaseMapper;
                    // 这个检查目前看着来说基本上可以不用判断Aop是不是存在了.
                    if (com.baomidou.mybatisplus.extension.toolkit.AopUtils.isLoadSpringAop()) {
                        while (AopUtils.isAopProxy(target)) {
                            target = AopProxyUtils.getSingletonTarget(target);
                        }
                    }
                    if (target instanceof MybatisMapperProxy) {
                        MybatisMapperProxy mybatisMapperProxy = (MybatisMapperProxy) Proxy.getInvocationHandler(target);
                        SqlSessionTemplate sqlSessionTemplate = (SqlSessionTemplate) mybatisMapperProxy.getSqlSession();
                        this.sqlSessionFactory = sqlSessionTemplate.getSqlSessionFactory();
                    } else if (Proxy.getInvocationHandler(target) instanceof MybatisMapperProxy) {
                        MybatisMapperProxy mybatisMapperProxy = (MybatisMapperProxy) Proxy.getInvocationHandler(target);
                        SqlSessionTemplate sqlSessionTemplate = (SqlSessionTemplate) mybatisMapperProxy.getSqlSession();
                        this.sqlSessionFactory = sqlSessionTemplate.getSqlSessionFactory();
                    } else {
                        this.sqlSessionFactory = GlobalConfigUtils.currentSessionFactory(this.getEntityClass());
                    }
                }
            }
        }
        return this.sqlSessionFactory;
    }

    /**
     * 批量插入
     *
     * @param entityList ignore
     * @param batchSize  ignore
     * @return ignore
     */
    @Transactional(rollbackFor = Exception.class)
    @Override
    public boolean saveBatch(Collection<T> entityList, int batchSize) {
        String sqlStatement = getSqlStatement(SqlMethod.INSERT_ONE);
        return executeBatch(entityList, batchSize, (sqlSession, entity) -> sqlSession.insert(sqlStatement, entity));
    }

    /**
     * 获取mapperStatementId
     *
     * @param sqlMethod 方法名
     * @return 命名id
     * @since 3.4.0
     */
    protected String getSqlStatement(SqlMethod sqlMethod) {
        return SqlHelper.getSqlStatement(mapperClass, sqlMethod);
    }

    /**
     * TableId 注解存在更新记录，否插入一条记录
     *
     * @param entity 实体对象
     * @return boolean
     */
    @Override
    public boolean saveOrUpdate(T entity) {
        if (null != entity) {
            TableInfo tableInfo = TableInfoHelper.getTableInfo(this.getEntityClass());
            Assert.notNull(tableInfo, "error: can not execute. because can not find cache of TableInfo for entity!");
            String keyProperty = tableInfo.getKeyProperty();
            Assert.notEmpty(keyProperty, "error: can not execute. because can not find column for id from entity!");
            Object idVal = tableInfo.getPropertyValue(entity, tableInfo.getKeyProperty());
            return StringUtils.checkValNull(idVal) || Objects.isNull(getById((Serializable) idVal)) ? save(entity) : updateById(entity);
        }
        return false;
    }

    @Transactional(rollbackFor = Exception.class)
    @Override
    public boolean saveOrUpdateBatch(Collection<T> entityList, int batchSize) {
        TableInfo tableInfo = TableInfoHelper.getTableInfo(this.getEntityClass());
        Assert.notNull(tableInfo, "error: can not execute. because can not find cache of TableInfo for entity!");
        String keyProperty = tableInfo.getKeyProperty();
        Assert.notEmpty(keyProperty, "error: can not execute. because can not find column for id from entity!");
        return SqlHelper.saveOrUpdateBatch(getSqlSessionFactory(), this.mapperClass, log, entityList, batchSize, (sqlSession, entity) -> {
            Object idVal = tableInfo.getPropertyValue(entity, keyProperty);
            return StringUtils.checkValNull(idVal)
                    || CollectionUtils.isEmpty(sqlSession.selectList(getSqlStatement(SqlMethod.SELECT_BY_ID), entity));
        }, (sqlSession, entity) -> {
            MapperMethod.ParamMap<T> param = new MapperMethod.ParamMap<>();
            param.put(Constants.ENTITY, entity);
            sqlSession.update(getSqlStatement(SqlMethod.UPDATE_BY_ID), param);
        });
    }

    @Transactional(rollbackFor = Exception.class)
    @Override
    public boolean updateBatchById(Collection<T> entityList, int batchSize) {
        String sqlStatement = getSqlStatement(SqlMethod.UPDATE_BY_ID);
        return executeBatch(entityList, batchSize, (sqlSession, entity) -> {
            MapperMethod.ParamMap<T> param = new MapperMethod.ParamMap<>();
            param.put(Constants.ENTITY, entity);
            sqlSession.update(sqlStatement, param);
        });
    }

    @Override
    public T getOne(Wrapper<T> queryWrapper, boolean throwEx) {
        return this.globalBaseMapper.selectOne(queryWrapper, throwEx);
    }

    @Override
    public Optional<T> getOneOpt(Wrapper<T> queryWrapper, boolean throwEx) {
        return Optional.ofNullable(this.globalBaseMapper.selectOne(queryWrapper, throwEx));
    }

    @Override
    public Map<String, Object> getMap(Wrapper<T> queryWrapper) {
        return SqlHelper.getObject(log, this.globalBaseMapper.selectMaps(queryWrapper));
    }

    @Override
    public <V> V getObj(Wrapper<T> queryWrapper, Function<? super Object, V> mapper) {
        return SqlHelper.getObject(log, listObjs(queryWrapper, mapper));
    }

    /**
     * 执行批量操作
     *
     * @param list      数据集合
     * @param batchSize 批量大小
     * @param consumer  执行方法
     * @param <E>       泛型
     * @return 操作结果
     * @since 3.3.1
     */
    protected <E> boolean executeBatch(Collection<E> list, int batchSize, BiConsumer<SqlSession, E> consumer) {
        return SqlHelper.executeBatch(getSqlSessionFactory(), this.log, list, batchSize, consumer);
    }

    @Override
    public boolean removeById(Serializable id) {
        TableInfo tableInfo = TableInfoHelper.getTableInfo(getEntityClass());
        if (tableInfo.isWithLogicDelete() && tableInfo.isWithUpdateFill()) {
            return removeById(id, true);
        }
        return SqlHelper.retBool(getBaseMapper().deleteById(id));
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public boolean removeByIds(Collection<?> list) {
        if (CollectionUtils.isEmpty(list)) {
            return false;
        }
        TableInfo tableInfo = TableInfoHelper.getTableInfo(getEntityClass());
        if (tableInfo.isWithLogicDelete() && tableInfo.isWithUpdateFill()) {
            return removeBatchByIds(list, true);
        }
        return SqlHelper.retBool(getBaseMapper().deleteBatchIds(list));
    }


}
