package com.sonic.common.auth;

import java.lang.annotation.Annotation;
import java.security.KeyFactory;
import java.security.interfaces.RSAPublicKey;
import java.security.spec.X509EncodedKeySpec;
import java.util.Objects;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import com.sonic.common.enums.AppEnv;
import com.sonic.common.exception.BizException;
import org.apache.commons.lang3.StringUtils;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.core.annotation.Order;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;

import com.alibaba.fastjson.JSONObject;
import com.auth0.jwt.JWT;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.exceptions.TokenExpiredException;
import com.auth0.jwt.interfaces.DecodedJWT;
import com.google.common.base.Strings;
import com.google.common.io.BaseEncoding;

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

/**
 * @author code
 */
@Slf4j
@Order(100)
public class AuthInterceptor implements HandlerInterceptor {

    public final static String AUTH_PAYLOAD_KEY = "_auth_payload_";
    public final static String CALLER_PAYLOAD_KEY = "_caller_payload_";

    private Algorithm algorithm;

    private static final String AUTH_HEADER = "Authorization";

    private static final String SUPPORT_AUTH = "Bearer";

    public static final String ORIGINAL_CALLER_HEADER_NAME = "xg-original-caller";

    public AuthInterceptor(String publicKey) throws Exception {
        Objects.requireNonNull(publicKey);

        RSAPublicKey rsaPublicKey = (RSAPublicKey) KeyFactory.getInstance("RSA")
                .generatePublic(new X509EncodedKeySpec(BaseEncoding.base64().decode(publicKey)));

        this.algorithm = Algorithm.RSA256(rsaPublicKey, null);
    }

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
        if (!(handler instanceof HandlerMethod)) {
            return true;
        }
        IgnoreAuth ignoreAuth = getAnnotation((HandlerMethod) handler, IgnoreAuth.class);
        if (ignoreAuth != null) {
            return true;
        }

        Token token = Token.from(request.getHeader(AUTH_HEADER));
        if (!StringUtils.equals(SUPPORT_AUTH, token.getType())) {
            throw new BizException(AuthResultCode.API_AUTH_NOT_SUPPORTED);
        }

        try {
            DecodedJWT jwt = JWT.require(algorithm).build()
                    .verify(token.getToken());
            AuthCaller payload = AuthCaller.builder()
                    .appId(jwt.getClaim("appId").asString())
                    .appEnv(AppEnv.valueOf(jwt.getClaim("appEnv").asString()))
                    .ext(jwt.getClaim("ext").asString())
                    .build();

            request.setAttribute(AUTH_PAYLOAD_KEY, payload);

            OriginalCaller caller;
            String originalCallerHeader = request.getHeader(ORIGINAL_CALLER_HEADER_NAME);
            if (!Strings.isNullOrEmpty(originalCallerHeader)) {
                caller = JSONObject.parseObject(originalCallerHeader, OriginalCaller.class);
            } else {
                caller = OriginalCaller.fromAuthCaller(payload);
            }
            request.setAttribute(CALLER_PAYLOAD_KEY, caller);
        } catch (TokenExpiredException e) {
            response.setStatus(com.sonic.common.rpc.exception.TokenExpiredException.STATUS_CODE);
            throw new BizException(AuthResultCode.API_AUTH_TOKEN_EXPIRED);
        } catch (Exception e) {
            log.error("api auth, token is invalid. token = {}", token, e);
            throw new BizException(AuthResultCode.API_AUTH_TOKEN_INVALID);
        }

        return true;
    }

    private <T extends Annotation> T getAnnotation(HandlerMethod handlerMethod, Class<T> clazz) {
        T annotation = handlerMethod.getMethodAnnotation(clazz);
        if (annotation != null) {
            return annotation;
        }
        annotation = AnnotationUtils.findAnnotation(handlerMethod.getBeanType(), clazz);
        return annotation;
    }

    @Data
    @Builder
    @AllArgsConstructor
    @ToString
    private static class Token {
        private static final transient String DELIMITER = StringUtils.SPACE;

        private String type;

        private String token;

        public static Token from(String header) {
            return Token.builder()
                    .type(StringUtils.substringBefore(header, DELIMITER))
                    .token(StringUtils.substringAfter(header, DELIMITER))
                    .build();
        }
    }
}
