package cn.lihongjie.coal.user.service;

import cn.lihongjie.coal.base.dto.BaseDto;
import cn.lihongjie.coal.base.dto.CommonQuery;
import cn.lihongjie.coal.base.dto.IdRequest;
import cn.lihongjie.coal.base.entity.BaseEntity;
import cn.lihongjie.coal.base.service.BaseService;
import cn.lihongjie.coal.common.Constants;
import cn.lihongjie.coal.common.Ctx;
import cn.lihongjie.coal.exception.BizException;
import cn.lihongjie.coal.organization.entity.OrganizationEntity;
import cn.lihongjie.coal.organization.service.OrganizationService;
import cn.lihongjie.coal.passwordDict.service.PasswordDictService;
import cn.lihongjie.coal.permission.dto.PermissionDto;
import cn.lihongjie.coal.permission.dto.PermissionSimpleDto;
import cn.lihongjie.coal.permission.entity.PermissionEntity;
import cn.lihongjie.coal.permission.mapper.PermissionMapper;
import cn.lihongjie.coal.permission.service.PermissionService;
import cn.lihongjie.coal.resource.dto.ResourceDto;
import cn.lihongjie.coal.resource.mapper.ResourceMapper;
import cn.lihongjie.coal.resource.service.ResourceService;
import cn.lihongjie.coal.role.entity.RoleEntity;
import cn.lihongjie.coal.role.service.RoleService;
import cn.lihongjie.coal.sysconfig.service.SysConfigService;
import cn.lihongjie.coal.user.dto.*;
import cn.lihongjie.coal.user.entity.UserEntity;
import cn.lihongjie.coal.user.mapper.UserMapper;
import cn.lihongjie.coal.user.repository.UserRepository;

import com.nulabinc.zxcvbn.Strength;
import com.nulabinc.zxcvbn.Zxcvbn;

import io.vavr.collection.Stream;
import io.vavr.control.Try;

import jakarta.annotation.PostConstruct;
import jakarta.persistence.EntityManager;
import jakarta.persistence.PersistenceContext;
import jakarta.persistence.criteria.CriteriaBuilder;
import jakarta.persistence.criteria.CriteriaQuery;
import jakarta.persistence.criteria.Predicate;
import jakarta.persistence.criteria.Root;

import lombok.extern.slf4j.Slf4j;

import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.BooleanUtils;
import org.apache.commons.lang3.ObjectUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.core.convert.ConversionService;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Sort;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.security.crypto.password.Pbkdf2PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StopWatch;

import java.util.*;
import java.util.stream.Collectors;

@Service
@Slf4j
@Transactional
public class UserService extends BaseService<UserEntity, UserRepository> {

    @Autowired UserRepository repository;

    @Autowired UserMapper mapper;
    @Autowired ConversionService conversionService;

    @Autowired RoleService roleService;

    @Autowired SysConfigService sysConfigService;

    @Autowired PasswordDictService passwordDictService;
    private Pbkdf2PasswordEncoder passwordEncoder;

    @Autowired UserService that;

    @PostConstruct
    public void init() {

        passwordEncoder = new Pbkdf2PasswordEncoder("", 8, 10000, 256);
    }

    @Autowired OrganizationService organizationService;

    public UserDto create(CreateUserDto request) {

        String organizationId = Ctx.currentUser().getOrganizationId();

        OrganizationEntity organization = organizationService.get(organizationId);

        if (organization.getMaxUser() != null
                && organization.getMaxUser() > 0
                && organization.getMaxUser() <= repository.countByOrganizationId(organizationId)) {
            throw new BizException("用户数量已经达到上限 " + organization.getMaxUser() + " 个");
        }

        StopWatch stopWatch = new StopWatch();

        checkDuplicateUserName(request.getUsername());
        try {
            stopWatch.start("encode");
            String password = request.getPassword();

            checkPassword(password);

            request.setPassword(passwordEncoder.encode(password));
            stopWatch.stop();
            UserEntity entity = mapper.toEntity(request);

            stopWatch.start("save");
            this.repository.save(entity);
            stopWatch.stop();

            stopWatch.start("getById");
            that.userResourceIds(entity.getId());

            return getById(entity.getId());
        } finally {
            stopWatch.stop();
            log.info(stopWatch.prettyPrint());
        }
    }

    private void checkPassword(String password) {
        if (sysConfigService.isEnable(Constants.SYSCONFIG_PASSWORD_DICT_DETECT)) {
            if (passwordDictService.isInDict(password)) {
                throw new BizException("当前密码为常见密码，请重新设置");
            }
        }

        Integer strength = passwordStrength(password);
        int strength_min =
                Integer.parseInt(
                        sysConfigService.getConfigVal(Constants.SYSCONFIG_PASSWORD_STRENGTH_MIN));
        log.info("密码强度校验，当前密码强度为 {}，最低要求为 {}", strength, strength_min);
        if (strength_min > strength) {
            throw new BizException("密码强度过低，请重新设置");
        }
    }

