package com.njcn.rdms.module.system.convert.auth;

import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.ObjUtil;
import cn.hutool.core.util.StrUtil;
import com.fasterxml.jackson.core.type.TypeReference;
import com.njcn.rdms.framework.common.util.json.JsonUtils;
import com.njcn.rdms.framework.common.util.object.BeanUtils;
import com.njcn.rdms.module.system.controller.admin.auth.vo.AuthRouteMetaRespVO;
import com.njcn.rdms.module.system.controller.admin.auth.vo.AuthRouteNodeRespVO;
import com.njcn.rdms.module.system.controller.admin.auth.vo.AuthPermissionInfoRespVO;
import com.njcn.rdms.module.system.controller.admin.auth.vo.AuthUserInfoRespVO;
import com.njcn.rdms.module.system.controller.admin.auth.vo.AuthUserRouteRespVO;
import com.njcn.rdms.module.system.dal.dataobject.permission.MenuDO;
import com.njcn.rdms.module.system.dal.dataobject.permission.RoleDO;
import com.njcn.rdms.module.system.dal.dataobject.user.AdminUserDO;
import com.njcn.rdms.module.system.enums.permission.MenuRouteKindEnum;
import com.njcn.rdms.module.system.enums.permission.MenuTypeEnum;
import org.mapstruct.Mapper;
import org.mapstruct.factory.Mappers;
import org.slf4j.LoggerFactory;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;

import static com.njcn.rdms.framework.common.exception.util.ServiceExceptionUtil.exception;
import static com.njcn.rdms.framework.common.util.collection.CollectionUtils.convertList;
import static com.njcn.rdms.framework.common.util.collection.CollectionUtils.convertSet;
import static com.njcn.rdms.framework.common.util.collection.CollectionUtils.filterList;
import static com.njcn.rdms.module.system.dal.dataobject.permission.MenuDO.ID_ROOT;
import static com.njcn.rdms.module.system.enums.ErrorCodeConstants.MENU_ROUTE_NAME_DUPLICATE;

@Mapper
public interface AuthConvert {

    AuthConvert INSTANCE = Mappers.getMapper(AuthConvert.class);

    default AuthPermissionInfoRespVO convert(AdminUserDO user, List<RoleDO> roleList, List<MenuDO> menuList) {
        return AuthPermissionInfoRespVO.builder()
                .user(BeanUtils.toBean(user, AuthPermissionInfoRespVO.UserVO.class))
                .roles(convertSet(roleList, RoleDO::getCode))
                .permissions(convertSet(filterList(menuList, menu -> StrUtil.isNotBlank(menu.getPermission())),
                        MenuDO::getPermission))
                .menus(buildMenuTree(menuList))
                .build();
    }

    default AuthUserInfoRespVO convertUserInfo(AdminUserDO user, List<RoleDO> roleList, List<MenuDO> menuList) {
        return AuthUserInfoRespVO.builder()
                .userId(String.valueOf(user.getId()))
                .userName(user.getUsername())
                .nickname(user.getNickname())
                .avatar(user.getAvatar())
                .company(user.getCompany())
                .roles(sortDistinctStrings(convertList(roleList, RoleDO::getCode)))
                .buttons(sortDistinctStrings(convertList(menuList, MenuDO::getPermission,
                        menu -> StrUtil.isNotBlank(menu.getPermission()))))
                .build();
    }

