package quant.rich.emoney.entity.config;

import java.net.Proxy;
import java.time.LocalDateTime;
import java.util.Objects;

import org.apache.commons.lang3.ObjectUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.Validate;
import org.springframework.beans.factory.annotation.Autowired;

import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.fasterxml.jackson.databind.node.ObjectNode;

import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer;
import lombok.AccessLevel;
import lombok.Data;
import lombok.Getter;
import lombok.Setter;
import lombok.experimental.Accessors;
import lombok.extern.slf4j.Slf4j;
import quant.rich.emoney.entity.config.DeviceInfoConfig.DeviceInfo;
import quant.rich.emoney.interfaces.ConfigInfo;
import quant.rich.emoney.interfaces.IConfig;
import quant.rich.emoney.interfaces.ValidEmoneyRequestConfig;
import quant.rich.emoney.patch.okhttp.PatchOkHttp;
import quant.rich.emoney.util.EncryptUtils;
import quant.rich.emoney.util.SpringContextHolder;
import quant.rich.emoney.util.TextUtils;

/**
 * 用于配置请求时的请求行为，一般而言，请求头与安卓系统的信息有关（build.prop）
 * 虽然部分请求对应服务器可能不进行审核，但合理的请求头能尽可能模仿真机行为，避免风险
 * @see DeviceInfoConfig
 * @see AndroidSdkLevelConfig
 * @see ChromeVersionsConfig
 */
@Data
@Accessors(chain = true)
@Slf4j
@ValidEmoneyRequestConfig
@ConfigInfo(field = "emoneyRequest", name = "益盟请求设置", initDefault = true)
public class EmoneyRequestConfig implements IConfig<EmoneyRequestConfig> {
    
    /**
     * 是否匿名登录
     */
    private Boolean isAnonymous = true;
    
    /**
     * 非匿名登录时的用户名
     */
    private String username = "";
    
    /**
     * 非匿名登录时的密码
     */
    private String password = "";
    
    /**
     * 鉴权信息
     */
    private String authorization;
    
    /**
     * 当前 authorization 更新时间
     */
    @JsonSerialize(using = LocalDateTimeSerializer.class)
    @JsonDeserialize(using = LocalDateTimeDeserializer.class)
    private LocalDateTime authorizationUpdateTime;
    
    /**
     * <b>用于：</b><ul>
     * <li>益盟登录接口 <code><i>guid</i> = MD5(<b>androidId</b>)</code></li>
     * <li>益盟登录接口 <code><i>exIdentify.AndroidID</i> = <b>androidId</b></code></li>
     * </ul>
     * <b>来源：</b><br>本例随机生成并管理，需要符合 16 位
     * 
     */
    private String androidId = TextUtils.randomString("abcdef0123456789", 16);
    
    /**
     * <b>用于：</b><ul>
     * <li>Webview <code><i>User-Agent</i></li>
     * <li>Non-Webview Image <code><i>User-Agent</i></code></li>
     * </ul>
     * <b>来源：</b><code><b>DeviceInfoConfig</b></code>
     * @see DeviceInfoConfig
     */
    @Setter(AccessLevel.PRIVATE)
    private String androidVersion;
    
    /**
     * <b>用于：</b><ul>
     * <li>益盟通讯接口请求头 <code><i>X-Android-Agent</i> = EMAPP/{<b>emoneyVersion</b>}(Android;{<b>androidSdkLevel</b>})</code></li>
     * <li>益盟登录接口 <code><i>osVersion</i> = <b>androidSdkLevel</b></code></li>
     * </ul>
     * <b>来源：</b><code><b>DeviceInfoConfig</b>, 经由 <code><b>AndroidSdkLevelConfig</b></code> 转换，由本例代管</code>
     * @see DeviceInfoConfig
     * @see AndroidSdkLevelConfig
     */
    @Setter(AccessLevel.PRIVATE)
    private String androidSdkLevel;
    
    /**
     * <b>用于：</b><ul>
     * <li>益盟登录接口 <code><i>softwareType</i> = <b>softwareType</b></code></li>
     * </ul>
     * <b>来源：</b><code><b>DeviceInfoConfig</b>，由本例代管</code>
     * @see DeviceInfoConfig
     */
    private String softwareType;

    /**
     * <b>用于：</b><ul>
     * <li>益盟通讯接口请求头 <code><i>User-Agent</i> = <b>okHttpUserAgent</b></code></li>
     * </ul>
     * 一般由程序所使用的 OkHttp 版本决定<br>
     * <b>来源：</b>本例管理
     */
    private String okHttpUserAgent = "okhttp/3.12.2";
    
