package quant.rich.emoney.client;

import java.io.Serializable;
import com.baomidou.mybatisplus.core.toolkit.StringUtils;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.google.protobuf.nano.InvalidProtocolBufferNanoException;
import com.google.protobuf.nano.MessageNano;

import lombok.Data;
import lombok.experimental.Accessors;
import lombok.extern.slf4j.Slf4j;
import nano.BaseResponse;
import okhttp3.Call;
import okhttp3.MediaType;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
import quant.rich.emoney.entity.sqlite.RequestInfo;
import quant.rich.emoney.exception.EmoneyDecodeException;
import quant.rich.emoney.exception.EmoneyIllegalRequestParamException;
import quant.rich.emoney.exception.EmoneyRequestException;
import quant.rich.emoney.exception.EmoneyResponseException;
import quant.rich.emoney.service.RequestResponseInspectService;
import quant.rich.emoney.service.sqlite.RequestInfoService;
import quant.rich.emoney.util.EncryptUtils;
import quant.rich.emoney.util.SpringContextHolder;
import okhttp3.OkHttpClient;

/**
 * 益盟操盘手基本请求客户端，提供基本功能，如登录和请求。
 * <p>本类一般只提供静态方法，故不能也不该实例化本类。具体请求内容需要自己封装。整个系统共用一套鉴权，所以不要复制本例
 * <p><b>请求头顺序</b></p>
 * <p>
 * <ul>
 * <li>X-Protocol-Id</li>
 * <li>X-Request-Id</li>
 * <li>EM-Sign</li>
 * <li>Authorization</li>
 * <li>X-Android-Agent</li>
 * <li>Emapp-ViewMode</li>
 * <li>Content-Type</li>
 * <li>Content-Length</li>
 * <li>Host</li>
 * <li>Connection: "Keep-Alive"</li>
 * <li>Accept-Encoding: "gzip"</li>
 * <li>User-Agent</li>
 * </ul>
 * </p>
 *  <p>从 X-Protocol-Id 到 Emapp-ViewMode 由本例添加，剩余为 okhttp 默认添加，
 *  User-Agent 由 ByteBuddy 重写 header 方法控制添加</p>
 *  @see quant.rich.emoney.patch.okhttp.PatchOkHttp
 */
@Data
@Slf4j
@Accessors(chain = true)
public class EmoneyClient {

    private static final String MBS_URL = "https://mbs.emoney.cn/";
    private static final String STRATEGY_URL = "https://mbs.emoney.cn/strategy/";
    private static final String LOGIN_URL = "https://emapp.emoney.cn/user/auth/login";
    private static final String RELOGIN_URL = "https://emapp.emoney.cn/user/auth/ReLogin";
    private static final String STRATEGY_X_PROTOCOL_ID = "9400";
    private static final String LOGIN_X_PROTOCOL_ID = "user%2Fauth%2Flogin";
    private static final String RELOGIN_X_PROTOCOL_ID = "user%2Fauth%2FReLogin";
    
    private static volatile RequestInfoService requestInfoService;
    private static volatile RequestResponseInspectService requestResponseInspectService;
    
    /**
     * 根据 protocolId 返回 URL
     * <p>益盟操盘手对于不同的 protocolId 有不同的 URL 负责。在进行某类请求之前，请先通过调试 APP 进行确认，否则可能无法获取到相应内容
     * 
     * @param protocolId
     * @return 对应的 URL，当所给 protocolId 为 null 或非 String/Integer 类型时，返回为 null，使用前需要检查
     */
    private static String getUrlByProtocolId(Serializable protocolId) {
        String strProtocolId;
        if (protocolId instanceof Integer intProtocolId) {
            strProtocolId = String.valueOf(intProtocolId);
        }
        else if (protocolId instanceof String s) {
            strProtocolId = s;
        }
        else {
            return null;
        }
        switch (strProtocolId) {
            case STRATEGY_X_PROTOCOL_ID: return STRATEGY_URL;
            case LOGIN_X_PROTOCOL_ID: return LOGIN_URL;
            case RELOGIN_X_PROTOCOL_ID: return RELOGIN_URL;
            default: return MBS_URL;
        }
    }