    default AuthUserRouteRespVO convertUserRoutes(List<MenuDO> menuList) {
        if (CollUtil.isEmpty(menuList)) {
            return AuthUserRouteRespVO.builder()
                    .routes(Collections.emptyList())
                    .home("")
                    .build();
        }

        List<MenuDO> routeMenus = filterList(menuList, this::canExposeAsRoute);
        if (CollUtil.isEmpty(routeMenus)) {
            return AuthUserRouteRespVO.builder()
                    .routes(Collections.emptyList())
                    .home("")
                    .build();
        }

        Map<Long, MenuDO> menuMap = new LinkedHashMap<>();
        routeMenus.forEach(menu -> menuMap.put(menu.getId(), menu));

        Map<Long, String> fullPathCache = new HashMap<>();
        List<MenuDO> validMenus = filterList(routeMenus, menu -> {
            String fullPath = resolveFullPath(menu, menuMap, fullPathCache);
            return StrUtil.isNotBlank(fullPath) && !isExternalPath(fullPath);
        });
        if (CollUtil.isEmpty(validMenus)) {
            return AuthUserRouteRespVO.builder()
                    .routes(Collections.emptyList())
                    .home("")
                    .build();
        }

        Set<Long> validMenuIds = convertSet(validMenus, MenuDO::getId);
        Set<Long> parentIds = new HashSet<>();
        validMenus.forEach(menu -> {
            if (validMenuIds.contains(menu.getParentId())) {
                parentIds.add(menu.getParentId());
            }
        });

        Map<Long, String> routeNameMap = buildRouteNameMap(validMenus, fullPathCache);
        Map<Long, AuthRouteNodeRespVO> nodeMap = new LinkedHashMap<>();
        validMenus.forEach(menu -> nodeMap.put(menu.getId(), buildRouteNode(menu,
                fullPathCache.get(menu.getId()), routeNameMap.get(menu.getId()), parentIds.contains(menu.getId()))));

        List<AuthRouteNodeRespVO> roots = new ArrayList<>();
        validMenus.forEach(menu -> {
            AuthRouteNodeRespVO node = nodeMap.get(menu.getId());
            if (!validMenuIds.contains(menu.getParentId()) || ID_ROOT.equals(menu.getParentId())) {
                roots.add(node);
                return;
            }

            AuthRouteNodeRespVO parentNode = nodeMap.get(menu.getParentId());
            if (parentNode == null) {
                roots.add(node);
                return;
            }
            if (parentNode.getChildren() == null) {
                parentNode.setChildren(new ArrayList<>());
            }
            parentNode.getChildren().add(node);
        });

        List<AuthRouteNodeRespVO> routes = sortRouteNodes(roots);
        return AuthUserRouteRespVO.builder()
                .routes(routes)
                .home(StrUtil.blankToDefault(resolveHome(routes), ""))
                .build();
    }

    /**
     * 将菜单列表，构建成菜单树。
     */
    default List<AuthPermissionInfoRespVO.MenuVO> buildMenuTree(List<MenuDO> menuList) {
        if (CollUtil.isEmpty(menuList)) {
            return Collections.emptyList();
        }
        menuList.removeIf(menu -> menu.getType().equals(MenuTypeEnum.BUTTON.getType()));
        menuList.sort(Comparator.comparing(MenuDO::getSort));

        Map<Long, AuthPermissionInfoRespVO.MenuVO> treeNodeMap = new LinkedHashMap<>();
        menuList.forEach(menu -> treeNodeMap.put(menu.getId(),
                BeanUtils.toBean(menu, AuthPermissionInfoRespVO.MenuVO.class)));

        treeNodeMap.values().stream().filter(node -> ObjUtil.notEqual(node.getParentId(), ID_ROOT)).forEach(childNode -> {
            AuthPermissionInfoRespVO.MenuVO parentNode = treeNodeMap.get(childNode.getParentId());
            if (parentNode == null) {
                LoggerFactory.getLogger(getClass()).error("[buildRouterTree][resource({}) 找不到父资源({})]",
                        childNode.getId(), childNode.getParentId());
                return;
            }
            if (parentNode.getChildren() == null) {
                parentNode.setChildren(new ArrayList<>());
            }
            parentNode.getChildren().add(childNode);
        });

        return filterList(treeNodeMap.values(), node -> ID_ROOT.equals(node.getParentId()));
    }

    default List<String> sortDistinctStrings(List<String> values) {
        if (CollUtil.isEmpty(values)) {
            return Collections.emptyList();
        }
        return values.stream()
                .filter(StrUtil::isNotBlank)
                .distinct()
                .sorted()
                .collect(Collectors.toList());
    }

    default boolean canExposeAsRoute(MenuDO menu) {
        return menu != null
                && !MenuTypeEnum.BUTTON.getType().equals(menu.getType())
                && (MenuTypeEnum.DIR.getType().equals(menu.getType()) || !Boolean.FALSE.equals(menu.getVisible()));
    }

