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.config.EmoneyRequestConfig;
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.util.EncryptUtils;
import quant.rich.emoney.util.SpringContextHolder;
import okhttp3.OkHttpClient;

@Data
@Slf4j
@Accessors(chain = true)
public class EmoneyClient implements Cloneable {

    private static final String MBS_URL = "https://mbs.emoney.cn/";
    private static final String LOGIN_URL = "https://emapp.emoney.cn/user/auth/login";
    // private static final String LOGIN_URL = "http://localhost:7790/user/auth/login";
    private static final String LOGIN_X_PROTOCOL_ID = "user%2Fauth%2Flogin";
    
    private static volatile EmoneyRequestConfig emoneyRequestConfig;

    private static EmoneyRequestConfig getEmoneyRequestConfig() {
        if (emoneyRequestConfig == null) {
            synchronized (EmoneyClient.class) {
                if (emoneyRequestConfig == null) {
                    emoneyRequestConfig = SpringContextHolder.getBean(EmoneyRequestConfig.class);
                }
            }
        }
        return emoneyRequestConfig;
    }
    
    private EmoneyClient() {}
    
    /**
     * 根据系统配置自动选择登录方式
     * @return
     * @see EmoneyRequestConfig
     */
    public static Boolean loginWithManaged() {
        if (getEmoneyRequestConfig().getIsAnonymous()) {
            return loginWithAnonymous();
        }
        else {
            return loginWithUsernamePassword();
        }
    }
    

    /**
     * 使用系统管理的用户名密码登录
     * @return
     */
    public static Boolean loginWithUsernamePassword() {
        ObjectNode formObject = getEmoneyRequestConfig().getUsernamePasswordLoginObject();
        return login(formObject);
    }
    
    /**
     * 使用给定的用户名密码登录
     * @param username 用户名
     * @param password 密码，可以是明文，也可是密文
     * @return
     */
    public static Boolean loginWithUsernamePassword(String username, String password) {
        ObjectNode formObject = getEmoneyRequestConfig().getUsernamePasswordLoginObject(username, password);
        return login(formObject);
    }
    
    /**
     * 匿名登录
     * @return
     */
    public static Boolean loginWithAnonymous() {
        ObjectNode formObject = getEmoneyRequestConfig().getAnonymousLoginObject();
        return login(formObject);
    }
    
    /**
     * 登录总方法，唯一要控制的只有 formObject
     * @param formObject
     * @return
     */
    private static Boolean login(ObjectNode formObject) {
        try {
            //OkHttpClient okHttpClient = new OkHttpClient();
            
            OkHttpClient okHttpClient = OkHttpClientProvider.getInstance();
            MediaType type = MediaType.parse("application/json");
            //type.charset(StandardCharsets.UTF_8);
            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", getEmoneyRequestConfig().getXAndroidAgent())
                .header("Emapp-ViewMode", getEmoneyRequestConfig().getEmappViewMode());
                //.header("User-Agent", getEmoneyRequestConfig().getOkHttpUserAgent())
            
            Request request = requestBuilder.build();
            
            final Call call = okHttpClient.newCall(request);
            Response response = call.execute();
            ObjectNode loginResult = (ObjectNode) new ObjectMapper().readTree(response.body().string());
            
            Integer code = loginResult.get("result").get("code").asInt();
            
            if (code == 0) {
                getEmoneyRequestConfig().setAuthorization(
                        loginResult.get("detail").get("token").asText())
                .updateAuthorizationTime().saveOrUpdate();
                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
     * @return
     */
    public 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());
        }
        if (StringUtils.isBlank(getEmoneyRequestConfig().getAuthorization())) {
            throw new EmoneyIllegalRequestParamException("执行 emoney 请求错误，Authorization 为空，是否未登录？",
                    new IllegalArgumentException());
        }

        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(MBS_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", getEmoneyRequestConfig().getAuthorization())
                .header("X-Android-Agent", getEmoneyRequestConfig().getXAndroidAgent())
                .header("Emapp-ViewMode", getEmoneyRequestConfig().getEmappViewMode())
                ;

            Request request = requestBuilder.build();

            final Call call = okHttpClient.newCall(request);
            Response response = call.execute();
            
            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);
        }
    }

    /**
     * 根据指定 clazz 获取返回
     * 
     * @param <T>
     * @param <U>
     * @param nanoRequest
     * @param clazz
     * @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 请求成功");
                    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);
        }
    }

}
