package cn.ey.biz.module.system.controller.admin.auth;

import cn.ey.biz.common.enums.CommonStatusEnum;
import cn.ey.biz.common.pojo.R;
import cn.ey.biz.common.util.collection.SetUtils;
import cn.ey.biz.common.util.i18n.I18nContextHolder;
import cn.ey.biz.framework.operatelog.core.annotations.OperateLog;
import cn.ey.biz.framework.security.config.SecurityProperties;
import cn.ey.biz.module.system.controller.admin.auth.vo.*;
import cn.ey.biz.module.system.controller.admin.user.vo.user.AdminUserVO;
import cn.ey.biz.module.system.convert.auth.AuthConvert;
import cn.ey.biz.module.system.dal.dataobject.app.AppDO;
import cn.ey.biz.module.system.dal.dataobject.permission.MenuDO;
import cn.ey.biz.module.system.dal.dataobject.permission.RoleDO;
import cn.ey.biz.module.system.dal.dataobject.user.AdminUserDO;
import cn.ey.biz.module.system.enums.category.CategoryAppEnum;
import cn.ey.biz.module.system.enums.logger.LoginLogTypeEnum;
import cn.ey.biz.module.system.enums.permission.MenuTypeEnum;
import cn.ey.biz.module.system.service.abac.AbacService;
import cn.ey.biz.module.system.service.app.AppService;
import cn.ey.biz.module.system.service.auth.AdminAuthService;
import cn.ey.biz.module.system.service.i18n.I18nService;
import cn.ey.biz.module.system.service.permission.PermissionService;
import cn.ey.biz.module.system.service.permission.RoleService;
import cn.ey.biz.module.system.service.social.SocialUserService;
import cn.ey.biz.module.system.service.user.AdminUserService;
import cn.hutool.core.util.StrUtil;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiImplicitParam;
import io.swagger.annotations.ApiImplicitParams;
import io.swagger.annotations.ApiOperation;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.StringUtils;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;

import javax.annotation.Resource;
import javax.annotation.security.PermitAll;
import javax.servlet.http.HttpServletRequest;
import javax.validation.Valid;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;

import static cn.ey.biz.common.pojo.R.success;
import static cn.ey.biz.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId;
import static cn.ey.biz.framework.security.core.util.SecurityFrameworkUtils.obtainAuthorization;
import static java.util.Collections.singleton;

@Api(tags = "管理后台 - 认证")
@RestController
@RequestMapping("/system/auth")
@Validated
@Slf4j
public class AuthController {

    @Resource
    private AdminAuthService authService;
    @Resource
    private AdminUserService userService;
    @Resource
    private RoleService roleService;
    @Resource
    private AppService appService;
    @Resource
    private PermissionService permissionService;
    @Resource
    private SocialUserService socialUserService;
    @Resource
    private SecurityProperties securityProperties;
    @Resource
    private I18nService i18nService;
    @Resource
    private AbacService abacService;

    @PostMapping("/login")
    @PermitAll
    @ApiOperation("使用账号密码登录")
    @OperateLog(enable = false) // 避免 Post 请求被记录操作日志
    public R<AuthLoginRespVO> login(@RequestBody @Valid AuthLoginReqVO reqVO) {
        return success(authService.login(reqVO));
    }

    @PostMapping("/logout")
    @PermitAll
    @ApiOperation("登出系统")
    @OperateLog(enable = false) // 避免 Post 请求被记录操作日志
    public R<Boolean> logout(HttpServletRequest request) {
        String token = obtainAuthorization(request, securityProperties.getTokenHeader());
        if (StrUtil.isNotBlank(token)) {
            authService.logout(token, LoginLogTypeEnum.LOGOUT_SELF.getType());
        }
        return success(true);
    }

    @PostMapping("/refresh-token")
    @PermitAll
    @ApiOperation("刷新令牌")
    @ApiImplicitParam(name = "refreshToken", value = "刷新令牌", required = true, dataTypeClass = String.class)
    @OperateLog(enable = false) // 避免 Post 请求被记录操作日志
    public R<AuthLoginRespVO> refreshToken(@RequestParam(value = "refreshToken",required = false) String refreshToken,
                                           @RequestBody(required = false) AuthController.RefreshToken body) {
        // 如果报文体传值则从报文体获取
        if (body != null && StringUtils.hasLength(body.getRefreshToken())) {
            refreshToken = body.getRefreshToken();
        }
        return success(authService.refreshToken(refreshToken));
    }