    default String resolveFullPath(MenuDO menu, Map<Long, MenuDO> menuMap, Map<Long, String> cache) {
        String cachedPath = cache.get(menu.getId());
        if (cachedPath != null) {
            return cachedPath;
        }

        String rawPath = StrUtil.trimToEmpty(menu.getPath());
        String fullPath;
        if (StrUtil.isBlank(rawPath)) {
            fullPath = "";
        } else if (isExternalPath(rawPath)) {
            fullPath = rawPath;
        } else if (StrUtil.startWith(rawPath, "/")) {
            fullPath = normalizePath(rawPath);
        } else {
            MenuDO parent = menuMap.get(menu.getParentId());
            if (parent == null || ID_ROOT.equals(menu.getParentId())) {
                fullPath = normalizePath("/" + rawPath);
            } else {
                String parentPath = resolveFullPath(parent, menuMap, cache);
                fullPath = normalizePath(parentPath + "/" + rawPath);
            }
        }

        cache.put(menu.getId(), fullPath);
        return fullPath;
    }

    default Map<Long, String> buildRouteNameMap(List<MenuDO> menus, Map<Long, String> fullPathCache) {
        Map<Long, String> routeNameMap = new LinkedHashMap<>();
        Map<String, Long> usedNames = new LinkedHashMap<>();
        menus.forEach(menu -> {
            String fullPath = fullPathCache.get(menu.getId());
            String baseName = resolveBaseRouteName(menu, fullPath);
            if (StrUtil.isBlank(baseName)) {
                baseName = "route_" + menu.getId();
            }
            Long conflictMenuId = usedNames.putIfAbsent(baseName, menu.getId());
            if (conflictMenuId != null) {
                throw exception(MENU_ROUTE_NAME_DUPLICATE,
                        StrUtil.format("{}（菜单 {} 与菜单 {}）", baseName, conflictMenuId, menu.getId()));
            }
            routeNameMap.put(menu.getId(), baseName);
        });
        return routeNameMap;
    }

    default String resolveBaseRouteName(MenuDO menu, String fullPath) {
        if (StrUtil.isNotBlank(menu.getComponentName())) {
            return StrUtil.trim(menu.getComponentName());
        }
        return normalizeRouteName(fullPath);
    }

    default AuthRouteNodeRespVO buildRouteNode(MenuDO menu, String fullPath, String routeName, boolean hasChildren) {
        MenuRouteKindEnum routeKind = resolveRouteKind(menu, hasChildren);
        return AuthRouteNodeRespVO.builder()
                .id(String.valueOf(menu.getId()))
                .name(routeName)
                .path(fullPath)
                .component(resolveComponentKey(menu, routeKind, routeName))
                .props(resolveRouteProps(menu.getRoutePropsJson()))
                .meta(buildRouteMeta(menu))
                .build();
    }

    default AuthRouteMetaRespVO buildRouteMeta(MenuDO menu) {
        return AuthRouteMetaRespVO.builder()
                .title(menu.getName())
                .icon(resolveIcon(menu.getIcon()))
                .order(menu.getSort())
                .keepAlive(menu.getKeepAlive())
                .hideInMenu(Boolean.FALSE.equals(menu.getVisible()))
                .build();
    }

    default MenuRouteKindEnum resolveRouteKind(MenuDO menu, boolean hasChildren) {
        if (hasChildren) {
            return MenuRouteKindEnum.DIR;
        }
        MenuRouteKindEnum routeKind = MenuRouteKindEnum.valueOfKind(menu.getRouteKind());
        if (routeKind != null) {
            return routeKind;
        }
        if (MenuTypeEnum.DIR.getType().equals(menu.getType())) {
            return MenuRouteKindEnum.DIR;
        }
        if (isExternalPath(menu.getPath())) {
            return MenuRouteKindEnum.EXTERNAL;
        }
        if (ID_ROOT.equals(menu.getParentId())) {
            return MenuRouteKindEnum.SINGLE;
        }
        return MenuRouteKindEnum.VIEW;
    }

