package cn.ey.biz.module.system.service.abac;

import cn.ey.biz.common.pojo.PageResult;
import cn.ey.biz.common.util.collection.CollectionUtils;
import cn.ey.biz.framework.mybatis.core.query.LambdaQueryWrapperX;
import cn.ey.biz.framework.security.core.LoginUser;
import cn.ey.biz.framework.security.core.util.SecurityFrameworkUtils;
import cn.ey.biz.module.system.api.auth.dto.AuthCodeBlockPermissionDTO;
import cn.ey.biz.module.system.controller.admin.abac.vo.AbacPageVo;
import cn.ey.biz.module.system.controller.admin.abac.vo.AbacVo;
import cn.ey.biz.module.system.controller.admin.abac.vo.ServiceLineAbAcVo;
import cn.ey.biz.module.system.convert.abac.AbacConvert;
import cn.ey.biz.module.system.convert.abac.ServiceLineAbAcVoList;
import cn.ey.biz.module.system.dal.dataobject.abac.AbacDO;
import cn.ey.biz.module.system.dal.dataobject.codeblock.CodeBlockDO;
import cn.ey.biz.module.system.dal.dataobject.user.AdminUserDO;
import cn.ey.biz.module.system.dal.mapper.abac.AbacMapper;
import cn.ey.biz.module.system.dal.mapper.codeblock.CodeBlockMapper;
import cn.ey.biz.module.system.dal.redis.abac.AbacRedisDAO;
import cn.ey.biz.module.system.framework.rule.DefaultRuleFactory;
import cn.ey.biz.module.system.framework.rule.Rule;
import cn.ey.biz.module.system.framework.rule.RuleDefinition;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.collection.CollectionUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.validation.annotation.Validated;

import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import java.lang.reflect.Field;
import java.util.*;
import java.util.stream.Collectors;

@Slf4j
@Service
@Validated
public class AbacServiceImpl implements AbacService {

    // 10分钟
    private static final long SCHEDULER_PERIOD = 10 * 60 * 1000L;

    private static final String COMMON_APP = "B009";

    @Resource
    private AbacMapper abacMapper;
    @Resource
    private AbacRedisDAO abacRedisDAO;
    @Resource
    private CodeBlockMapper codeBlockMapper;

    /**
     * 缓存的最大更新时间，用于后续的增量轮询，判断是否有更新
     */
    private volatile Date maxUpdateTime;

    @PostConstruct
    @Override
    public synchronized void initLocalCache() {
        // 获取功能点列表，如果有更新
        List<AbacDO> dos = this.loadAbacIfUpdate(maxUpdateTime);
        if (CollUtil.isEmpty(dos)) {
            return;
        }

        List<AbacVo> functions = AbacConvert.INSTANCE.dos2Vos(dos);

        Set<AbacRedisDTO> splitByAppAndScene = new HashSet<>();
        functions.forEach(abac -> {
            Optional<AbacRedisDTO> target = splitByAppAndScene.stream().filter(item -> item.getApp().equals(abac.getApp())
                    && item.getScene().equals(abac.getScene())).findFirst();
            if (target.isPresent()) {
                target.get().getContent().add(abac);
            } else {
                AbacRedisDTO dto = new AbacRedisDTO();
                dto.setApp(abac.getApp());
                dto.setScene(abac.getScene());
                dto.setContent(new ArrayList<AbacVo>() {{
                    add(abac);
                }});
                splitByAppAndScene.add(dto);
            }
        });

        splitByAppAndScene.forEach(item ->
                abacRedisDAO.refresh(item.getApp(), String.valueOf(item.getScene()), item.getContent()));
        maxUpdateTime = CollectionUtils.getMaxValue(dos, AbacDO::getUpdateTime);
    }