    @PostMapping("/get-permission-info/dynamic")
    @ApiOperation("获取登录用户的权限信息,包含动态属性判断")
    public R<AuthPermissionInfoRespVO> getPermissionInfo(@RequestParam(name = "app", required = false) String app,
                                                         @RequestBody Map<String, Object> dynamicAttr) {
        // 获得用户信息
        AdminUserDO user = userService.getUser(getLoginUserId());
        if (user == null) {
            return null;
        }
        // 获得角色列表
        Set<Long> roleIds = permissionService.getUserRoleIdsFromCache(getLoginUserId(), singleton(CommonStatusEnum.ENABLE.getStatus()));
        Set<Long> menuIds = new HashSet<>();

        // ABAC
        // 第一次调用 获取的新角色，作为第二次调用的 判断条件
        abacService.checkAbac(user, roleIds, menuIds, dynamicAttr, app);
        abacService.checkAbac(user, roleIds, menuIds, dynamicAttr, app);

        List<RoleDO> roleList = roleService.getRolesFromCache(roleIds);

        Set<String> apps;

        if (roleService.hasAnySuperAdmin(roleIds)) {
            // 超管只对后管系统起效
            apps = new HashSet<String>() {{
                add(CategoryAppEnum.ADMIN.getApp());
            }};
        } else {
            apps = roleList.stream().map(RoleDO::getApp).collect(Collectors.toSet());
        }

        // 获得菜单列表
        List<MenuDO> menuList = permissionService.getRoleMenuListFromCache(roleIds,
                SetUtils.asSet(MenuTypeEnum.DIR.getType(), MenuTypeEnum.MENU.getType(), MenuTypeEnum.BUTTON.getType()),
                singleton(CommonStatusEnum.ENABLE.getStatus()), menuIds); // 只要开启的
        // 拼接结果返回
        AdminUserVO adminUserVO = new AdminUserVO();
        adminUserVO.setId(user.getId());
        adminUserVO.setNickname(user.getNickname());
        adminUserVO.setAvatar(user.getAvatar());
        return success(AuthConvert.INSTANCE.convert(adminUserVO, roleList, menuList, apps));
    }

    @GetMapping("/list-menus/dynamic")
    @ApiOperation("获得登录用户的菜单列表,包含动态属性判断")
    public R<List<AuthMenuRespVO>> getMenus(@RequestParam(value = "app") String app, Map<String, Object> dynamicAttr) {
        AdminUserDO user = userService.getUser(getLoginUserId());

        // 获得角色列表
        Set<Long> roleIds = permissionService.getUserRoleIdsFromCache(user.getId(), singleton(CommonStatusEnum.ENABLE.getStatus()));

        // ABAC
        Set<Long> menuIds = new HashSet<>();
        // 第一次调用 获取的新角色，作为第二次调用的 判断条件
        abacService.checkAbac(user, roleIds, menuIds, dynamicAttr, app);
        abacService.checkAbac(user, roleIds, menuIds, dynamicAttr, app);

        List<AuthMenuRespVO> authMenuRespVOList = permissionService.getAuthMenuRespVOList(app, roleIds, menuIds);
        return success(authMenuRespVOList);
    }

    @GetMapping("/get-permission-info")
    @ApiOperation("获取登录用户的权限信息")
    public R<AuthPermissionInfoRespVO> getPermissionInfo(@RequestParam(name = "app", required = false) String app) {
        // 获得用户信息
        AdminUserDO user = userService.getUser(getLoginUserId());
        if (user == null) {
            return null;
        }
        // 获得角色列表
        Set<Long> roleIds = permissionService.getUserRoleIdsFromCache(getLoginUserId(), singleton(CommonStatusEnum.ENABLE.getStatus()));
        Set<Long> menuIds = new HashSet<>();

        // ABAC
        // 第一次调用 获取的新角色，作为第二次调用的 判断条件
        abacService.checkAbac(user, roleIds, menuIds, null, app);

        log.info("[ABAC] GOT ROLE ID: username={} ROLES={}.", user.getUsername(), roleIds);

        abacService.checkAbac(user, roleIds, menuIds, null, app);

        log.info("[ABAC] GOT ROLE ID: username={} ROLES={}.", user.getUsername(), roleIds);

        List<RoleDO> roleList = roleService.getRolesFromCache(roleIds);

        log.info("[ABAC] GOT ROLE: username={} ROLES={}.", user.getUsername(), roleList);

        Set<String> apps;
        // 判断超管拥有所有APP权限
        if (roleService.hasAnySuperAdmin(roleIds)) {
            apps = appService.getAllApp().stream().map(AppDO::getCode).collect(Collectors.toSet());
        } else {
            apps = roleList.stream().map(RoleDO::getApp).collect(Collectors.toSet());
        }

        // 获得菜单列表
        List<MenuDO> menuList = permissionService.getRoleMenuListFromCache(roleIds,
                SetUtils.asSet(MenuTypeEnum.DIR.getType(), MenuTypeEnum.MENU.getType(), MenuTypeEnum.BUTTON.getType()),
                singleton(CommonStatusEnum.ENABLE.getStatus()), menuIds); // 只要开启的

        // 拼接结果返回
        AdminUserVO adminUserVO = new AdminUserVO();
        adminUserVO.setId(user.getId());
        adminUserVO.setNickname(user.getDisplayName());
        adminUserVO.setGpn(user.getGpn());
        adminUserVO.setAvatar(user.getAvatar());
        return success(AuthConvert.INSTANCE.convert(adminUserVO, roleList, menuList, apps));
    }