    /**
     * 从 Spring 上下文中获取由 RequestInfoService 管理的默认请求配置
     * @see RequestInfo
     * @see RequestInfoService
     * @return
     */
    private static RequestInfo getDefaultRequestInfo() {
        if (requestInfoService == null) {
            synchronized (EmoneyClient.class) {
                requestInfoService = SpringContextHolder.getBean(RequestInfoService.class);
            }
            if (requestInfoService == null) {
                log.warn("获取 RequestInfoService 实例失败");
                return null;
            }
        }
        return requestInfoService.getDefaultRequestInfo();
    }
    
    /**
     * 从 Spring 上下文中获取 RequestResponseInspectService
     * @see RequestResponseInspectService
     * @return
     */
    private static RequestResponseInspectService getRequestResponseInspectService() {
        if (requestResponseInspectService == null) {
            synchronized (EmoneyClient.class) {
                requestResponseInspectService = SpringContextHolder.getBean(RequestResponseInspectService.class);
            }
            if (requestResponseInspectService == null) {
                log.warn("获取 RequestResponseInspectService 实例失败");
                return null;
            }
        }
        return requestResponseInspectService;
    }
    
    /**
     * 不允许外部实例化对象
     */
    private EmoneyClient() {}
    
    /**
     * 根据系统配置自动选择登录方式，即匿名或不匿名
     * @return
     * @see RequestInfo
     */
    public static Boolean loginWithManaged() {
        if (getDefaultRequestInfo().isAnonymous()) {
            return loginWithAnonymous();
        }
        else {
            return loginWithUsernamePassword();
        }
    }
    
    /**
     * 使用系统管理的用户名密码登录
     * <p>建议仅在调试时使用，其他情况请用 {@code loginWithManaged()}</p>
     * @return
     */
    @Deprecated
    public static Boolean loginWithUsernamePassword() {
        ObjectNode formObject = getDefaultRequestInfo().getUsernamePasswordLoginObject();
        return login(formObject);
    }
    
    /**
     * 使用给定的用户名密码登录
     * <p>建议仅在调试时使用，其他情况请用 {@code loginWithManaged()}</p>
     * @param username 用户名
     * @param password 密码，可以是明文，也可是密文
     * @return
     */
    @Deprecated
    public static Boolean loginWithUsernamePassword(String username, String password) {
        ObjectNode formObject = getDefaultRequestInfo().getUsernamePasswordLoginObject(username, password);
        return login(formObject);
    }
    
    /**
     * 匿名登录
     * <p>建议仅在调试时使用，其他情况请用 {@code loginWithManaged()}</p>
     * @return
     */
    @Deprecated
    public static Boolean loginWithAnonymous() {
        ObjectNode formObject = getDefaultRequestInfo().getAnonymousLoginObject();
        return login(formObject);
    }
    
    /**
     * 触发重登陆验证
     * @return
     */
    public static Boolean reloginCheck() {
       RequestInfo requestInfo = getDefaultRequestInfo();
       ObjectNode reloginObject = requestInfo.getReloginObject();
       if (reloginObject == null) {
           // 无登录信息，直接触发登录
           return loginWithManaged();
       }
       String token = reloginObject.get("token").asText();
       try {
           OkHttpClient okHttpClient = OkHttpClientProvider.getInstance();
           MediaType type = MediaType.parse("application/json");
           byte[] content = reloginObject.toString().getBytes("utf-8");
           RequestBody body = RequestBody.create(
                   content, type);
           
           Request.Builder requestBuilder = new Request.Builder()
               .url(RELOGIN_URL)
               .post(body)
               .header("X-Protocol-Id", RELOGIN_X_PROTOCOL_ID)
               .header("X-Request-Id", "1")
               .header("EM-Sign", EncryptUtils.getEMSign(content, "POST", RELOGIN_X_PROTOCOL_ID))
               .header("Authorization", token)
               .header("X-Android-Agent", requestInfo.getXAndroidAgent())
               .header("Emapp-ViewMode", requestInfo.getEmappViewMode());
           
           Request request = requestBuilder.build();
           
           final Call call = okHttpClient.newCall(request);
           Response response = call.execute();
           
           if (response.code() != 200) {
               // 不是 200，重新登录
               log.debug("ReLogin 重登录验证返回状态码 {}, 需要重新登录", response.code());
               return loginWithManaged();
           }
           
           String responseText = response.body().string();
           ObjectNode loginResult = (ObjectNode) new ObjectMapper().readTree(responseText);
            
           Integer code = loginResult.get("result").get("code").asInt();
           
           if (code != 0 || 
                   !token.equals(loginResult.get("detail").get("token").asText())) {
               // 我也不知道会不会进入这里，不过尽可能写完所有可能
               log.debug("ReLogin 返回消息 result.code = {}, token 与已有 authorization {}一致, 触发重登陆",
                       code, code == 0 ? "不" : "");
               return loginWithManaged();
           }
           else {
               log.debug("ReLogin 校验通过，不需要重登录");
               return true;
           }
       }
       catch (JsonProcessingException e) {
           throw new EmoneyDecodeException("ReLogin 试图将返回数据解析成 JSON 时失败", e);
       }
       catch (Exception e) {
           throw new EmoneyRequestException("执行 emoney ReLogin 请求/返回时出现错误", e);
       }
    }
    
