
package com.tmerclub.cloud.auth.controller;

import cn.binarywang.wx.miniapp.bean.WxMaJscode2SessionResult;
import cn.hutool.core.util.IdUtil;

import com.alibaba.fastjson.JSONObject;
import com.tmerclub.cloud.api.auth.bo.LoginInfoBO;
import com.tmerclub.cloud.api.auth.bo.UidInfoBO;
import com.tmerclub.cloud.api.auth.constant.SocialType;
import com.tmerclub.cloud.api.auth.vo.TokenInfoVO;
import com.tmerclub.cloud.api.biz.feign.NotifyFeignClient;
import com.tmerclub.cloud.api.rbac.dto.ClearUserPermissionsCacheDTO;
import com.tmerclub.cloud.auth.config.WxConfig;
import com.tmerclub.cloud.auth.manager.TokenStore;
import com.tmerclub.cloud.auth.model.AuthAccount;
import com.tmerclub.cloud.auth.model.AuthSocial;
import com.tmerclub.cloud.auth.service.AuthAccountService;
import com.tmerclub.cloud.auth.service.AuthSocialService;
import com.tmerclub.cloud.auth.vo.TokenWithTempUidVO;
import com.tmerclub.cloud.common.cache.constant.OauthCacheNames;
import com.tmerclub.cloud.common.cache.util.RedisUtil;
import com.tmerclub.cloud.common.response.ServerResponseEntity;
import com.tmerclub.cloud.common.util.BeanUtil;
import com.tmerclub.cloud.common.util.Json;
import com.tmerclub.cloud.rbac.service.MenuPermissionService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import me.chanjar.weixin.common.bean.WxOAuth2UserInfo;
import me.chanjar.weixin.common.bean.oauth2.WxOAuth2AccessToken;
import me.chanjar.weixin.common.error.WxErrorException;
import org.apache.dubbo.config.annotation.DubboReference;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.Objects;


/**
 * 尝试使用社交账户登录
 * 如果登录成功，返回token信息
 * 如果登录失败，返回一个临时的uid，同时把openid之类的信息保存到数据库
 * 前提：
 * 1. 如果在小程序注册，那么会直接绑定，下次打开不需要登录按钮进行登录（换了手机也会，只要是同一个微信号）
 * 2. 如果用户直接输入账号密码登录，会直接跟这个用户进行绑定，会有退出登录按钮
 * 3. 如果退出登录的话，就要解除绑定，不然没办法退出登录
 * 4. 如果用户登录过一次，其实就已经绑定了，下次进入也是直接登录，不需要输入账号密码
 * 5. 如果用户在别的微信上登录过（也就是绑定过），需要在别人的微信上进行退出登录（解除绑定），才能在新的微信上登录（换手机不影响）
 * 代码逻辑：
 * 一进去小程序/微信公众号的时候会通过code获取openid（1.获取code不需要按钮的 2.code大概几分钟会过期）。
 * 如果没有openid就没办法进行支付。所以我们要在登录的时候把openid和token进行关联，通过token获取用户的userId和openid进行支付。
 * 但是会出现一种情况 比如在微信公众号是通过页面跳转获取code的，如果跳转完页面，用户一直在这个页面不去登录。后面再登录的时候已经code已经过期了。
 * 那怎么解决这种情况呢？
 * 我们在拿到code的时候直接调用社交登录的接口，如/social/mp，/social/ma 这两个接口，会出现两种情况
 * 1. 如果通过code获取了openid，通过openid获取绑定的userid，如果能获取userid，那就直接登录，返回token。
 * 2. 如果没有获取到openid，就将这个openid保存用一个uuid关联起来，并保持到数据库。
 * 因为code会过期，数据库我们自己写的标识不会过期，将这个标识返回给前端（同时返回了A04001这个http状态，前端用tempUid保存起来）。
 * 前端进行登录的时候，用这个tempUid给到我们进行登录，这个时候用这个标识就能获取到数据库的openid了，也不会出现过期的情况。
 * 这个时候用openid可以关联登录信息、注册信息、绑定信息。
 *
 * @author tmerclub
 * @date 2021/1/16
 */
@RequestMapping("/ua/social")
@RestController
@Tag(name = "社交账号登录")
public class SocialLoginController {

    private static final Logger logger = LoggerFactory.getLogger(SocialLoginController.class);


    @Autowired
    private WxConfig wxConfig;
    @Autowired
    private AuthSocialService authSocialService;
    @Autowired
    private TokenStore tokenStore;
    @Autowired
    private AuthAccountService authAccountService;
    @Autowired
    private MenuPermissionService menuPermissionService;
    @DubboReference
    private NotifyFeignClient notifyFeignClient;