    /**
     * 对应 build.prop 中 Build.MODEL, <b>用于：</b><ul>
     * <li>WebView <code><i>User-Agent</i></code></li>
     * <li>非 WebView 图片<code><i>User-Agent</i></code></li>
     * </ul>
     * <b>来源：</b><code><b>DeviceInfoConfig</b>, 由本例代为管理
     * @see DeviceInfoConfig
     */
    private String deviceName;
    
    /**
     * 对应 build.prop 中 Build.FINGERPRINT, <b>用于：</b><ul>
     * <li>益盟登录接口 <code><i>hardware</i> = MD5(<b>fingerprint</b>)</code></li>
     * <li>益盟登录接口 <code><i>exIdentify.OSFingerPrint</i> = <b>fingerprint</b></code></li>
     * </ul>
     * <font color="red">注意最终生成的 exIdentify 是 jsonString, 且有对斜杠的转义</font><br>
     * <b>来源：</b><code><b>DeviceInfoConfig</b>, 由本例代为管理
     * @see DeviceInfoConfig
     * 
     */
    private String fingerprint;
    
    
    /**
     * 对应 build.prop 中 Build.ID, <b>用于：</b><ul>
     * <li>WebView <code><i>User-Agent</i></code></li>
     * <li>非 WebView 图片<code><i>User-Agent</i></code></li>
     * </ul>
     * <b>来源：</b><code><b>DeviceInfoConfig</b>, 由本例代为管理
     * @see DeviceInfoConfig
     * 
     */
    private String buildId;

    /**
     * <b>用于：</b><ul>
     * <li>WebView <code><i>User-Agent</i></code></li>
     * </ul>
     * <b>来源：</b><code><b>ChromeVersionsConfig</b>, 由本例代为管理
     * @see ChromeVersionsConfig
     */
    private String chromeVersion;

    /**
     * <b>用于：</b><ul>
     * <li>益盟通讯接口请求头 <code><i>X-Android-Agent</i> =
     * EMAPP/{<b>emoneyVersion</b>}(Android;{androidSdkLevel})</code></li>
     * </ul>
     * 由程序版本决定<br>
     * <b>来源：</b>本例管理
     * @see EmoneyRequestConfig.androidSdkLevel
     */
    private String emoneyVersion = "5.8.1";
    
    /**
     * <b>用于：</b><ul>
     * <li>益盟通讯接口请求头 <code><i>Emapp-ViewMode</i> = <b>emappViewMode</b></code></li>
     * </ul>
     * 由程序决定, 一般默认为 "1"<br>
     * <b>来源：</b>本例管理
     */
    private String emappViewMode = "1";

    @Getter(AccessLevel.PRIVATE)
    @Autowired
    private AndroidSdkLevelConfig androidSdkLevelConfig;

    @Getter(AccessLevel.PRIVATE)
    @Autowired
    private DeviceInfoConfig deviceInfoConfig;
    
    @Getter(AccessLevel.PRIVATE)
    @Autowired
    private ChromeVersionsConfig chromeVersionsConfig;
    
    public void afterBeanInit() {
        
        try {
            androidSdkLevelConfig = Objects.requireNonNullElseGet(androidSdkLevelConfig, () -> SpringContextHolder.getBean(AndroidSdkLevelConfig.class));
            deviceInfoConfig = Objects.requireNonNullElseGet(deviceInfoConfig, () -> SpringContextHolder.getBean(DeviceInfoConfig.class));
            chromeVersionsConfig = Objects.requireNonNullElseGet(chromeVersionsConfig, () -> SpringContextHolder.getBean(ChromeVersionsConfig.class));
        }
        catch (IllegalStateException e) {
            log.debug("SpringContext not ready");
        }
        
        if (ObjectUtils.anyNull(fingerprint, buildId, deviceName, androidVersion, androidSdkLevel, softwareType)) {
            // 任意是 null 的都要统一由 deviceInfo 进行设置
            initFromRandomDeviceInfo();
        }
        else {
            // 都不是 null，则由 fingerprint 来检查各项
            // model 和 softwareType 本应交由 deviceInfoConfig 检查以
            // 应对可能的通过修改本地 json 来进行攻击的方式，可是本身
            // deviceInfoConfig 对 model 和 softwareType 的信息也来源
            // 于本地，万一本地的 deviceInfo.(fallback.)json 也不值得信任？
            // 所以只检查 fingerprint
            
            DeviceInfo deviceInfo;
            boolean valid = true;
            try {
                deviceInfo = DeviceInfo.from(null, fingerprint);
                Validate.validState(androidVersion.equals(
                        deviceInfo.getVersionRelease()),
                        "androidVersion(versionRelease) doesn't match");
                Validate.validState(androidSdkLevel.equals(
                        String.valueOf(androidSdkLevelConfig.getSdkLevel(deviceInfo.getVersionRelease()))),
                        "androidSdkLevel doesn't match");
                Validate.validState(buildId.equals(deviceInfo.getBuildId()),
                        "buildId doesn't match");
            }
            catch (Exception e) {
                valid = false;
            }
            if (!valid) {
                initFromRandomDeviceInfo();
            }
        }
        
        if (chromeVersion == null) {
            chromeVersion = chromeVersionsConfig.getRandomChromeVersion();
        }
        
        // 注入 OkHttp
        PatchOkHttp.apply(
            r -> r
            .hostEndsWith("emoney.cn")
            .or(a -> a.hostContains("emapp"))
            .or(b -> b.hasHeaderName("X-Protocol-Id"))
            .overrideIf("User-Agent", getOkHttpUserAgent()));
    }
    
