package com.sonic.common.rpc;

import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;

import com.sonic.common.enums.AppEnv;
import com.sonic.common.rpc.exception.RpcNetworkException;
import com.sonic.common.exception.BizException;
import com.sonic.common.rpc.exception.TokenExpiredException;
import org.apache.commons.lang3.StringUtils;
import org.springframework.boot.info.GitProperties;
import org.springframework.context.ApplicationContext;

import com.alibaba.fastjson.JSONObject;
import com.sonic.common.AppRuntime;
import com.google.common.base.Charsets;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import com.google.common.hash.Hashing;
import com.google.common.io.BaseEncoding;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.extern.slf4j.Slf4j;

/**
 * @author code
 */
@Slf4j
public class AuthRequestProxyImpl implements AuthRequestProxy {
    private static final int MAX_RETRY_COUNT = 3;

    private RpcClient rpcClient;

    private TokenReq tokenReq;

    private JSONObject globalTokenReqExt;

    @Builder
    public AuthRequestProxyImpl(RpcClient rpcClient, TokenReq tokenReq, ApplicationContext applicationContext, AppRuntime appRuntime) {
        Objects.requireNonNull(tokenReq);

        this.rpcClient = Optional.ofNullable(rpcClient)
                .orElse(new RpcAuthClientImpl(SIMPLE_PROXY));
        this.tokenReq = tokenReq;

        AppRuntime runtime = Optional.ofNullable(appRuntime)
                .orElse(AppRuntime.builder()
                        .appId(tokenReq.getAppId())
                        .env(tokenReq.getAppEnv())
                        .applicationContext(applicationContext)
                        .build());
        this.globalTokenReqExt = new JSONObject()
                .fluentPut(TokenReq.EXT_START_TIME_KEY, runtime.getStartTime())
                .fluentPut(TokenReq.EXT_GIT_COMMIT_ID_KEY, Optional.ofNullable(runtime.getGitProperties())
                        .map(GitProperties::getShortCommitId).orElse(StringUtils.EMPTY))
                .fluentPutAll(runtime.getExt());
    }

    public AuthRequestProxyImpl(TokenReq tokenReq) {
        this(null, tokenReq, null, null);
    }

    @Deprecated
    public AuthRequestProxyImpl(TokenReq tokenReq, ApplicationContext applicationContext) {
        this(null, tokenReq, applicationContext, null);
    }

    public AuthRequestProxyImpl(RpcClient rpcClient, TokenReq tokenReq) {
        this(rpcClient, tokenReq, null, null);
    }

    private LoadingCache<String, TokenResp> tokenCache = CacheBuilder.
            newBuilder()
            .expireAfterWrite(23L, TimeUnit.HOURS)
            .maximumSize(2)
            .build(new CacheLoader<String, TokenResp>() {
                @Override
                public TokenResp load(String key) throws Exception {
                    return fetchToken();
                }
            });

    private TokenResp fetchToken() {
        String result;
        try {
            result = rpcClient.post(tokenReq.getTokenUrl(),
                    tokenReq.toTokenReq(globalTokenReqExt),
                    String.class);
        } catch (BizException e) {
            return TokenResp.builder()
                    .exception(e)
                    .build();
        } catch (Exception e) {
            log.error("get token from app-center error, params = {}", tokenReq, e);
            ApiResultCode resultCode = e.getClass().isAssignableFrom(RpcNetworkException.class)
                    ? GlobalResultCode.NETWORK_FAILURE
                    : GlobalResultCode.SYSTEM_EXCEPTION;
            return TokenResp.builder()
                    .exception(new BizException(resultCode, e))
                    .build();
        }

        return TokenResp.builder()
                .token(result)
                .build();
    }

    private String getToken() {
        TokenResp tokenResp = tokenCache.getUnchecked("TOKEN");
        if (tokenResp.exception != null) {
            tokenCache.invalidateAll();
            throw tokenResp.exception;
        }
        return tokenResp.token;
    }


    @Override
    public <T> Optional<T> request(Function<String, T> request) {
        for (int i = 0; i < MAX_RETRY_COUNT; i++) {
            String token = getToken();
            try {
                return Optional.ofNullable(request.apply(token));
            } catch (TokenExpiredException e) {
                synchronized (this) {
                    // 只有token没有发生了变化才去刷新. 避免并发情况下同时清除缓存.
                    if (token.equals(getToken())) {
                        tokenCache.invalidateAll();
                    }
                }
            }
        }
        return Optional.empty();
    }

    @Data
    @Builder
    @NoArgsConstructor
    @AllArgsConstructor
    static class TokenResp {
        String token;
        BizException exception;

        public TokenResp(String token) {
            this.token = token;
        }
    }

    @Data
    @Builder
    @AllArgsConstructor
    public static class TokenReq {
        private String appId;
        /**
         * 应用的环境
         */
        private AppEnv appEnv;
        /**
         * 扩展参数
         */
        private String ext;

        private String secret;

        private String tokenUrl;

        public static final String EXT_GIT_COMMIT_ID_KEY = "gci";
        public static final String EXT_START_TIME_KEY = "st";

        public JSONObject toTokenReq(JSONObject globalTokenReqExt) {
            long timestamp = System.currentTimeMillis();
            String mixedExt = Optional.ofNullable(JSONObject.parseObject(ext)).orElse(new JSONObject())
                    .fluentPutAll(globalTokenReqExt)
                    .toJSONString();

            String signature = BaseEncoding.base64().encode(Hashing.sha256().newHasher()
                    .putString(appId + appEnv.name() + mixedExt + timestamp + secret, Charsets.UTF_8)
                    .hash()
                    .asBytes());

            return new JSONObject()
                    .fluentPut("appId", appId)
                    .fluentPut("appEnv", appEnv.name())
                    .fluentPut("ext", mixedExt)
                    .fluentPut("signature", signature)
                    .fluentPut("timestamp", timestamp);
        }
    }
}
