package com.hys.app.controller.open;

import cn.hutool.core.lang.Assert;
import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.StrUtil;
import com.hys.app.converter.oauth2.OAuth2OpenConvert;
import com.hys.app.framework.exception.ServiceException;
import com.hys.app.model.oauth2.dos.OAuth2AccessTokenDO;
import com.hys.app.model.oauth2.dos.OAuth2ClientDO;
import com.hys.app.model.oauth2.enums.OAuth2GrantTypeEnum;
import com.hys.app.model.oauth2.vo.OAuth2OpenAccessTokenRespVO;
import com.hys.app.model.oauth2.vo.OAuth2OpenAuthorizeInfoRespVO;
import com.hys.app.model.oauth2.vo.OAuth2OpenCheckTokenRespVO;
import com.hys.app.service.oauth2.OAuth2ClientManager;
import com.hys.app.service.oauth2.OAuth2GrantManager;
import com.hys.app.service.oauth2.OAuth2TokenManager;
import com.hys.app.oauth2.utils.OAuth2HttpUtils;
import com.hys.app.oauth2.utils.OAuth2Utils;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiImplicitParam;
import io.swagger.annotations.ApiImplicitParams;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;

import javax.servlet.http.HttpServletRequest;
import java.util.List;

/**
 * OAuth2.0 授权 API
 *
 * @author 张崧
 * @since 2024-02-20
 */
@Api(tags = "OAuth2.0 授权")
@RestController
@RequestMapping("/oauth2")
@Validated
@Slf4j
public class OAuth2OpenController {

    @Autowired
    private OAuth2GrantManager oAuth2GrantManager;
    @Autowired
    private OAuth2ClientManager oAuth2ClientManager;
    @Autowired
    private OAuth2TokenManager oAuth2TokenManager;

    /**
     * 对应 Spring Security OAuth的  TokenEndpoint 类的 postAccessToken 方法
     * <p>
     * 授权码 authorization_code 模式时：code + redirectUri + state 参数
     * 密码 password 模式时：username + password + scope 参数
     * 刷新 refresh_token 模式时：refreshToken 参数
     * 客户端 client_credentials 模式：scope 参数
     * 简化 implicit 模式时：不支持
     * <p>
     * 默认都需要传递 client_id + client_secret 参数
     */
    @GetMapping("/token")
    @ApiOperation(value = "获得访问令牌")
    @ApiImplicitParams({
            @ApiImplicitParam(name = "grant_type", required = true, value = "授权类型",
                    allowableValues = "password,authorization_code,client_credentials,refresh_token"),
            @ApiImplicitParam(name = "code", value = "授权码（授权码模式必传）"),
            @ApiImplicitParam(name = "redirect_uri", value = "重定向 URI（授权码模式必传）"),
            @ApiImplicitParam(name = "state", value = "状态（授权码模式选传）"),
            @ApiImplicitParam(name = "username", value = "用户名（密码模式必传）"),
            @ApiImplicitParam(name = "password", value = "密码（密码模式必传）"),
            @ApiImplicitParam(name = "scope", value = "授权范围（授权码、密码模式必传）"),
            @ApiImplicitParam(name = "refresh_token", value = "刷新token（刷新模式必传）")
    })
    public OAuth2OpenAccessTokenRespVO postAccessToken(HttpServletRequest request,
                                                       @RequestParam("grant_type") String grantType,
                                                       @RequestParam(value = "code", required = false) String code,
                                                       @RequestParam(value = "redirect_uri", required = false) String redirectUri,
                                                       @RequestParam(value = "state", required = false) String state,
                                                       @RequestParam(value = "username", required = false) String username,
                                                       @RequestParam(value = "password", required = false) String password,
                                                       @RequestParam(value = "scope", required = false) String scope,
                                                       @RequestParam(value = "refresh_token", required = false) String refreshToken) {
        List<String> scopes = OAuth2Utils.buildScopes(scope);
        // 1.1 校验授权类型
        OAuth2GrantTypeEnum grantTypeEnum = OAuth2GrantTypeEnum.getByGranType(grantType);
        if (grantTypeEnum == null) {
            throw new ServiceException(StrUtil.format("未知授权类型({})", grantType));
        }
        if (grantTypeEnum == OAuth2GrantTypeEnum.IMPLICIT) {
            throw new ServiceException("Token 接口不支持 implicit 授权模式");
        }

        // 1.2 校验客户端
        String[] clientIdAndSign = obtainBasicAuthorization(request);
        OAuth2ClientDO client = oAuth2ClientManager.validOAuthClientFromCache(clientIdAndSign[0], clientIdAndSign[1], clientIdAndSign[2],
                grantType, scopes, redirectUri);

        // 2. 根据授权模式，获取访问令牌
        OAuth2AccessTokenDO accessTokenDO;
        switch (grantTypeEnum) {
            case CLIENT_CREDENTIALS:
                accessTokenDO = oAuth2GrantManager.grantClientCredentials(client.getClientId(), null);
                break;
            case REFRESH_TOKEN:
                accessTokenDO = oAuth2GrantManager.grantRefreshToken(refreshToken, client.getClientId());
                break;
            case AUTHORIZATION_CODE:
            case PASSWORD:
                // todo 目前只用到了客户端模式
                throw new UnsupportedOperationException("暂不支持");
            default:
                throw new IllegalArgumentException("未知授权类型：" + grantType);
        }
        Assert.notNull(accessTokenDO, "访问令牌错误");
        return OAuth2OpenConvert.INSTANCE.convert(accessTokenDO);
    }

