package com.xhpc.evcs.config;

import cn.hutool.core.date.DatePattern;
import cn.hutool.core.date.DateTime;
import cn.hutool.core.date.DateUtil;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.JsonNodeFactory;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.xhpc.evcs.domain.AuthSecretToken;
import com.xhpc.evcs.dto.CommonRequest;
import com.xhpc.evcs.dto.CommonResponse;
import com.xhpc.evcs.dto.EquipBizRequest;
import com.xhpc.evcs.encryption.Aes128Cbc;
import com.xhpc.evcs.encryption.EvcsConst;
import com.xhpc.evcs.encryption.HMAC;
import com.xhpc.evcs.http.HttpServletRequestRepeatReadWrapper;
import com.xhpc.evcs.http.HttpServletRequestWritableWrapper;
import com.xhpc.evcs.jpa.AuthSecretTokenRepository;
import com.xhpc.evcs.utils.JSONUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.util.ObjectUtils;
import org.springframework.web.filter.OncePerRequestFilter;
import org.springframework.web.util.ContentCachingResponseWrapper;

import javax.annotation.Resource;
import javax.crypto.BadPaddingException;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.*;

@SuppressWarnings({"Duplicates"}) //todo
@Slf4j
//@Component //spring boot way, see https://www.surasint.com/spring-boot-webfilter-instead-of-component/
//@Order(1) not supported, see https://github.com/spring-projects/spring-boot/issues/8276
//成都市监管平台(蓉成充)v10  v20 川逸充  v30  v40车为家  v50云快充
@WebFilter(urlPatterns = {"/v1/*", "/v2/*", "/v10/*", "/v20/*", "/v30/*", "/v40/*", "/v50/*"}, filterName = "v1n10filter") //multiple filters execute by filterName order
public class EvcsFilter extends OncePerRequestFilter {