    public Integer passwordStrength(String password) {

        Zxcvbn zxcvbn = new Zxcvbn();
        Strength strength = zxcvbn.measure(password);
        return strength.getScore();
    }

    private void checkDuplicateUserName(String username) {

        if (this.repository.countByUsername(username) > 0) {
            throw new BizException("用户名已经存在");
        }
    }

    public UserDto createOrgAdmin(CreateOrgAdminDto request) {

        StopWatch stopWatch = new StopWatch();
        checkDuplicateUserName(request.getUsername());
        checkPassword(request.getPassword());
        try {
            stopWatch.start("encode");
            request.setPassword(passwordEncoder.encode(request.getPassword()));
            stopWatch.stop();
            UserEntity entity = mapper.toEntity(request);
            entity.setOrgAdmin(true);
            entity.setSysAdmin(false);

            stopWatch.start("save");
            this.repository.save(entity);
            stopWatch.stop();

            stopWatch.start("getById");
            that.userResourceIds(entity.getId());
            return getById(entity.getId());
        } finally {
            stopWatch.stop();
            log.info(stopWatch.prettyPrint());
        }
    }

    @CacheEvict(cacheNames = Constants.CACHE_USER_RESOURCES, key = "#request.id")
    public UserDto update(UpdateUserDto request) {
        UserEntity user = this.repository.get(request.getId());
        this.mapper.updateEntity(user, request);
        this.repository.save(user);
        String organizationId = user.getOrganizationId();

        assertAdmin(organizationId);

        clearUserCache(request.getId());
        return getById(user.getId());
    }

    private void assertAdmin(String organizationId) {
        long adminCnt =
                this.repository.count(
                        new Specification<UserEntity>() {
                            @Override
                            public Predicate toPredicate(
                                    Root<UserEntity> root,
                                    CriteriaQuery<?> query,
                                    CriteriaBuilder criteriaBuilder) {
                                return criteriaBuilder.and(
                                        criteriaBuilder.equal(
                                                root.get("organizationId"), organizationId),
                                        criteriaBuilder.equal(root.get("orgAdmin"), true));
                            }
                        });

        if (adminCnt == 0) {
            throw new BizException("组织必须有一个管理员");
        }
    }

    public void delete(IdRequest request) {

        this.repository.deleteAllById(request.getIds());
        request.getIds().forEach(this::clearUserCache);

        this.assertAdmin(Ctx.currentUser().getOrganizationId());
    }

    public UserDto getById(String id) {

        UserEntity user = repository.get(id);

        List<RoleEntity> defaultRoles = roleService.getSysDefaultRoles();
        user.setOtherRoles(defaultRoles);
        if (BooleanUtils.isTrue(user.getOrgAdmin())) {
            List<RoleEntity> orgAdminRoles = roleService.getOrgAdminRoles();

            user.getOtherRoles().addAll(orgAdminRoles);
        }

        UserDto dto = mapper.toDto(user);
        if (Ctx.isLoggedIn()) {
            dto.setSessionId(Ctx.getSessionId());
        }

        return dto;
    }

    public UserDto changePwd(ChangeUserPwdDto request) {

        UserEntity user =
                repository.findById(request.getId()).orElseThrow(() -> new BizException("用户不存在"));

        if (!StringUtils.equals(request.getNewPassword(), request.getNewPassword2())) {
            throw new BizException("两次输入的密码不一致");
        }

        checkPassword(request.getNewPassword());

        user.setPassword(passwordEncoder.encode(request.getNewPassword()));

        repository.save(user);

        return getById(request.getId());
    }

    public void resetPwd(String userId, String password) {

        UserEntity user = repository.findById(userId).orElseThrow(() -> new BizException("用户不存在"));
        checkPassword(password);
        user.setPassword(passwordEncoder.encode(password));

        repository.save(user);
    }

    public Page<UserDto> list(CommonQuery query) {

        Page<UserEntity> page =
                repository.findAll(
                        query.specification(conversionService),
                        PageRequest.of(
                                query.getPageNo(),
                                query.getPageSize(),
                                Sort.by(query.getOrders())));

        return page.map(this.mapper::toDto);
    }

    public Optional<UserEntity> findByUsername(String username) {

        return this.findAll(
                        new Specification<UserEntity>() {
                            @Override
                            public Predicate toPredicate(
                                    Root<UserEntity> root,
                                    CriteriaQuery<?> query,
                                    CriteriaBuilder criteriaBuilder) {
                                return criteriaBuilder.equal(root.get("username"), username);
                            }
                        })
                .stream()
                .findFirst();
    }