    /**
     * 从随机 deviceInfo 填充本例相关字段
     * @return
     */
    private EmoneyRequestConfig initFromRandomDeviceInfo() {
        DeviceInfo deviceInfo = deviceInfoConfig.getRandomDeviceInfo();
        // 更新 deviceInfo 后对应 androidId 也要修改，哪怕原来非空
        androidId = TextUtils.randomString("abcdef0123456789", 16);
        return initFromDeviceInfo(deviceInfo);
    }
    
    /**
     * 从指定 deviceInfo 填充本例相关字段
     * @param deviceInfo
     * @return
     */
    private EmoneyRequestConfig initFromDeviceInfo(DeviceInfo deviceInfo) {
        if (deviceInfo == null) {
            log.error("deviceInfo is null");
            RuntimeException e = new RuntimeException("deviceInfo is null");
            e.printStackTrace();
            throw e;
        }
        deviceName = deviceInfo.getModel();
        androidVersion = deviceInfo.getVersionRelease();
        androidSdkLevel = String.valueOf(androidSdkLevelConfig.getSdkLevel(androidVersion));
        softwareType = deviceInfo.getDeviceType();
        fingerprint = deviceInfo.getFingerprint();
        buildId = deviceInfo.getBuildId();
        return this;
    }
    
    public EmoneyRequestConfig() {}
    
    public EmoneyRequestConfig setFingerprint(String fingerprint) {
        // 进入前即便 androidSdkLevelConfig 为 null 也要尝试获取一下
        // 因为为 null 时不一定是程序初始化时，也有可能是从前端 Post 而来的
        try {
            androidSdkLevelConfig = Objects.requireNonNullElseGet(androidSdkLevelConfig, () -> SpringContextHolder.getBean(AndroidSdkLevelConfig.class));
        }
        catch (IllegalStateException e) {
            log.debug("SpringContext not ready");
        }
        
        if (ObjectUtils.allNotNull(deviceName, softwareType, fingerprint, androidSdkLevelConfig)) {
            DeviceInfo deviceInfo = DeviceInfo.from(deviceName, fingerprint, softwareType);
            initFromDeviceInfo(deviceInfo);
        }
        else {
            this.fingerprint = fingerprint;
        }
        return this;
    }
    
    /**
     * 根据当前配置获取 guid，用于益盟登录接口
     * @return
     */
    @JsonIgnore
    public String getGuid() {
        return EncryptUtils.toMD5String(androidId);
    }
    
    /**
     * 一般 Protobuf 请求 X-Android-Agent 头，由 emoneyVersion 和 androidSdkLevel 组成
     * @return
     */
    @JsonIgnore
    public String getXAndroidAgent() {
        // EMAPP/{emoneyVersion}(Android;{androidSdkLevel})
        return
            new StringBuilder()
            .append("EMAPP/")
            .append(getEmoneyVersion())
            .append("(Android;")
            .append(getAndroidSdkLevel())
            .append(")").toString();
    }

    /**
     * 用于 App 内用到 Webview 的地方
     * @return
     */
    @JsonIgnore
    public String getWebviewUserAgent() {
        return new StringBuilder()
                .append("Mozilla/5.0 (Linux; Android ")
                .append(getAndroidVersion())
                .append("; ")
                .append(getDeviceName())
                .append(" Build/")
                .append(getBuildId())
                .append("; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/")
                .append(getChromeVersion())
                .append(" Mobile Safari/537.36")
                .toString();
    }
    
    /**
     * 用于 App 内少量未用到 Webview 的地方，如首页获取图片等
     * @return
     */
    @JsonIgnore
    public String getNonWebviewResourceUserAgent() {
        // Dalvik/2.1.0 (Linux; U; Android {安卓版本};{Build.DEVICE} Build/{Build.ID})
        return new StringBuilder()
                .append("Dalvik/2.1.0 (Linux; U; Android ")
                .append(getAndroidVersion())
                .append(";")
                .append(getDeviceName())
                .append(" Build/")
                .append(getBuildId())
                .append(")")
                .toString();
    }
    /**
     * 根据当前配置获取 hardware，用于益盟登录接口
     * @return
     */
    @JsonIgnore
    public String getHardware() {
        return EncryptUtils.toMD5String(getFingerprint());
    }
    