    @Resource
    private AuthSecretTokenRepository authSecretTokenRepository;
    @Value("${xhpc.evcs.encrypt:false}")
    private boolean encrypt;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException {

        if (!encrypt) {
            chain.doFilter(request, response);
            return;
        }
        ServletRequest requestWrapper = new HttpServletRequestRepeatReadWrapper(request);
//        final String encin = request.getHeader("enc.in");
//        if (encin != null && encin.equals("false")) { //todo comment out in prd env
//            chain.doFilter(requestWrapper, response);
//            return;
//        }
        Scanner scanner = new Scanner(requestWrapper.getInputStream(), "UTF-8").useDelimiter("\\A");
        String bodyString = scanner.hasNext() ? scanner.next() : null;
        log.debug("in.enc: {}", bodyString);
        CommonResponse resp = new CommonResponse();
        if (!ObjectUtils.isEmpty(bodyString)) {
            String servletPath = request.getServletPath();
            log.debug("servletPath: " + servletPath);
            //log.info("=================servletPath=========================="+servletPath);
            CommonRequest commonRequest = JSONUtil.readParams(bodyString, CommonRequest.class);
            resp.setRet(EvcsConst.RET_FAIL);
            ContentCachingResponseWrapper responseWrapper = new ContentCachingResponseWrapper(response);
            if (commonRequest == null) {
                resp.setMsg("Request params validation failed");
                resp.setRet("4004");
                String data = JSONUtil.toJSONString(resp);
                response.getOutputStream().write(data.getBytes(StandardCharsets.UTF_8));
                chain.doFilter(requestWrapper, responseWrapper);
                return;
            } else {
                String operatorId = commonRequest.getOperatorId();

               // log.info("=================operatorId=========================="+operatorId);

                String authorization = request.getHeader("Authorization");
                log.debug("Authorization: {}", authorization);
                AuthSecretToken authSecretTokenIn = null;
                Date now = Calendar.getInstance().getTime();

                if (servletPath.endsWith("/v1/query_token")) {
                  //  log.info("================老接口过滤 token========operatorId=========="+operatorId);
                    authSecretTokenIn = authSecretTokenRepository.findByStatusAndOperatorId3irdptyAndSecretTokenType(1,operatorId,
                            AuthSecretToken.SECRET_TOKEN_TYPE_IN).orElse(null);
                    handleQueryToken(request, response, chain, requestWrapper, bodyString, commonRequest,
                            responseWrapper, authSecretTokenIn);
                }else if (servletPath.endsWith("/v10/query_token")) {
                   // log.info("================新监管平台（蓉城充） token========operatorId=========="+operatorId);
                    authSecretTokenIn = authSecretTokenRepository.findByStatusAndOperatorId3irdptyAndSecretTokenType(2,operatorId,
                            AuthSecretToken.SECRET_TOKEN_TYPE_IN).orElse(null);
                    handleQueryToken(request, response, chain, requestWrapper, bodyString, commonRequest,
                            responseWrapper, authSecretTokenIn);
                }else if (servletPath.endsWith("/v20/query_token")) {
                  //  log.info("================川逸充 token========operatorId=========="+operatorId);
                    authSecretTokenIn = authSecretTokenRepository.findByStatusAndOperatorId3irdptyAndSecretTokenType(3,operatorId,
                            AuthSecretToken.SECRET_TOKEN_TYPE_IN).orElse(null);
                    handleQueryToken(request, response, chain, requestWrapper, bodyString, commonRequest,
                            responseWrapper, authSecretTokenIn);
                }else if (servletPath.endsWith("/v40/query_token")) {
                    authSecretTokenIn = authSecretTokenRepository.findByStatusAndOperatorId3irdptyAndSecretTokenType(4,operatorId,AuthSecretToken.SECRET_TOKEN_TYPE_IN).orElse(null);
                    handleQueryToken(request, response, chain, requestWrapper, bodyString, commonRequest,responseWrapper, authSecretTokenIn);
                }else if (servletPath.endsWith("/v50/query_token")) {
                    authSecretTokenIn = authSecretTokenRepository.findByStatusAndOperatorId3irdptyAndSecretTokenType(5,operatorId,AuthSecretToken.SECRET_TOKEN_TYPE_IN).orElse(null);
                    handleQueryToken(request, response, chain, requestWrapper, bodyString, commonRequest,responseWrapper, authSecretTokenIn);
                } else {
                    if (authorization != null && authorization.startsWith("Bearer ")) {
                        String token = authorization.replace("Bearer ", "");

//                        if(servletPath.endsWith("/v1/")){
//                            authSecretTokenIn =
//                                    authSecretTokenRepository.findByStatusAndOperatorId3irdptyAndSecretTokenTypeAndTokenExpiryGreaterThan(1,
//                                            operatorId, AuthSecretToken.SECRET_TOKEN_TYPE_IN, now).orElse(null);
//                        }else if(servletPath.endsWith("/v10/")){
//                            authSecretTokenIn =
//                                    authSecretTokenRepository.findByStatusAndOperatorId3irdptyAndSecretTokenTypeAndTokenExpiryGreaterThan(2,
//                                            operatorId, AuthSecretToken.SECRET_TOKEN_TYPE_IN, now).orElse(null);
//                        }else if(servletPath.endsWith("/v20/")){
//                            authSecretTokenIn =
//                                    authSecretTokenRepository.findByStatusAndOperatorId3irdptyAndSecretTokenTypeAndTokenExpiryGreaterThan(3,
//                                            operatorId, AuthSecretToken.SECRET_TOKEN_TYPE_IN, now).orElse(null);
//                        }

                        authSecretTokenIn =authSecretTokenRepository.findByOperatorId3irdptyAndSecretTokenTypeAndTokenExpiryGreaterThan(
                                        operatorId, AuthSecretToken.SECRET_TOKEN_TYPE_IN, now).orElse(null);
                        if (authSecretTokenIn == null) {
                            resp.setRet("4002");
                            resp.setMsg("Invalid token(db)");
                            String data = JSONUtil.toJSONString(resp);
                            response.getOutputStream().write(data.getBytes(StandardCharsets.UTF_8));
                            chain.doFilter(requestWrapper, responseWrapper);
                            return;
                        } else if (!token.equals(authSecretTokenIn.getToken())) {
                            log.error("op[{}] Invalid auth: {}", operatorId, authorization);
                            resp.setRet("4002");
                            resp.setMsg("Invalid token(inequal)");
                            String data = JSONUtil.toJSONString(resp);
                            response.getOutputStream().write(data.getBytes(StandardCharsets.UTF_8));
                            chain.doFilter(requestWrapper, responseWrapper);
                            return;
                        }
                    } else {
                        log.error("op[{}] Invalid auth: {}", operatorId, authorization);
                        resp.setRet("4002");
                        resp.setMsg("Authorization header is not present or invalid");
                        String data = JSONUtil.toJSONString(resp);
                        response.getOutputStream().write(data.getBytes(StandardCharsets.UTF_8));
                        chain.doFilter(requestWrapper, responseWrapper);
                        return;
                    }
                    //decrypt request
                    byte[] decryptedReq = null;
                    String erroMsg = "Decryption error";
                    try {
                        decryptedReq = decrypt(request, authSecretTokenIn, commonRequest, bodyString);
                        commonRequest.setData(new String(decryptedReq));
                        log.debug("in.dec: {}", commonRequest);
                    } catch (BadPaddingException | InvalidAlgorithmParameterException | NoSuchAlgorithmException | IllegalBlockSizeException | NoSuchPaddingException | InvalidKeyException e) {
                        erroMsg = e.getMessage();
                    }
                    if (decryptedReq != null && decryptedReq.length > 0) {
                        requestWrapper = new HttpServletRequestWritableWrapper(request,
                                JSONUtil.toJSONString(commonRequest).getBytes(StandardCharsets.UTF_8));
                    } else {
                        resp.setRet("4004");
                        resp.setMsg(erroMsg);
                        String data = JSONUtil.toJSONString(resp);
                        response.getOutputStream().write(data.getBytes(StandardCharsets.UTF_8));
                        chain.doFilter(requestWrapper, responseWrapper);
                        return;
                    }
                }

                //encrypt response
                final String encout = request.getHeader("enc.out");
                chain.doFilter(requestWrapper, responseWrapper);
                byte[] buf = responseWrapper.getContentAsByteArray();
               // log.debug("out.plain: {}", new String(buf, StandardCharsets.UTF_8));
                String encryptedData;
                if (encout == null) {
                    encryptedData = encryptRespOut(authSecretTokenIn.getDataSecret(), authSecretTokenIn.getDataSecretIV(),
                            authSecretTokenIn.getSigSecret(), buf).toString();
//                    log.debug("out.enc: {}", encryptedData);
                    response.getOutputStream().write(encryptedData == null ? internalError() :
                            encryptedData.getBytes(StandardCharsets.UTF_8));
                } else if ("false".equals(encout)) {
                    response.getOutputStream().write(buf);
                }
            }
        }
    }