    public boolean isValidPassword(String rawPassword, String encodedPassword) {
        return passwordEncoder.matches(rawPassword, encodedPassword);
    }

    public UserEntity initOrGetAdminUser(OrganizationEntity e) {

        UserEntity adminuser = this.repository.findByUsername("adminuser");

        if (adminuser == null) {

            adminuser = new UserEntity();

            adminuser.setUsername("adminuser");

            adminuser.setPassword(passwordEncoder.encode("adminuser@123"));

            adminuser.setOrgAdmin(true);
            adminuser.setName("超级管理员");

            adminuser.setSysAdmin(true);

            adminuser.setOrganizationId(e.getId());

            this.repository.save(adminuser);
        }

        return adminuser;
    }

    @Autowired ResourceMapper resourceMapper;

    @Autowired ResourceService resourceService;

    @Autowired PermissionService permissionService;

    @Autowired CacheManager cacheManager;

    @Cacheable(cacheNames = Constants.CACHE_USER_RESOURCES, key = "#id")
    public List<ResourceDto> resources(String id) {

        UserEntity user = get(id);

        if (BooleanUtils.isTrue(user.getSysAdmin())) {

            return resourceService.allResources();
        }

        if (BooleanUtils.isTrue(user.getOrgAdmin())) {

            var defaultPermissionIds =
                    organizationService.getDefaultPermissionIds(user.getOrganizationId());

            List<PermissionDto> allPermissions = permissionService.getAllFromCache();

            return Stream.ofAll(allPermissions)
                    .filter(
                            x ->
                                    defaultPermissionIds.isEmpty()
                                            ? StringUtils.equalsAny(
                                                    x.getPermissionType(), "0", "1", "2")
                                            : (StringUtils.equalsAny(
                                                            x.getPermissionType(), "0", "1")
                                                    || defaultPermissionIds.contains(x.getId())))
                    .flatMap(PermissionDto::getResources)
                    .distinctBy(BaseDto::getId)
                    .toJavaList();
        }

        OrganizationEntity organization = organizationService.get(user.getOrganizationId());

        return Stream.ofAll(user.allRoles())
                .flatMap(x -> x.getPermissions() == null ? Stream.empty() : x.getPermissions())
                .flatMap(x -> x.getResources() == null ? Stream.empty() : x.getResources())
                .appendAll(
                        Stream.ofAll(permissionService.getByTypes(new String[] {"0", "1"}))
                                .flatMap(PermissionEntity::getResources))
                .appendAll(
                        Stream.ofAll(
                                        BooleanUtils.isTrue(user.getOrgAdmin())
                                                ? (CollectionUtils.isEmpty(
                                                                organization.getPermissions())
                                                        ? permissionService.getByTypes(
                                                                new String[] {"0", "1", "2"})
                                                        : organization.getPermissions())
                                                : Collections.emptyList())
                                .flatMap(PermissionEntity::getResources))
                .distinctBy(BaseEntity::getId)
                .map(x -> resourceMapper.toDto(x))
                .collect(Collectors.toList());
    }

    @Autowired PermissionMapper permissionMapper;

    @Cacheable(cacheNames = Constants.CACHE_USER_PERMISSIONS, key = "#id")
    public List<PermissionSimpleDto> permissions(String id) {

        UserEntity user = get(id);

        if (user.getSysAdmin() != null && user.getSysAdmin()) {
            return permissionService.findAll().stream()
                    .map(x -> permissionMapper.toSimpleDto(x))
                    .collect(Collectors.toList());
        }

        OrganizationEntity organization = organizationService.get(user.getOrganizationId());
        return Stream.ofAll(user.allRoles())
                .flatMap(x -> x.getPermissions() == null ? Stream.empty() : x.getPermissions())
                .appendAll(permissionService.getByTypes(new String[] {"0", "1"}))
                .appendAll(
                        Stream.ofAll(
                                BooleanUtils.isTrue(user.getOrgAdmin())
                                        ? ObjectUtils.defaultIfNull(
                                                organization.getPermissions(), new ArrayList<>())
                                        : Collections.emptyList()))
                .distinctBy(BaseEntity::getId)
                .map(x -> permissionMapper.toSimpleDto(x))
                .collect(Collectors.toList());
    }

    public void clearUserCache(String id) {
        if (StringUtils.isEmpty(id)) return;

        Try.run(() -> cacheManager.getCache(Constants.CACHE_USER_BY_ID).evict(id));
        Try.run(() -> cacheManager.getCache(Constants.CACHE_USER_RESOURCES).evict(id));
        Try.run(() -> cacheManager.getCache(Constants.CACHE_USER_PERMISSIONS).evict(id));
        Try.run(() -> cacheManager.getCache(Constants.CACHE_USER_RESOURCE_IDS).evict(id));
    }