    /**
     * 登录总方法，唯一要控制的只有 formObject
     * @param formObject
     * @return
     */
    private static Boolean login(ObjectNode formObject) {
        try {
            RequestInfo requestInfo = getDefaultRequestInfo();
            OkHttpClient okHttpClient = OkHttpClientProvider.getInstance();
            MediaType type = MediaType.parse("application/json");
            byte[] content = formObject.toString().getBytes("utf-8");
            RequestBody body = RequestBody.create(
                    content, type);
            
            Request.Builder requestBuilder = new Request.Builder()
                .url(LOGIN_URL)
                .post(body)
                // 这玩意可能也有顺序
                // 按照 Fiddler HexView 顺序如下：
                // X-Protocol-Id > X-Request-Id > EM-Sign > Authorization >
                // X-Android-Agent > Emapp-ViewMode > Content-Type > Content-Length >
                // Host > Connection: "Keep-Alive" > Accept-Encoding: "gzip" > User-Agent
                .header("X-Protocol-Id", LOGIN_X_PROTOCOL_ID)
                .header("X-Request-Id", "null")
                .header("EM-Sign", EncryptUtils.getEMSign(content, "POST", LOGIN_X_PROTOCOL_ID))
                .header("Authorization", "")
                .header("X-Android-Agent", requestInfo.getXAndroidAgent())
                .header("Emapp-ViewMode", requestInfo.getEmappViewMode());
                //此处 User-Agent 由 ByteBuddy 拦截修改
            
            Request request = requestBuilder.build();
            
            final Call call = okHttpClient.newCall(request);
            Response response = call.execute();
            String responseText = response.body().string();
            ObjectNode loginResult = (ObjectNode) new ObjectMapper().readTree(responseText);
            
            Integer code = loginResult.get("result").get("code").asInt();
            
            if (code == 0) {
                requestInfo
                    .setAuthorization(
                        loginResult
                            .get("detail").get("token").asText())
                    .setUid(
                            loginResult
                                .get("detail").get("uid").asInt())
                    .insertOrUpdate();
                log.info("执行 emoney LOGIN 成功");
                return true;
            }
            else {
                String msg = loginResult.get("result").get("msg").asText();
                throw new EmoneyResponseException("执行 emoney LOGIN 请求返回错误，code: " + code + ", msg: " + msg, code);
            }
        }
        catch (JsonProcessingException e) {
            throw new EmoneyDecodeException("试图将返回数据解析成 JSON 时失败", e);
        }
        catch (Exception e) {
            throw new EmoneyRequestException("执行 emoney LOGIN 请求/返回时出现错误", e);
        }
    }