    private boolean handleQueryToken(HttpServletRequest request, HttpServletResponse response, FilterChain chain,
                                     ServletRequest requestWrapper, String bodyString, CommonRequest commonRequest,
                                     ContentCachingResponseWrapper responseWrapper, AuthSecretToken authSecretToken) throws IOException,
            ServletException {

        if (authSecretToken == null) {
            CommonResponse resp = new CommonResponse();
            resp.setRet("4003");
            resp.setMsg("Invalid OperatorID");
            String data = JSONUtil.toJSONString(resp);
            response.getOutputStream().write(data.getBytes(StandardCharsets.UTF_8));
            chain.doFilter(requestWrapper, responseWrapper);
            return false;
        } else {
            final byte[] decrypt;
            try {
                decrypt = decrypt(request, authSecretToken, commonRequest, bodyString);
            } catch (BadPaddingException | InvalidAlgorithmParameterException | NoSuchAlgorithmException | IllegalBlockSizeException | NoSuchPaddingException | InvalidKeyException e) {
                e.printStackTrace();
                CommonResponse resp = new CommonResponse();
                resp.setRet("4003");
                resp.setMsg("Invalid Encryption");
                String data = JSONUtil.toJSONString(resp);
                response.getOutputStream().write(data.getBytes(StandardCharsets.UTF_8));
                chain.doFilter(requestWrapper, responseWrapper);
                return false;
            }
            ((HttpServletRequestRepeatReadWrapper) requestWrapper).setBody(decrypt);
            return true;
        }
    }

    private byte[] internalError() {

        CommonResponse resp = new CommonResponse();
        resp.setData("");
        resp.setRet("1");
        resp.setMsg("Internal error.");
        return resp.toString().getBytes();
    }

    public static String encryptReqOut(String dataSecret, String dataSecretIV, String sigSecret, CommonRequest commonRequest)
            throws BadPaddingException, InvalidAlgorithmParameterException, NoSuchAlgorithmException,
            IllegalBlockSizeException, NoSuchPaddingException, InvalidKeyException, JsonProcessingException {

        String data = Aes128Cbc.encrypt(commonRequest.getData(), dataSecret, dataSecretIV);
        commonRequest.setData(data);
        Calendar cal = Calendar.getInstance();
        DateTime dt = new DateTime(cal);
        commonRequest.setTimeStamp(dt.toString(DatePattern.PURE_DATETIME_PATTERN));
        commonRequest.setSeq(String.valueOf(DateUtil.millisecond(dt)));
        String sig =
                HMAC.hmacDigest(commonRequest.getOperatorId() + data + commonRequest.getTimeStamp() + commonRequest.getSeq(),
                        sigSecret);
        commonRequest.setSig(sig);
        final String jsonString = JSONUtil.toJSONString(commonRequest);
//        log.debug("enc to out:{} ", jsonString);
        return jsonString;
    }