    @Override
    public void resetMaxUpdateTime() {
        log.info("[ABAC] Reset maxUpdateTime");
        maxUpdateTime = null;
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public void createABAC(AbacVo abacVO) {
        if (abacVO.getId() != null) {
            abacMapper.updateById(AbacConvert.INSTANCE.vo2Do(abacVO));
            abacRedisDAO.update(abacVO);
        } else {
            AbacDO doo = AbacConvert.INSTANCE.vo2Do(abacVO);
            abacMapper.insert(doo);
            abacVO.setId(doo.getId());
            abacRedisDAO.set(abacVO);
        }
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public void deleteAbac(AbacVo abacVO) {
        if (abacVO.getId() != null) {
            abacMapper.deleteById(AbacConvert.INSTANCE.vo2Do(abacVO));
            abacRedisDAO.refresh(abacVO.getApp(), String.valueOf(abacVO.getScene()),
                    AbacConvert.INSTANCE.dos2Vos(abacMapper
                            .selectList(new LambdaQueryWrapper<AbacDO>()
                                    .eq(AbacDO::getApp, abacVO.getApp())
                                    .eq(AbacDO::getScene, abacVO.getScene()))));
        }
    }

    @Override
    public List<AbacVo> getAbacByAppAndScene(String app, String scene) {
        return AbacConvert.INSTANCE.dos2Vos(
                abacMapper.selectList(new LambdaQueryWrapperX<AbacDO>()
                        .eq(AbacDO::getApp, app).eq(AbacDO::getScene, scene)));
    }

    @Override
    public PageResult<AbacVo> getAbacByPage(AbacPageVo abacPageVo) {
        LambdaQueryWrapperX<AbacDO> queryWrapperX = new LambdaQueryWrapperX<>();
        queryWrapperX.eq(AbacDO::getDeleted, false)
                .likeIfPresent(AbacDO::getApp, abacPageVo.getApp())
                .likeIfPresent(AbacDO::getName, abacPageVo.getName())
                .eqIfPresent(AbacDO::getScene, abacPageVo.getScene())
                .orderByDesc(AbacDO::getId);
        PageResult<AbacDO> pageResult = abacMapper.selectPage(abacPageVo, queryWrapperX);
        return AbacConvert.INSTANCE.convertPage(pageResult);
    }

    @Override
    public void checkAbac(AdminUserDO user, Set<Long> roleIds, Set<Long> menuIds, Map<String, Object> dynamicAttr, String app) {

        List<AbacVo> abacs = abacRedisDAO.get(COMMON_APP, HUMAN);

        if (abacs == null) {
            abacs = new ArrayList<>();
        }

        if (StringUtils.isNoneBlank(app)) {
            abacs.addAll(abacRedisDAO.get(app, HUMAN));
        }

        abacs = abacs.stream().filter(abac -> abac.getStatus() != 0).collect(Collectors.toList());


        abacs.forEach(abacVo -> {
            if (checkAttribute(abacVo, user, roleIds, dynamicAttr)) {
                roleIds.addAll(abacVo.getRoleIds());
                log.info("[ABAC] ROLE ASSIGN: username={} abac={} ROLES={}."
                        , user.getUsername(), abacVo.getName(), abacVo.getRoleIds());
                if (CollectionUtil.isNotEmpty(abacVo.getMenus())) {
                    menuIds.addAll(abacVo.getMenus());
                    log.info("[ABAC] MENUS ASSIGN: username={} abac={} MENUS={}."
                            , user.getUsername(), abacVo.getName(), abacVo.getMenus());
                }
            }
        });
    }

    @Override
    public AuthCodeBlockPermissionDTO checkAbac(String codeBlock, Map<String, Object> dynamicAttr, String app, String scene) {
        log.info("[ABAC] [CodeBlock] codeblock:{}, app:{}, scene:{}", codeBlock, app, scene);
        AuthCodeBlockPermissionDTO ret = new AuthCodeBlockPermissionDTO();
        CodeBlockDO codeBlockDO = codeBlockMapper.selectOne(new LambdaQueryWrapper<CodeBlockDO>().eq(CodeBlockDO::getCodeBlockCode, codeBlock));
        log.info("[ABAC] [CodeBlock] codeblock:{}, get codeblockDO:{}", codeBlock, codeBlockDO);
        List<AbacVo> abacs = abacRedisDAO.get(app, scene);
        log.info("[ABAC] [CodeBlock] codeblock:{}, app:{}, get abacs:{}", codeBlock, app, abacs);
        abacs.forEach(abacVo -> {
            if (checkAttribute(abacVo, codeBlockDO, dynamicAttr)) {
                ret.getRoleIds().addAll(abacVo.getRoleIds());
                ret.getMenuIds().addAll(abacVo.getMenus());
            }
        });

        return ret;
    }


    @Override
    public List<AuthCodeBlockPermissionDTO> checkAbacList(Collection<String> codeBlock, Map<String, Object> dynamicAttr, String app, String scene) {
        List<AuthCodeBlockPermissionDTO> retList = new ArrayList<>();

        List<CodeBlockDO> codeBlockDOList = codeBlockMapper.selectList(new LambdaQueryWrapper<CodeBlockDO>()
                .in(CodeBlockDO::getCodeBlockCode, codeBlock));

        List<AbacVo> abacs = abacRedisDAO.get(app, scene);
        if (CollectionUtil.isNotEmpty(codeBlockDOList)) {
            for (CodeBlockDO codeBlockDO : codeBlockDOList) {
                AuthCodeBlockPermissionDTO ret = new AuthCodeBlockPermissionDTO();
                abacs.forEach(abacVo -> {
                    if (checkAttribute(abacVo, codeBlockDO, dynamicAttr)) {
                        ret.getRoleIds().addAll(abacVo.getRoleIds());
                        ret.getMenuIds().addAll(abacVo.getMenus());
                        ret.setCodeBlock(codeBlockDO.getCodeBlockCode());
                    }
                });
                retList.add(ret);
            }
        }
        return retList;
    }


    @Scheduled(fixedDelay = SCHEDULER_PERIOD, initialDelay = SCHEDULER_PERIOD)
    public void scheduleABACRefresh() {
        initLocalCache();
    }

    private boolean checkAttribute(AbacVo abacVo, AdminUserDO user, Set<Long> roleIds, Map<String, Object> dynamicAttr) {
        ServiceLineAbAcVoList voList = AbacConvert.INSTANCE.abacVoConvert(abacVo);
        List<ServiceLineAbAcVo> serviceLine = voList.getServiceLine();
        // 修正 serviceLine 为空导致的bug
        if (serviceLine == null) {
            serviceLine = new ArrayList<>();
        }

        // 未起效
        if (abacVo.getStatus() == 0) {
            return false;
        }

        if (CollectionUtil.isNotEmpty(abacVo.getMr()) && !abacVo.getMr().contains(user.getMgmtRegion())) {
            log.info("[ABAC] MR NOT MATCH:  username={} abac={} expect={} got={}.",
                    user.getUsername(), abacVo.getName(), abacVo.getMr(), user.getMgmtRegion());
            return false;
        }

        if (CollectionUtil.isNotEmpty(abacVo.getOu()) && !abacVo.getOu().contains(user.getOperatingUnit())) {
            log.info("[ABAC] OU NOT MATCH:  username={} abac={} expect={} got={}.",
                    user.getUsername(), abacVo.getName(), abacVo.getOu(), user.getOperatingUnit());
            return false;
        }

        if (CollectionUtil.isNotEmpty(abacVo.getBu()) && !abacVo.getBu().contains(user.getBuesinessUnit())) {
            log.info("[ABAC] BU NOT MATCH:  username={} abac={} expect={} got={}.",
                    user.getUsername(), abacVo.getName(), abacVo.getBu(), user.getBuesinessUnit());
            return false;
        }

        List<String> slList = serviceLine.stream().map(ServiceLineAbAcVo::getSlCode).filter(Objects::nonNull)
                .collect(Collectors.toList());
        if (CollectionUtil.isNotEmpty(slList) && StringUtils.isNotEmpty(user.getServiceLine())
                && !slList.contains(user.getServiceLine())) {
            log.info("[ABAC] SL NOT MATCH:  username={} abac={} expect={} got={}.",
                    user.getUsername(), abacVo.getName(), slList, user.getServiceLine());
            return false;
        }

        List<String> sslList = serviceLine.stream().map(ServiceLineAbAcVo::getSslCode).filter(Objects::nonNull)
                .collect(Collectors.toList());
        if (CollectionUtil.isNotEmpty(sslList) && StringUtils.isNotEmpty(user.getSubServiceLine())
                && !sslList.contains(user.getSubServiceLine())) {
            log.info("[ABAC] SSL NOT MATCH:  username={} abac={} expect={} got={}.",
                    user.getUsername(), abacVo.getName(), sslList, user.getSubServiceLine());
            return false;
        }

        List<String> compList = serviceLine.stream().map(ServiceLineAbAcVo::getCompetencyCode).filter(Objects::nonNull)
                .collect(Collectors.toList());
        if (CollectionUtil.isNotEmpty(compList) && StringUtils.isNotEmpty(user.getCompetency())
                && !compList.contains(user.getCompetency())) {
            log.info("[ABAC] Competency NOT MATCH:  username={} abac={} expect={} got={}.",
                    user.getUsername(), abacVo.getName(), compList, user.getCompetency());
            return false;
        }

        List<String> smuList = serviceLine.stream().map(ServiceLineAbAcVo::getSmuCode).filter(Objects::nonNull)
                .collect(Collectors.toList());
        if (CollectionUtil.isNotEmpty(smuList) && StringUtils.isNotEmpty(user.getSubMgmtUnit())
                && !smuList.contains(user.getSubMgmtUnit())) {
            log.info("[ABAC] SMU NOT MATCH:  username={} abac={} expect={} got={}.",
                    user.getUsername(), abacVo.getName(), smuList, user.getSubMgmtUnit());
            return false;
        }

        if (CollectionUtil.isNotEmpty(abacVo.getRanks()) && !abacVo.getRanks().contains(user.getRankCode())) {
            log.info("[ABAC] RANK NOT MATCH:  username={} abac={} expect={} got={}.",
                    user.getUsername(), abacVo.getName(), abacVo.getRanks(), user.getRankCode());
            return false;
        }


        if (CollectionUtil.isNotEmpty(abacVo.getRegions()) && !abacVo.getRegions().contains(user.getGeoRegion())) {
            log.info("[ABAC] REGION NOT MATCH:  username={} abac={} expect={} got={}.",
                    user.getUsername(), abacVo.getName(), abacVo.getRegions(), user.getGeoRegion());
            return false;
        }


        if (CollectionUtil.isNotEmpty(abacVo.getCheckRoles()) && Collections.disjoint(abacVo.getCheckRoles(), roleIds)) {
            log.info("[ABAC] ROLE NOT MATCH:  username={} abac={} expect={} got={}.",
                    user.getUsername(), abacVo.getName(), abacVo.getCheckRoles(), roleIds);
            return false;
        }

        if (StringUtils.isNoneBlank(abacVo.getAadGroup())) {
            LoginUser loginUser = SecurityFrameworkUtils.getLoginUser();
            if (loginUser != null) {
                Map<String, Object> ext = loginUser.getExt();
                if (ext != null) {
                    List<String> groups = (List<String>) ext.get("groups");
                    if (groups == null) {
                        log.info("[ABAC] AAD GROUP NOT MATCH:  NO AAD GROUPS. username={}", user.getUsername());
                        return false;
                    }

                    if (CollectionUtil.isNotEmpty(groups) && !groups.contains(abacVo.getAadGroup())) {
                        log.info("[ABAC] AAD Group NOT MATCH:  username={} abac={} expect={} got={}.",
                                user.getUsername(), abacVo.getName(), abacVo.getAadGroup(), groups);
                        return false;
                    }
                } else {
                    return false;
                }
            } else {
                return false;
            }
        }


        // check dynamic condition
        if (CollectionUtil.isNotEmpty(abacVo.getRule().getAll()) || CollectionUtil.isNotEmpty(abacVo.getRule().getAny())) {
            Rule rule = new DefaultRuleFactory().createRule(abacVo.getRule());
            if (dynamicAttr == null) {
                dynamicAttr = new HashMap<>();
            }

            dynamicAttr.putAll(getAdminUserFileAndValue(user, abacVo.getRule()));
            if (!rule.evaluate(dynamicAttr)) {
                log.info("[ABAC] DYNAMIC NOT MATCH: username={} abac={}", user.getUsername(), abacVo.getName());
                return false;
            }
        }

        log.info("[ABAC] CHECK SUCCESS: username={} abac={}", user.getUsername(), abacVo.getName());
        return true;
    }

    private boolean checkAttribute(AbacVo abacVo, CodeBlockDO codeBlockDO, Map<String, Object> dynamicAttr) {
        ServiceLineAbAcVoList voList = AbacConvert.INSTANCE.abacVoConvert(abacVo);
        List<ServiceLineAbAcVo> serviceLine = voList.getServiceLine();
        // 修正 serviceLine 为空导致的bug
        if (serviceLine == null) {
            serviceLine = new ArrayList<>();
        }

        // 未起效
        if (abacVo.getStatus() == 0) {
            return false;
        }

        if (CollectionUtil.isNotEmpty(abacVo.getMr()) && !abacVo.getMr().contains(codeBlockDO.getMrCode())) {
            log.info("[ABAC] MR NOT MATCH: codeBlock={} abac={} expect={} got={}.",
                    codeBlockDO.getCodeBlockCode(), abacVo.getName(), abacVo.getMr(), codeBlockDO.getMrCode());
            return false;
        }

        if (CollectionUtil.isNotEmpty(abacVo.getOu()) && !abacVo.getOu().contains(codeBlockDO.getOuCode())) {
            log.info("[ABAC] OU NOT MATCH: codeBlock={} abac={} expect={} got={}.",
                    codeBlockDO.getCodeBlockCode(), abacVo.getName(), abacVo.getOu(), codeBlockDO.getOuCode());
            return false;
        }

        if (CollectionUtil.isNotEmpty(abacVo.getBu()) && !abacVo.getBu().contains(codeBlockDO.getBuCode())) {
            log.info("[ABAC] BU NOT MATCH: codeBlock={} abac={} expect={} got={}.",
                    codeBlockDO.getCodeBlockCode(), abacVo.getName(), abacVo.getBu(), codeBlockDO.getBuCode());
            return false;
        }

        List<String> slList = serviceLine.stream().map(ServiceLineAbAcVo::getSlCode).filter(Objects::nonNull)
                .collect(Collectors.toList());
        if (CollectionUtil.isNotEmpty(slList) && StringUtils.isNotEmpty(codeBlockDO.getSlCode())
                && !slList.contains(codeBlockDO.getSlCode())) {
            log.info("[ABAC] SL NOT MATCH: codeBlock={} abac={} expect={} got={}.",
                    codeBlockDO.getCodeBlockCode(), abacVo.getName(), slList, codeBlockDO.getSlCode());
            return false;
        }

        List<String> sslList = serviceLine.stream().map(ServiceLineAbAcVo::getSslCode).filter(Objects::nonNull)
                .collect(Collectors.toList());
        if (CollectionUtil.isNotEmpty(sslList) && StringUtils.isNotEmpty(codeBlockDO.getSslCode())
                && !sslList.contains(codeBlockDO.getSslCode())) {
            log.info("[ABAC] SSL NOT MATCH: codeBlock={} abac={} expect={} got={}.",
                    codeBlockDO.getCodeBlockCode(), abacVo.getName(), sslList, codeBlockDO.getSslCode());
            return false;
        }

        List<String> compList = serviceLine.stream().map(ServiceLineAbAcVo::getCompetencyCode).filter(Objects::nonNull)
                .collect(Collectors.toList());
        if (CollectionUtil.isNotEmpty(compList) && StringUtils.isNotEmpty(codeBlockDO.getCompetencyCode())
                && !compList.contains(codeBlockDO.getCompetencyCode())) {
            log.info("[ABAC] Competency NOT MATCH:  codeBlock={} abac={} expect={} got={}.",
                    codeBlockDO.getCodeBlockCode(), abacVo.getName(), compList, codeBlockDO.getCompetencyCode());
            return false;
        }

        List<String> smuList = serviceLine.stream().map(ServiceLineAbAcVo::getSmuCode).filter(Objects::nonNull)
                .collect(Collectors.toList());
        if (CollectionUtil.isNotEmpty(smuList) && StringUtils.isNotEmpty(codeBlockDO.getSmuCode())
                && !smuList.contains(codeBlockDO.getSmuCode())) {
            log.info("[ABAC] SMU NOT MATCH:  codeBlock={} abac={} expect={} got={}.",
                    codeBlockDO.getCodeBlockCode(), abacVo.getName(), smuList, codeBlockDO.getSmuCode());
            return false;
        }

        if (CollectionUtil.isNotEmpty(abacVo.getRegions()) && !abacVo.getRegions().contains(codeBlockDO.getGeoRegionCode())) {
            log.info("[ABAC] REGION NOT MATCH:  codeBlock={} abac={} expect={} got={}.",
                    codeBlockDO.getCodeBlockCode(), abacVo.getName(), abacVo.getRegions(), codeBlockDO.getGeoRegionCode());
            return false;
        }

        log.info("[ABAC] CHECK SUCCESS: codeBlock={} abac={}", codeBlockDO.getCodeBlockCode(), abacVo.getName());


        // check dynamic condition
        if (dynamicAttr != null && abacVo.getRule() != null) {
            Rule rule = new DefaultRuleFactory().createRule(abacVo.getRule());
            return rule.evaluate(dynamicAttr);
        } else {
            return true;
        }
    }

    private Map<String, Object> getAdminUserFileAndValue(AdminUserDO user, RuleDefinition rule) {
        Map<String, Object> ret = new HashMap<>();
        rule.getAll().addAll(rule.getAny());

        rule.getAll().forEach(subRule -> {
            if (isFieldExist(subRule.getName())) {
                try {
                    Field f = AdminUserDO.class.getDeclaredField(subRule.getName());
                    f.setAccessible(true);
                    ret.put(subRule.getName(), f.get(user));
                    if (CollectionUtil.isNotEmpty(subRule.getAll()) || CollectionUtil.isNotEmpty(subRule.getAll())) {
                        ret.putAll(getAdminUserFileAndValue(user, subRule));
                    }
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        });

        return ret;
    }

    private boolean isFieldExist(String fieldName) {
        boolean exist = false;
        Field[] fields = AdminUserDO.class.getDeclaredFields();
        for (Field field : fields) {
            if (field.getName().equals(fieldName)) {
                exist = true;
                break;
            }
        }
        return exist;
    }

    private List<AbacDO> loadAbacIfUpdate(Date maxUpdateTime) {
        log.info("[loadAbacIfUpdate][执行更新]");
        // 第一步，判断是否要更新。
        if (maxUpdateTime == null) { // 如果更新时间为空，说明 DB 一定有新数据
            log.info("[loadAbacIfUpdate][首次加载全量功能点]");
        } else { // 判断数据库中是否有更新
            if (abacMapper.selectABACCountByUpdateTimeGt(maxUpdateTime) == 0) {
                log.info("[loadAbacIfUpdate][没有需要更新的数据] MaxUpdateTime:{}", maxUpdateTime);
                return null;
            }
            log.info("[loadAbacIfUpdate][增量加载全量功能点]");
        }
        // 第二步，如果有更新，则从数据库加载所有
        LambdaQueryWrapper<AbacDO> query = new LambdaQueryWrapper<>();
        query.eq(AbacDO::getState, 1);
        return abacMapper.selectList(query);
    }
}