    public UserEntity findUniqUserByPhone(final String phone) {
        List<UserEntity> users =
                findAll(
                        new Specification<UserEntity>() {
                            @Override
                            public Predicate toPredicate(
                                    Root<UserEntity> root,
                                    CriteriaQuery<?> query,
                                    CriteriaBuilder criteriaBuilder) {
                                return criteriaBuilder.equal(root.get("phone"), phone);
                            }
                        });
        if (CollectionUtils.isEmpty(users)) {
            throw new BizException("手机号不存在");
        }

        if (users.size() > 1) {
            throw new BizException("手机号重复");
        }

        UserEntity user = users.get(0);
        return user;
    }

    public Map<String, String> getIdNameMap(Set<String> strings) {

        strings = Stream.ofAll(strings).filter(StringUtils::isNotBlank).toJavaSet();
        if (strings.isEmpty()) return Map.of();
        List<String> names = new ArrayList<>();

        try {

            names = this.repository.findNamesByIds(strings);

            Map<String, String> javaMap =
                    Stream.ofAll(names)
                            .filter(StringUtils::isNotBlank)
                            .toMap(x -> x.split(",")[0], x -> x.split(",")[1])
                            .toJavaMap();

            strings.forEach(x -> javaMap.putIfAbsent(x, null));

            return javaMap;
        } catch (Exception e) {
            log.error("获取用户名称失败 {}", StringUtils.join(names, " | "), e);
            return Map.of();
        }
    }

    @PersistenceContext EntityManager em;

    public void resetOrgAdminPassword(String organizationId, String password) {

        em.createQuery(
                        "from UserEntity where organizationId = :organizationId and orgAdmin = true",
                        UserEntity.class)
                .setParameter("organizationId", organizationId)
                .getResultList()
                .forEach(
                        x -> {
                            x.setPassword(passwordEncoder.encode(password));
                            repository.save(x);
                        });
    }

    @Cacheable(cacheNames = Constants.CACHE_USER_BY_ID, key = "#userId")
    public UserDto getFromCache(String userId) {

        return getById(userId);
    }

    @Cacheable(cacheNames = Constants.CACHE_USER_RESOURCE_IDS, key = "#userId")
    public Set<String> userResourceIds(String userId) {
        UserDto user = getFromCache(userId);

        List<PermissionDto> allPermissions = permissionService.getAllFromCache();
        if (BooleanUtils.isTrue(user.getOrgAdmin())) {

            var defaultPermissionIds =
                    organizationService.getDefaultPermissionIds(user.getOrganizationId());

            return Stream.ofAll(allPermissions)
                    .filter(
                            x ->
                                    defaultPermissionIds.isEmpty()
                                            ? StringUtils.equalsAny(
                                                    x.getPermissionType(), "0", "1", "2")
                                            : (StringUtils.equalsAny(
                                                            x.getPermissionType(), "0", "1")
                                                    || defaultPermissionIds.contains(x.getId())))
                    .flatMap(PermissionDto::getResources)
                    .distinctBy(BaseDto::getId)
                    .map(BaseDto::getId)
                    .toJavaSet();
        } else {

            Set<String> ids =
                    em
                            .createQuery(
                                    "select rr.id from UserEntity  u join u.roles r join r.permissions p join p.resources rr where u.id = :userId",
                                    String.class)
                            .setParameter("userId", userId)
                            .getResultList()
                            .stream()
                            .map(x -> x)
                            .collect(Collectors.toSet());

            ids.addAll(
                    allPermissions.stream()
                            .filter(x -> StringUtils.equalsAny(x.getPermissionType(), "0", "1"))
                            .flatMap(
                                    x ->
                                            x.getResources() == null
                                                    ? java.util.stream.Stream.empty()
                                                    : x.getResources().stream())
                            .map(BaseDto::getId)
                            .collect(Collectors.toSet()));
            return ids;
        }
    }

    public boolean hasResource(UserDto user, String resourceId) {

        if (BooleanUtils.isTrue(user.getSysAdmin())) {
            return true;
        }
        StopWatch stopWatch = new StopWatch();

        stopWatch.start("userResourceIds");
        Set<String> ids = that.userResourceIds(user.getId());
        stopWatch.stop();
        stopWatch.start("contains");
        boolean contains = ids.contains(resourceId);
        stopWatch.stop();
        //        log.info(stopWatch.prettyPrint());
        return contains;
    }

    public void clearUserPermissionCache() {
        Try.run(() -> cacheManager.getCache(Constants.CACHE_USER_RESOURCES).clear());
        Try.run(() -> cacheManager.getCache(Constants.CACHE_USER_PERMISSIONS).clear());
        Try.run(() -> cacheManager.getCache(Constants.CACHE_USER_RESOURCE_IDS).clear());
    }
}