    public static JsonNode encryptRespOut(String dataSecret, String dataSecretIV, String sigSecret, byte[] buf) {

        JsonNode rootNode;
        String data = null;
        if (buf.length > 0) {
            ObjectMapper objectMapper = new ObjectMapper();
            try {
                rootNode = objectMapper.readTree(buf);
                JsonNode msg = rootNode.path("Msg");
                JsonNode ret = rootNode.path("Ret");
                if (msg.toString().equals("")) {
                    String retstring = ret.toString();
                    if ("0".equals(retstring)) {
                        ((ObjectNode) rootNode).put("Msg", "Success");
                    } else {
                        ((ObjectNode) rootNode).put("Msg", "Failure");
                    }
                }
                JsonNode dataNode = rootNode.path("Data");
                String rawData = dataNode.toString();
                data = Aes128Cbc.encrypt(rawData, dataSecret, dataSecretIV);
                ((ObjectNode) rootNode).put("Data", data);
            } catch (Exception e) {
                rootNode = new ObjectNode(JsonNodeFactory.instance);
                ((ObjectNode) rootNode).put("Msg", e.getMessage());
            }
        } else {
            rootNode = new ObjectNode(JsonNodeFactory.instance);
        }
        String sig = HMAC.hmacDigest(rootNode.path("Ret").asText() + rootNode.path("Msg").asText() + data, sigSecret);
        ((ObjectNode) rootNode).put("Sig", sig);
//        log.debug("enc to out: " + rootNode);
        return rootNode;
    }

    private byte[] decrypt(HttpServletRequest request, AuthSecretToken authSecretToken,
                           CommonRequest commonRequest, String bodyString) throws IOException, BadPaddingException,
            InvalidAlgorithmParameterException, NoSuchAlgorithmException, IllegalBlockSizeException, NoSuchPaddingException,
            InvalidKeyException {

        byte[] buf;
        final String encin = request.getHeader("enc.in");
        if (request.getServletPath().endsWith("/query_token")) {
            String data;
            if (("false".equals(encin)) || commonRequest.getData() == null) {
                data = bodyString;
            } else {
                data = Aes128Cbc.decryptString(commonRequest.getData(), authSecretToken.getDataSecret(), authSecretToken
                        .getDataSecretIV());
            }
            buf = data.getBytes(StandardCharsets.UTF_8);
        } else {
            //decrypt Data field
            buf = bodyString.getBytes(StandardCharsets.UTF_8);
            ObjectMapper objectMapper = new ObjectMapper();
            JsonNode rootNode = objectMapper.readTree(buf);
            JsonNode sigNode = rootNode.path("Sig");
            JsonNode operatorIDNode = rootNode.path("OperatorID");
            JsonNode dataNode = rootNode.path("Data");
            JsonNode timestampNode = rootNode.path("TimeStamp");
            JsonNode seqNode = rootNode.path("Seq");
            if (!dataNode.isNull()) {
                if (!dataNode.asText().startsWith("{")) {
                    String computedSig = HMAC.hmacDigest(
                            operatorIDNode.asText().concat(dataNode.asText()).concat(timestampNode.asText()).concat(seqNode.asText()),
                            authSecretToken.getSigSecret());
                    if (("false".equals(encin)) && !computedSig.equals(sigNode.asText())) {
                        throw new InvalidAlgorithmParameterException("Illegal Sig, computed: ".concat(computedSig));
                    }
                }
                String rawData = dataNode.asText();
                String decryptedData = rawData;
                if ((encin == null || encin.equals("true")) || authSecretToken.isEncrypt()) {
                    decryptedData = Aes128Cbc.decryptString(rawData, authSecretToken.getDataSecret(),
                            authSecretToken.getDataSecretIV());
                    ((ObjectNode) rootNode).put("Data", decryptedData);
                }
                buf = decryptedData.getBytes();
            }
        }
        return buf;
    }

    @Override
    public void destroy() {

    }



    private static CommonRequest<EquipBizRequest> structure() {
        CommonRequest<EquipBizRequest> commonRequest = new CommonRequest<>();
        try {
            EquipBizRequest equipBizRequest = new EquipBizRequest();
            equipBizRequest.setEquipBizSeq("MA25CNM3800000");
            equipBizRequest.setConnectorId("8083600001000801");

            String data = JSONUtil.toJSONString(equipBizRequest);
            //System.out.println("========data========"+data);


            commonRequest.setData(data);
            commonRequest.setOperatorId("MA25CNM38");
        } catch (Exception e) {

        }
        return commonRequest;
    }

    public static void main(String[] args) {
        try{

            CommonRequest commonRequest = structure();
            String s = encryptReqOut("8LpncubmWiPCzY3V", "av6A8QdnRaVRMXu6","Ut5UFdqDthiJyncU", commonRequest);
            System.out.println("s================"+s);

            String servletPath="/v40/query_token";
            if(servletPath.endsWith("/query_token")){
                System.out.println("77777");
            }else{
                System.out.println("2222");
            }
        }catch (Exception e){

        }

    }
}