    /**
     * 根据本例信息（包括保存的用户名和密码）生成一个用于登录的 ObjectNode
     * @return
     */
    public ObjectNode getUsernamePasswordLoginObject() {
        return getUsernamePasswordLoginObject(username, password);
    }
    
    /**
     * 根据指定用户名、密码和本例信息生成一个用于登录的 ObjectNode
     * @param username 用户名
     * @param password 密码（可以是加密过的，也可以是明文）
     * @return
     */
    public ObjectNode getUsernamePasswordLoginObject(String username, String password) {
       
        if (StringUtils.isAnyBlank(username, password)) {
            throw new RuntimeException("Try to generate a emoney login object but username and/or password is blank");
        }
        
        ObjectNode node = getAnonymousLoginObject();
        node.put("accId", username);
        node.put("accType", 1);
        
        // 尝试解密 password 看是否成功，如果成功说明原本就已经是加密了的
        String tryDecryptPassword = EncryptUtils.decryptAesForEmoneyPassword(password);
        
        node.put("pwd", 
            tryDecryptPassword != null ? password :
                EncryptUtils.encryptAesForEmoneyPassword(password)
        );
        
        return node;
    }
    
    /**
     * 根据本例信息生成一个用于匿名登录的 ObjectNode
     * @return
     */
    @JsonIgnore
    public ObjectNode getAnonymousLoginObject() {
        ObjectMapper mapper = new ObjectMapper();
        ObjectNode node = mapper.createObjectNode();
        ObjectNode exIdentify = mapper.createObjectNode();
        exIdentify.put("IMEI", "");
        exIdentify.put("AndroidID", getAndroidId());
        exIdentify.put("MAC", "");
        exIdentify.put("OSFingerPrint", getFingerprint());
        String exIdentifyString = exIdentify.toString().replace("/", "\\/");
        
        // 这玩意最好按照顺序来，当前这个顺序是 5.8.1 的顺序
        String guid = getGuid();
        node.put("appVersion", getEmoneyVersion());
        node.put("productId", 4);
        node.put("softwareType", getSoftwareType());
        node.put("deviceName", getDeviceName());
        node.put("ssid", "0");
        node.put("platform", "android");
        node.put("exIdentify", exIdentifyString);
        node.put("osVersion", getAndroidSdkLevel());
        node.put("accId", guid);
        node.put("guid", guid);
        node.put("accType", 4);
        node.put("pwd", "");
        node.put("channelId", "1711");
        node.put("hardware", getHardware());

        return node;
    }
    
    /**
     * 设置密码：<ul>
     * <li>null or empty，保存空字符串</li>
     * <li>尝试解密成功，说明是密文，直接保存</li>
     * <li>尝试解密失败，说明是明文，加密保存</li>
     * </ul>
     * @param password
     * @return
     */
    public EmoneyRequestConfig setPassword(String password) {
        if (StringUtils.isEmpty(password)) {
            this.password = "";
            return this;
        }
        String tryDecryptPassword = EncryptUtils.decryptAesForEmoneyPassword(password);
        if (tryDecryptPassword != null) {
            this.password = password;
        }
        else {
            this.password = EncryptUtils.encryptAesForEmoneyPassword(password);
        }
        return this;
    }
    
    /**
     * 确保 androidVersion/androidSdkLevel 不为 null
     */
    public EmoneyRequestConfig beforeSaving() {
        setFingerprint(this.fingerprint);
        return this;
    }
    
    /**
     * 触发更新 Authorization 的更新时间为现在
     * @return
     */
    public EmoneyRequestConfig updateAuthorizationTime() {
        this.authorizationUpdateTime = LocalDateTime.now();
        return this;
    }
    
    public EmoneyRequestConfig mergeTo(EmoneyRequestConfig other) {
        boolean authorizationUpdated = !Objects.equals(other.authorization, this.authorization);
        IConfig.super.mergeTo(other);
        if (authorizationUpdated) {
            other.updateAuthorizationTime();
        }
        return other;
    }
    
    public EmoneyRequestConfig mergeFrom(EmoneyRequestConfig other) {
        boolean authorizationUpdated = !Objects.equals(other.authorization, this.authorization);
        IConfig.super.mergeFrom(other);
        if (authorizationUpdated) {
            this.updateAuthorizationTime();
        }
        return this;
    }
    
}