    @PostMapping("/mp")
    @Operation(summary = "公众号code登录", description = "通过公众号进行登录，只要在一进入到应用的时候调用微信登录的这个接口就行了，不要重复调用这个接口(不要在什么注册页面之类的地方再调用这个接口，打开应用的时候调用就已经足够了)" +
            "1.返回状态码 A04001 社交账号未绑定，当前端看到该异常时，将该值tempUid存起来，并在应该在合适的时间（比如在购买的时候跳），跳转到登录的页面" +
            "2.如果返回码是正常的登录成功，前端要保留原本已经存在的tempUid，如果有人切换登录的话，起码能够用这个tempUid重新获取openid进行绑定")
    public ServerResponseEntity<?> mp(@RequestBody String code) throws WxErrorException {
        Integer socialType = SocialType.MP.value();
        WxOAuth2AccessToken wxAccessToken = wxConfig.getWxMpService().getOAuth2Service().getAccessToken(code);

        AuthSocial authSocial = authSocialService.getByBizUserId(wxAccessToken.getOpenId(), socialType);
        if (authSocial != null && authSocial.getUid() != null) {
            return ServerResponseEntity.success(new TokenWithTempUidVO(authSocial.getTempUid(), getTokenVo(authSocial).getData()));
        }
        String tempUid = IdUtil.simpleUUID();
        WxOAuth2UserInfo wxUserInfo = wxConfig.getWxMpService().getOAuth2Service().getUserInfo(wxAccessToken, null);
        if (authSocial == null) {
            authSocial = new AuthSocial();
        }
        authSocial.setSocialType(socialType);
        authSocial.setTempUid(tempUid);
        authSocial.setNickName(wxUserInfo.getNickname());
        authSocial.setImageUrl(wxUserInfo.getHeadImgUrl());
        authSocial.setBizUserId(wxUserInfo.getOpenid());
        authSocial.setBizUnionid(wxUserInfo.getUnionId());
        authSocialService.saveOrUpdate(authSocial);
        // 前端要保存这个tempUid
        return ServerResponseEntity.success(new TokenWithTempUidVO(authSocial.getTempUid(), null));
    }


    @PostMapping("/ma")
    @Operation(summary = "小程序code登录", description = "通过小程序进行登录，只要在一进入到应用的时候调用微信登录的这个接口就行了，不要重复调用这个接口(不要在什么注册页面之类的地方再调用这个接口，打开应用的时候调用就已经足够了)" +
            "1.返回状态码 A04001 社交账号未绑定，当前端看到该异常时，将该值tempUid存起来，并在应该在合适的时间（比如在购买的时候跳），跳转到登录的页面" +
            "2.如果返回码是正常的登录成功，前端要保留原本已经存在的tempUid，如果有人切换登录的话，起码能够用这个tempUid重新获取openid进行绑定")
    public ServerResponseEntity<?> ma(@RequestBody String code) throws WxErrorException {
        String recode = code.replaceAll("\"", "");
        Integer socialType = SocialType.MA.value();
        WxMaJscode2SessionResult session = wxConfig.getWxMaService().getUserService().getSessionInfo(recode);

        AuthSocial authSocial = authSocialService.getByBizUserId(session.getOpenid(), socialType);
        logger.info("ma-session：{}， authSocial：{}", Json.toJsonString(session), Json.toJsonString(authSocial));
        if (authSocial != null && authSocial.getUid() != null) {
            return ServerResponseEntity.success(new TokenWithTempUidVO(authSocial.getTempUid(), getTokenVo(authSocial).getData()));
        }

        if (authSocial == null) {
            authSocial = new AuthSocial();
        }
        String tempUid = IdUtil.simpleUUID();
        authSocial.setTempUid(tempUid);
        authSocial.setSocialType(socialType);
        authSocial.setBizTempSession(session.getSessionKey());
        authSocial.setBizUserId(session.getOpenid());
        authSocial.setBizUnionid(session.getUnionid());
        authSocialService.saveOrUpdate(authSocial);
        return ServerResponseEntity.success(new TokenWithTempUidVO(authSocial.getTempUid(), null));
    }

    private ServerResponseEntity<TokenInfoVO> getTokenVo(AuthSocial authSocial) {
        AuthAccount authAccount = authAccountService.getByUid(authSocial.getUid());
        UidInfoBO uidInfoBO = BeanUtil.map(authAccount, UidInfoBO.class);

        ClearUserPermissionsCacheDTO clearUserPermissionsCacheDTO = new ClearUserPermissionsCacheDTO();
        clearUserPermissionsCacheDTO.setSysType(uidInfoBO.getSysType());
        clearUserPermissionsCacheDTO.setUserId(uidInfoBO.getUserId());
        // 将以前的权限清理了,以免权限有缓存
        menuPermissionService.clearUserPermissionsCache(clearUserPermissionsCacheDTO.getUserId(), clearUserPermissionsCacheDTO.getSysType());

        // 保存token，返回token数据给前端
        TokenInfoVO tokenInfoVO = tokenStore.storeAndGetVo(uidInfoBO);
        if (Objects.nonNull(authSocial)) {
            // 缓存当前登录的openId
            LoginInfoBO loginInfoBO = new LoginInfoBO();
            loginInfoBO.setSocialType(authSocial.getSocialType());
            loginInfoBO.setBizUserId(authSocial.getBizUserId());
            RedisUtil.set(OauthCacheNames.LOGIN_INFO + tokenInfoVO.getAccessToken(),
                    JSONObject.toJSONString(loginInfoBO),
                    tokenInfoVO.getExpiresIn());
        }
        return ServerResponseEntity.success();
    }
}