    default String resolveComponentKey(MenuDO menu, MenuRouteKindEnum routeKind, String routeName) {
        if (StrUtil.isNotBlank(menu.getComponent())) {
            return StrUtil.trim(menu.getComponent());
        }
        if (MenuRouteKindEnum.DIR.equals(routeKind)) {
            return "layout.base";
        }
        if (MenuRouteKindEnum.IFRAME.equals(routeKind)) {
            return "view.iframe-page";
        }
        if (MenuRouteKindEnum.SINGLE.equals(routeKind)) {
            return "layout.base$view." + routeName;
        }
        if (MenuRouteKindEnum.VIEW.equals(routeKind)) {
            return "view." + routeName;
        }
        return null;
    }

    default Object resolveRouteProps(String routePropsJson) {
        if (StrUtil.isBlank(routePropsJson)) {
            return null;
        }
        Object routeProps = JsonUtils.parseObjectQuietly(routePropsJson, new TypeReference<Object>() {
        });
        if (routeProps != null) {
            return routeProps;
        }
        LoggerFactory.getLogger(getClass()).warn("[resolveRouteProps][routePropsJson({}) 解析失败]", routePropsJson);
        return null;
    }

    default String resolveIcon(String icon) {
        if (StrUtil.isBlank(icon) || "#".equals(icon)) {
            return null;
        }
        return icon;
    }

    default String normalizePath(String path) {
        String normalized = path.replace('\\', '/');
        normalized = normalized.replaceAll("/{2,}", "/");
        if (!normalized.startsWith("/")) {
            normalized = "/" + normalized;
        }
        if (normalized.length() > 1 && normalized.endsWith("/")) {
            normalized = normalized.substring(0, normalized.length() - 1);
        }
        return normalized;
    }

    default boolean isExternalPath(String path) {
        return StrUtil.startWithAnyIgnoreCase(path, "http://", "https://");
    }

    default String normalizeRouteName(String fullPath) {
        if (StrUtil.isBlank(fullPath)) {
            return "";
        }
        return fullPath
                .replaceAll("([A-Z]+)([A-Z][a-z])", "$1_$2")
                .replaceAll("([a-z\\d])([A-Z])", "$1_$2")
                .replaceAll("^/+", "")
                .replaceAll("/+$", "")
                .replaceAll("[/\\\\.\\-\\s]+", "_")
                .replaceAll("_+", "_")
                .replaceAll("^_+", "")
                .replaceAll("_+$", "")
                .toLowerCase();
    }

    default List<AuthRouteNodeRespVO> sortRouteNodes(List<AuthRouteNodeRespVO> routes) {
        if (CollUtil.isEmpty(routes)) {
            return Collections.emptyList();
        }

        List<AuthRouteNodeRespVO> sortedRoutes = new ArrayList<>(routes);
        sortedRoutes.sort(Comparator
                .comparing((AuthRouteNodeRespVO route) -> route.getMeta() != null && route.getMeta().getOrder() != null
                        ? route.getMeta().getOrder() : Integer.MAX_VALUE)
                .thenComparing(AuthRouteNodeRespVO::getPath, Comparator.nullsLast(String::compareTo))
                .thenComparing(AuthRouteNodeRespVO::getId, Comparator.nullsLast(String::compareTo)));

        sortedRoutes.forEach(route -> {
            if (CollUtil.isEmpty(route.getChildren())) {
                route.setChildren(null);
                return;
            }

            List<AuthRouteNodeRespVO> children = sortRouteNodes(route.getChildren());
            route.setChildren(children);
            route.setRedirect(children.get(0).getPath());
        });
        return sortedRoutes;
    }

    default String resolveHome(List<AuthRouteNodeRespVO> routes) {
        if (CollUtil.isEmpty(routes)) {
            return null;
        }

        for (AuthRouteNodeRespVO route : routes) {
            if (CollUtil.isNotEmpty(route.getChildren())) {
                String childHome = resolveHome(route.getChildren());
                if (StrUtil.isNotBlank(childHome)) {
                    return childHome;
                }
            }

            if (CollUtil.isEmpty(route.getChildren())) {
                return route.getName();
            }
        }
        return null;
    }

}
