package net.northking.cctp.element.internal.protocol.http.auth.oauth2;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import net.northking.cctp.element.util.Assert;
import org.apache.commons.lang3.StringUtils;
import org.apache.http.Header;
import org.apache.http.NameValuePair;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.util.EntityUtils;

import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * OAuth2Client
 *
 * @author fengxiang.zhang
 * @since 1.0
 */
public class OAuth2Client {

    private CloseableHttpClient client;

    public OAuth2Client(CloseableHttpClient client) {
        this.client = client;
    }

    /**
     * 请求Token
     */
    public Token requestToken(OAuth2Credentials credentials) throws Exception {
        String grantType = credentials.getGrantType();
        Assert.notNull(grantType, "OAuth2授权方式NULL");
        switch (grantType) {
            case OAuth2Credentials.MODE_AUTHORIZATION_CODE: {
                return authorizationCode(credentials);
            }
            case OAuth2Credentials.MODE_CLIENT_CREDENTIALS: {
                return clientCredentials(credentials);
            }
            case OAuth2Credentials.MODE_PASSWORD: {
                return password(credentials);
            }
            default:
                throw new IllegalArgumentException("OAuth2授权方式" + grantType + "未被支持");
        }
    }

    /**
     * 请求Token<br>
     * Authorization_code模式
     */
    private Token authorizationCode(OAuth2Credentials credentials) throws Exception {
        HttpPost post = new HttpPost(credentials.getAccessTokenUrl());
        List<NameValuePair> nvs = new ArrayList();
        nvs.add(new BasicNameValuePair("grant_type", OAuth2Credentials.MODE_AUTHORIZATION_CODE));
        nvs.add(new BasicNameValuePair("code", credentials.getCode()));
        nvs.add(new BasicNameValuePair("client_id", credentials.getClientId()));
        nvs.add(new BasicNameValuePair("client_secret", credentials.getClientSecret()));
        nvs.add(new BasicNameValuePair("scope", credentials.getScope()));
        post.setEntity(new UrlEncodedFormEntity(nvs, StandardCharsets.UTF_8.name()));
        return getToken(post);
    }

    /**
     * 请求Token<br>
     * PASSWORD模式
     */
    private Token password(OAuth2Credentials credentials) throws Exception {
        HttpPost post = new HttpPost(credentials.getAccessTokenUrl());
        List<NameValuePair> nvs = new ArrayList();
        nvs.add(new BasicNameValuePair("grant_type", OAuth2Credentials.MODE_PASSWORD));
        nvs.add(new BasicNameValuePair("client_id", credentials.getClientId()));
        nvs.add(new BasicNameValuePair("client_secret", credentials.getClientSecret()));
        nvs.add(new BasicNameValuePair("username", credentials.getUsername()));
        nvs.add(new BasicNameValuePair("password", credentials.getPassword()));
        nvs.add(new BasicNameValuePair("scope", credentials.getScope()));
        post.setEntity(new UrlEncodedFormEntity(nvs, StandardCharsets.UTF_8.name()));
        return getToken(post);
    }

    /**
     * 请求Token<br>
     * CLIENT_CREDENTIALS模式
     */
    private Token clientCredentials(OAuth2Credentials credentials) throws Exception {
        HttpPost post = new HttpPost(credentials.getAccessTokenUrl());
        List<NameValuePair> nvs = new ArrayList();
        nvs.add(new BasicNameValuePair("grant_type", OAuth2Credentials.MODE_CLIENT_CREDENTIALS));
        nvs.add(new BasicNameValuePair("client_id", credentials.getClientId()));
        nvs.add(new BasicNameValuePair("client_secret", credentials.getClientSecret()));
        nvs.add(new BasicNameValuePair("scope", credentials.getScope()));
        post.setEntity(new UrlEncodedFormEntity(nvs, StandardCharsets.UTF_8.name()));
        return getToken(post);
    }

    /**
     * 存在Refresh_token则刷新，反之则重新申请
     */
    public Token refreshToken(OAuth2Credentials credentials, Token token) throws Exception {
        HttpPost post = new HttpPost(credentials.getAccessTokenUrl());
        List<NameValuePair> nvs = new ArrayList();
        nvs.add(new BasicNameValuePair("client_id", credentials.getClientId()));
        nvs.add(new BasicNameValuePair("client_secret", credentials.getClientSecret()));
        nvs.add(new BasicNameValuePair("username", credentials.getUsername()));
        nvs.add(new BasicNameValuePair("password", credentials.getPassword()));
        nvs.add(new BasicNameValuePair("scope", credentials.getScope()));
        if (StringUtils.isNotBlank(token.getRefresh_token())) {
            nvs.add(new BasicNameValuePair("grant_type", "refresh_token"));
            nvs.add(new BasicNameValuePair("refresh_token", token.getRefresh_token()));
        } else {
            nvs.add(new BasicNameValuePair("grant_type", credentials.getGrantType()));
        }
        post.setEntity(new UrlEncodedFormEntity(nvs, StandardCharsets.UTF_8.name()));
        return getToken(post);
    }

    /**
     * 请求Token<br>
     * implicit 模式
     */
    private Token implicit(OAuth2Credentials credentials) throws Exception {
        HttpGet get = new HttpGet(credentials.getAccessTokenUrl());
        Map<String, String> params = new HashMap<>();
        params.put("response_type", "token");
        params.put("redirect_url", credentials.getRedirectUrl());
        // 设置请求前时间为获取到授权时的时间
        long generatedTime = System.currentTimeMillis();
        CloseableHttpResponse response = client.execute(get);
        // 提取Token
        Header location = response.getFirstHeader("Location");

        URL url = new URL(location.getValue());
        String query = url.getQuery();
        String[] kvs = StringUtils.split(query, "&");// #
        JSONObject args = new JSONObject();
        for (int i = 0; i < kvs.length; i++) {
            String kv = kvs[i];
            String[] part = kv.split("=");
            args.put(part[0], part[1]);
        }
        Token token = args.toJavaObject(Token.class);
        token.setGeneratedTime(generatedTime);
        return token;
    }

    private Token getToken(HttpPost post) throws Exception {
        // 设置请求前时间为获取到授权时的时间
        long generatedTime = System.currentTimeMillis();
        CloseableHttpResponse response = client.execute(post);
        String text = EntityUtils.toString(response.getEntity());
        Token token = JSON.parseObject(text, Token.class);
        token.setGeneratedTime(generatedTime);
        return token;
    }

    public static class Token {
        private long expires_in;
        private String token_type;
        private String refresh_token;
        private String access_token;
        private String scope;
        private long generatedTime;

        public boolean isExpire() {
            return System.currentTimeMillis() > generatedTime + (expires_in - 60) * 1000;
        }

        public long getExpires_in() {
            return expires_in;
        }

        public void setExpires_in(long expires_in) {
            this.expires_in = expires_in;
        }

        public String getToken_type() {
            return token_type;
        }

        public void setToken_type(String token_type) {
            this.token_type = token_type;
        }

        public String getRefresh_token() {
            return refresh_token;
        }

        public void setRefresh_token(String refresh_token) {
            this.refresh_token = refresh_token;
        }

        public String getAccess_token() {
            return access_token;
        }

        public void setAccess_token(String access_token) {
            this.access_token = access_token;
        }

        public String getScope() {
            return scope;
        }

        public void setScope(String scope) {
            this.scope = scope;
        }

        public long getGeneratedTime() {
            return generatedTime;
        }

        public void setGeneratedTime(long generatedTime) {
            this.generatedTime = generatedTime;
        }
    }
}