    @GetMapping("/list-menus")
    @ApiOperation("获得登录用户的菜单列表")
    @ApiImplicitParam(name = "app", value = "应用", dataTypeClass = String.class)
    public R<List<AuthMenuRespVO>> getMenus(@RequestParam(value = "app") String app) {
        AdminUserDO user = userService.getUser(getLoginUserId());

        // 获得角色列表
        Set<Long> roleIds = permissionService.getUserRoleIdsFromCache(user.getId(), singleton(CommonStatusEnum.ENABLE.getStatus()));

        // ABAC
        Set<Long> menuIds = new HashSet<>();
        // 第一次调用 获取的新角色，作为第二次调用的 判断条件
        abacService.checkAbac(user, roleIds, menuIds, null, app);
        abacService.checkAbac(user, roleIds, menuIds, null, app);

        // 获得用户拥有的菜单列表
        List<MenuDO> menuList = permissionService.getRoleMenuListFromCache(roleIds,
                SetUtils.asSet(MenuTypeEnum.DIR.getType(), MenuTypeEnum.MENU.getType()), // 只要目录和菜单类型
                singleton(CommonStatusEnum.ENABLE.getStatus()), menuIds); // 只要开启的
        // 多语言转化
        menuList.forEach(item -> {
            item.setName(i18nService.getLabel(String.valueOf(item.getId()),
                    I18nContextHolder.getLangId(),
                    "menu"));
        });
        // 转换成 Tree 结构返回
        List<MenuDO> appList = menuList.stream().filter(d -> app.equals(d.getApp())).collect(Collectors.toList());
        return success(AuthConvert.INSTANCE.buildMenuTree(appList));
    }

    // ========== 短信登录相关 ==========

    @PostMapping("/sms-login")
    @PermitAll
    @ApiOperation("使用短信验证码登录")
    @OperateLog(enable = false) // 避免 Post 请求被记录操作日志
    public R<AuthLoginRespVO> smsLogin(@RequestBody @Valid AuthSmsLoginReqVO reqVO) {
        return success(authService.smsLogin(reqVO));
    }

    @PostMapping("/send-sms-code")
    @PermitAll
    @ApiOperation(value = "发送手机验证码")
    @OperateLog(enable = false) // 避免 Post 请求被记录操作日志
    public R<Boolean> sendLoginSmsCode(@RequestBody @Valid AuthSmsSendReqVO reqVO) {
        authService.sendSmsCode(reqVO);
        return success(true);
    }

    // ========== 社交登录相关 ==========

    @GetMapping("/social-auth-redirect")
    @PermitAll
    @ApiOperation("社交授权的跳转")
    @ApiImplicitParams({
            @ApiImplicitParam(name = "type", value = "社交类型", required = true, dataTypeClass = Integer.class),
            @ApiImplicitParam(name = "redirectUri", value = "回调路径", dataTypeClass = String.class)
    })
    public R<String> socialLogin(@RequestParam("type") Integer type,
                                 @RequestParam("redirectUri") String redirectUri) {
        return R.success(socialUserService.getAuthorizeUrl(type, redirectUri));
    }

    @PostMapping("/social-login")
    @PermitAll
    @ApiOperation(value = "社交快捷登录，使用 code 授权码", notes = "适合未登录的用户，但是社交账号已绑定用户")
    @OperateLog(enable = false) // 避免 Post 请求被记录操作日志
    public R<AuthLoginRespVO> socialQuickLogin(@RequestBody @Valid AuthSocialLoginReqVO reqVO) {
        return success(authService.socialLogin(reqVO));
    }

    @Data
    private static class RefreshToken {
        private String refreshToken;
    }

}