    /**
     * 获取基本返回 Base_Response
     * 
     * @param <T> 请求类型泛型
     * @param nanoRequest 请求
     * @param xProtocolId 请求对应的 Protocol ID
     * @param xRequestId 视具体请求而定。可能是 null，也可能是 String.valueOf(System.currentTimeMillis())
     * @return
     */
    protected static <T extends MessageNano> BaseResponse.Base_Response post(
            T nanoRequest,
            Serializable xProtocolId,
            Serializable xRequestId) {

        if (xProtocolId == null) {
            throw new EmoneyIllegalRequestParamException("执行 emoney 请求错误，Protocol id 不能为 null!",
                new IllegalArgumentException());
        }
        RequestInfo requestInfo = getDefaultRequestInfo();
        if (StringUtils.isBlank(requestInfo.getAuthorization())) {
            throw new EmoneyIllegalRequestParamException("执行 emoney 请求错误，Authorization 为空，是否未登录？",
                    new IllegalArgumentException());
        }

        String url = getUrlByProtocolId(xProtocolId);
        if (StringUtils.isBlank(url)) {
            throw new EmoneyRequestException("无法根据 xProtocolId " + xProtocolId + "获取请求 URL");
        }
        
        try {
            OkHttpClient okHttpClient = OkHttpClientProvider.getInstance();
            
            byte[] content = MessageNano.toByteArray(nanoRequest);
            RequestBody body = RequestBody.create(
                    content,
                    MediaType.parse("application/x-protobuf-v3"));
            
            Request.Builder requestBuilder = new Request.Builder()
                .url(url)
                .post(body)
                // 这玩意可能也有顺序
                // 按照 Fiddler HexView 顺序如下：
                // X-Protocol-Id > X-Request-Id > EM-Sign > Authorization > X-Android-Agent > Emapp-ViewMode > Content-Type > Content-Length > Host ...
                .header("X-Protocol-Id", xProtocolId.toString())
                .header("X-Request-Id", xRequestId == null ? "null" : xRequestId.toString())
                .header("EM-Sign", EncryptUtils.getEMSign(content, "POST", xProtocolId.toString()))
                .header("Authorization", requestInfo.getAuthorization())
                .header("X-Android-Agent", requestInfo.getXAndroidAgent())
                .header("Emapp-ViewMode", requestInfo.getEmappViewMode());
                //此处 User-Agent 由 ByteBuddy 拦截修改

            Request request = requestBuilder.build();

            final Call call = okHttpClient.newCall(request);
            Response response = call.execute();
            
            // 错误的时候这里是 404，比如 strategy 是独立网页的时候，Content-Type 是 text/plain
            if (response.code() != 200) {
                throw new EmoneyRequestException("请求返回错误，状态码 " + response.code());
            }
            BaseResponse.Base_Response baseResponse = BaseResponse.Base_Response.parseFrom(response.body().bytes());
            return baseResponse;
        } catch (InvalidProtocolBufferNanoException e) {
            throw new EmoneyDecodeException("试图将返回数据解析成 Base_Response 时失败", e);
        } catch (Exception e) {
            throw new EmoneyRequestException("执行 emoney 请求/返回时出现错误", e);
        }
    }

    /**
     * 请求并返回
     * @param <T> 请求类型泛型
     * @param <U> 返回类型泛型
     * @param nanoRequest 请求
     * @param clazz 返回类型
     * @param xProtocolId 请求对应的 Protocol ID
     * @param xRequestId 视具体请求而定。可能是 null，也可能是 String.valueOf(System.currentTimeMillis())
     * @return
     */
    public static <T extends MessageNano, U extends MessageNano> U post(
            T nanoRequest, 
            Class<U> clazz, 
            Serializable xProtocolId, 
            Serializable xRequestId) {

        BaseResponse.Base_Response baseResponse;
        try {
            baseResponse = post(nanoRequest, xProtocolId, xRequestId);
            if (baseResponse.result.getCode() == 0) {
                try {
                    @SuppressWarnings("unchecked")
                    U nanoResponse = (U) MessageNano.mergeFrom(
                            (MessageNano) clazz.getDeclaredConstructor().newInstance(), baseResponse.detail.getValue());
                    log.debug("执行 emoney 请求成功");
                    try {
                        getRequestResponseInspectService().inspectResponse(xProtocolId.toString(), nanoResponse, true);
                    }
                    catch (Exception e) {
                        log.warn("执行 Inspect response 时发生错误, protocolId={}", xProtocolId, e);
                    }
                    return nanoResponse;
                } catch (Exception e) {
                    throw new EmoneyDecodeException("试图将返回数据解析成 " + clazz.getSimpleName() + " 时失败", e);
                }
            } else {
                Integer code = baseResponse.result.getCode();
                String msg = baseResponse.result.getMsg();
                throw new EmoneyResponseException("执行 emoney 请求返回错误，code: " + code + ", msg: " + msg, code);
            }
        } catch (Exception e) {
            throw new EmoneyRequestException("执行 emoney " + nanoRequest.getClass().getSimpleName() + " 请求/返回时出现错误", e);
        }
    }
    
}