    @DeleteMapping("/token")
    @ApiOperation(value = "删除访问令牌")
    @ApiImplicitParam(name = "token", required = true, value = "访问令牌")
    public void revokeToken(HttpServletRequest request, @RequestParam("token") String token) {
        // 校验客户端
        String[] clientIdAndSecret = obtainBasicAuthorization(request);
        OAuth2ClientDO client = oAuth2ClientManager.validOAuthClientFromCache(clientIdAndSecret[0], clientIdAndSecret[1],
                clientIdAndSecret[2], null, null, null);

        // 删除访问令牌
        oAuth2GrantManager.revokeToken(client.getClientId(), token);
    }

    /**
     * 对应 Spring Security OAuth 的 CheckTokenEndpoint 类的 checkToken 方法
     */
    @PostMapping("/check-token")
    @ApiOperation(value = "校验访问令牌")
    @ApiImplicitParam(name = "token", required = true, value = "访问令牌", example = "biu")
    public OAuth2OpenCheckTokenRespVO checkToken(HttpServletRequest request,
                                                 @RequestParam("token") String token) {
        // 校验客户端
        String[] clientIdAndSecret = obtainBasicAuthorization(request);
        oAuth2ClientManager.validOAuthClientFromCache(clientIdAndSecret[0], clientIdAndSecret[1],
                clientIdAndSecret[2], null, null, null);

        // 校验令牌
        OAuth2AccessTokenDO accessTokenDO = oAuth2TokenManager.checkAccessToken(token);
        return OAuth2OpenConvert.INSTANCE.convert2(accessTokenDO);
    }

    /**
     * 对应 Spring Security OAuth 的 AuthorizationEndpoint 类的 authorize 方法
     */
    @GetMapping("/authorize")
    @ApiOperation(value = "获得授权信息", notes = "适合 code 授权码模式，或者 implicit 简化模式；在 sso.vue 单点登录界面被【获取】调用")
    @ApiImplicitParam(name = "clientId", required = true, value = "客户端编号", example = "tudou")
    public OAuth2OpenAuthorizeInfoRespVO authorize(@RequestParam("clientId") String clientId) {
        // todo 目前系统只实现客户端模式
        throw new UnsupportedOperationException("暂不支持");
    }

    /**
     * 对应 Spring Security OAuth 的 AuthorizationEndpoint 类的 approveOrDeny 方法
     * <p>
     * 场景一：【自动授权 autoApprove = true】
     * 刚进入 sso.vue 界面，调用该接口，用户历史已经给该应用做过对应的授权，或者 OAuth2Client 支持该 scope 的自动授权
     * 场景二：【手动授权 autoApprove = false】
     * 在 sso.vue 界面，用户选择好 scope 授权范围，调用该接口，进行授权。此时，approved 为 true 或者 false
     * <p>
     * 因为前后端分离，Axios 无法很好的处理 302 重定向，所以和 Spring Security OAuth 略有不同，返回结果是重定向的 URL，剩余交给前端处理
     */
    @PostMapping("/authorize")
    @ApiOperation(value = "申请授权", notes = "适合 code 授权码模式，或者 implicit 简化模式；在 sso.vue 单点登录界面被【提交】调用")
    @ApiImplicitParams({
            @ApiImplicitParam(name = "response_type", required = true, value = "响应类型"),
            @ApiImplicitParam(name = "client_id", required = true, value = "客户端编号"),
            @ApiImplicitParam(name = "scope", value = "授权范围", example = "userinfo.read"), // 使用 Map<String, Boolean> 格式，Spring MVC 暂时不支持这么接收参数
            @ApiImplicitParam(name = "redirect_uri", required = true, value = "重定向 URI"),
            @ApiImplicitParam(name = "auto_approve", required = true, value = "用户是否接受", example = "true"),
            @ApiImplicitParam(name = "state", example = "1")
    })
    public String approveOrDeny(@RequestParam("response_type") String responseType,
                                @RequestParam("client_id") String clientId,
                                @RequestParam(value = "scope", required = false) String scope,
                                @RequestParam("redirect_uri") String redirectUri,
                                @RequestParam(value = "auto_approve") Boolean autoApprove,
                                @RequestParam(value = "state", required = false) String state) {
        // todo 目前系统只实现客户端模式
        throw new UnsupportedOperationException("暂不支持");
    }

    private String[] obtainBasicAuthorization(HttpServletRequest request) {
        String[] clientIdAndSecret = OAuth2HttpUtils.obtainBasicAuthorization(request);
        if (ArrayUtil.isEmpty(clientIdAndSecret) || clientIdAndSecret.length != 3) {
            throw new ServiceException("client_id 或 sign 或 timestamp 未正确传递");
        }
        return clientIdAndSecret;
    }

}
