package com.miraclegarden.smsmessage.xposed.hook;

import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.nio.charset.StandardCharsets;

import de.robv.android.xposed.XC_MethodHook;
import de.robv.android.xposed.XposedBridge;
import de.robv.android.xposed.XposedHelpers;
import de.robv.android.xposed.callbacks.XC_LoadPackage;

/**
 * register body 加密前 Hook：{@code com.shopee.bke.lib.jni.utils.d} / {@code uvwuvwuv}。
 * 勿 Hook {@code utils.f}（SoUtils），否则会 libsdkutils 白屏。
 */
public final class MariBankSdkUtilsHook {

    private static final String TAG = "notiMessageHook/MariBankEncrypt";
    private static final String WRAPPER = "com.shopee.bke.lib.jni.utils.d";
    private static final String NATIVE_ENCRYPT = "com.shopee.bke.lib.jni.utils.uvwuvwuv";
    private static final String SG_ENCRYPT_HELPER = "com.shopee.bke.lib.jni.utils.uvwwwwuv";

    private static volatile boolean encryptHooksInstalled = false;
    private static volatile boolean gsonHookInstalled = false;
    private static volatile boolean encryptLoadClassWatcherInstalled = false;
    private static volatile String installedForPackage;

    private MariBankSdkUtilsHook() {
    }

    /** BkeApplication.onCreate 之后安装（libsdkutils 已加载）。 */
    public static int installLate(XC_LoadPackage.LoadPackageParam lpparam) {
        installEncryptLoadClassWatcher(lpparam);
        if (encryptHooksInstalled && lpparam.packageName.equals(installedForPackage)) {
            return 0;
        }
        int n = 0;
        if (MariBankRegionProfile.hasEncryptWrapperD(lpparam.packageName)) {
            n += hookEncryptWrapper(lpparam);
        }
        n += hookNativeEncryptUtils(lpparam);
        if (MariBankRegionProfile.hasEncryptHelperUvwwwwuv(lpparam.packageName)) {
            n += hookEncryptHelperClass(lpparam, SG_ENCRYPT_HELPER, "uvwwwwuv");
        }
        if (n > 0) {
            encryptHooksInstalled = true;
            installedForPackage = lpparam.packageName;
        }
        if (!gsonHookInstalled) {
            int g = hookGsonRegister(lpparam);
            if (g > 0) {
                gsonHookInstalled = true;
                n += g;
            }
        }
        if (n > 0) {
            XposedBridge.log(TAG + " late encrypt hooks=" + n + " region="
                    + MariBankRegionProfile.label(lpparam.packageName));
        }
        return n;
    }

    private static void installEncryptLoadClassWatcher(XC_LoadPackage.LoadPackageParam lpparam) {
        if (encryptLoadClassWatcherInstalled) {
            return;
        }
        encryptLoadClassWatcherInstalled = true;
        final String pkg = lpparam.packageName;
        try {
            XposedHelpers.findAndHookMethod(
                    ClassLoader.class,
                    "loadClass",
                    String.class,
                    boolean.class,
                    new XC_MethodHook() {
                        @Override
                        protected void afterHookedMethod(MethodHookParam param) {
                            if (param.getThrowable() != null) {
                                return;
                            }
                            String name = (String) param.args[0];
                            if (NATIVE_ENCRYPT.equals(name)
                                    || SG_ENCRYPT_HELPER.equals(name)
                                    || (MariBankRegionProfile.hasEncryptWrapperD(pkg)
                                    && WRAPPER.equals(name))) {
                                installLate(lpparam);
                            }
                        }
                    });
            XposedBridge.log(TAG + " ClassLoader encrypt watcher OK");
        } catch (Throwable t) {
            XposedBridge.log(TAG + " skip encrypt ClassLoader watcher: " + t.getMessage());
        }
    }

    private static int hookEncryptHelperClass(
            XC_LoadPackage.LoadPackageParam lpparam, String className, String logLabel) {
        int count = 0;
        try {
            Class<?> clazz = XposedHelpers.findClass(className, lpparam.classLoader);
            for (Method method : clazz.getDeclaredMethods()) {
                if (Modifier.isStatic(method.getModifiers())) {
                    continue;
                }
                XposedBridge.hookMethod(method, new XC_MethodHook() {
                    @Override
                    protected void beforeHookedMethod(MethodHookParam param) {
                        XposedBridge.log(TAG + " >> " + logLabel + "." + method.getName());
                        for (int i = 0; i < param.args.length; i++) {
                            Object sanitized = sanitizeArg(param.args[i]);
                            if (sanitized != param.args[i]) {
                                param.args[i] = sanitized;
                            }
                            logArg("  in" + i, param.args[i]);
                        }
                    }

                    @Override
                    protected void afterHookedMethod(MethodHookParam param) {
                        Object result = param.getResult();
                        if (result instanceof byte[]) {
                            logArg("  out", result);
                        } else if (result instanceof String) {
                            logArg("  out", result);
                        }
                    }
                });
                count++;
            }
            if (count > 0) {
                XposedBridge.log(TAG + " hooked " + logLabel + " methods=" + count);
            }
        } catch (Throwable t) {
            XposedBridge.log(TAG + " skip " + logLabel + ": " + t.getMessage());
        }
        return count;
    }

    private static int hookEncryptWrapper(XC_LoadPackage.LoadPackageParam lpparam) {
        int count = 0;
        try {
            Class<?> clazz = XposedHelpers.findClass(WRAPPER, lpparam.classLoader);
            for (Method method : clazz.getDeclaredMethods()) {
                if (Modifier.isStatic(method.getModifiers())) {
                    continue;
                }
                XposedBridge.hookMethod(method, new XC_MethodHook() {
                    @Override
                    protected void beforeHookedMethod(MethodHookParam param) {
                        String name = method.getName();
                        XposedBridge.log(TAG + " >> utils.d." + name);
                        for (int i = 0; i < param.args.length; i++) {
                            Object sanitized = sanitizeArg(param.args[i]);
                            if (sanitized != param.args[i]) {
                                param.args[i] = sanitized;
                            }
                            logArg("  in" + i, param.args[i]);
                        }
                    }

                    @Override
                    protected void afterHookedMethod(MethodHookParam param) {
                        Object result = param.getResult();
                        if (result == null) {
                            return;
                        }
                        if (result instanceof String[]) {
                            for (int i = 0; i < ((String[]) result).length; i++) {
                                logArg("  out" + i, ((String[]) result)[i]);
                            }
                        } else if (result instanceof byte[]) {
                            logArg("  out", result);
                        }
                    }
                });
                count++;
            }
            if (count > 0) {
                XposedBridge.log(TAG + " hooked utils.d methods=" + count);
            }
        } catch (Throwable t) {
            XposedBridge.log(TAG + " skip utils.d: " + t.getMessage());
        }
        return count;
    }

    private static int hookNativeEncryptUtils(XC_LoadPackage.LoadPackageParam lpparam) {
        int count = 0;
        try {
            Class<?> clazz = XposedHelpers.findClass(NATIVE_ENCRYPT, lpparam.classLoader);
            for (Method method : clazz.getDeclaredMethods()) {
                if (!Modifier.isStatic(method.getModifiers())) {
                    continue;
                }
                XposedBridge.hookMethod(method, new XC_MethodHook() {
                    @Override
                    protected void beforeHookedMethod(MethodHookParam param) {
                        XposedBridge.log(TAG + " >> uvwuvwuv." + method.getName());
                        for (int i = 0; i < param.args.length; i++) {
                            Object sanitized = sanitizeArg(param.args[i]);
                            if (sanitized != param.args[i]) {
                                param.args[i] = sanitized;
                            }
                            logArg("  in" + i, param.args[i]);
                        }
                    }
                });
                count++;
            }
            if (count > 0) {
                XposedBridge.log(TAG + " hooked uvwuvwuv methods=" + count);
            }
        } catch (Throwable t) {
            XposedBridge.log(TAG + " skip uvwuvwuv: " + t.getMessage());
        }
        return count;
    }

    private static int hookGsonRegister(XC_LoadPackage.LoadPackageParam lpparam) {
        try {
            XposedHelpers.findAndHookMethod(
                    "com.google.gson.Gson",
                    lpparam.classLoader,
                    "toJson",
                    Object.class,
                    new XC_MethodHook() {
                        @Override
                        protected void afterHookedMethod(MethodHookParam param) {
                            if (!(param.getResult() instanceof String)) {
                                return;
                            }
                            String json = (String) param.getResult();
                            if (!looksLikeRegisterJson(json)) {
                                return;
                            }
                            String sanitized = MariBankRegisterPayloadUtil.isRegistrationPayload(json)
                                    ? MariBankRegisterPayloadUtil.sanitizeRegistrationJson(json)
                                    : MariBankRiskTokenUtil.sanitizeAllInText(json);
                            if (!sanitized.equals(json)) {
                                param.setResult(sanitized);
                                json = sanitized;
                            }
                            if (MariBankRegisterPayloadUtil.isRegistrationPayload(json)) {
                                MariBankCaptureUtil.logText("Gson REGISTRATION plaintext", json);
                            }
                        }
                    }
            );
            return 1;
        } catch (Throwable t) {
            XposedBridge.log(TAG + " skip Gson.toJson: " + t.getMessage());
            return 0;
        }
    }

    private static boolean looksLikeRegisterJson(String text) {
        if (text == null || text.length() < 8) {
            return false;
        }
        if (MariBankRegisterPayloadUtil.isRegistrationPayload(text)) {
            return true;
        }
        String lower = text.toLowerCase();
        return lower.contains("rdverifyinfo")
                || lower.contains("devicefingerprint")
                || (lower.contains("encphone") && lower.contains("scene"));
    }

    private static Object sanitizeArg(Object arg) {
        if (arg instanceof String) {
            String s = (String) arg;
            if (!s.contains("|") && !looksLikeRegisterJson(s)) {
                return arg;
            }
            String out = MariBankRegisterPayloadUtil.isRegistrationPayload(s)
                    ? MariBankRegisterPayloadUtil.sanitizeRegistrationJson(s)
                    : MariBankRiskTokenUtil.sanitizeAllInText(s);
            return out.equals(s) ? arg : out;
        }
        if (arg instanceof byte[]) {
            byte[] bytes = (byte[]) arg;
            byte[] out = MariBankRegisterPayloadUtil.sanitizeRegistrationBytes(bytes);
            return out == bytes ? arg : out;
        }
        return arg;
    }

    private static void logArg(String label, Object arg) {
        if (arg == null) {
            XposedBridge.log(TAG + label + " null");
            return;
        }
        if (arg instanceof String) {
            String s = (String) arg;
            if (s.length() > 4 || s.contains("|") || looksLikeRegisterJson(s)) {
                MariBankCaptureUtil.logText(label, s);
            }
            return;
        }
        if (arg instanceof byte[]) {
            byte[] bytes = (byte[]) arg;
            String text;
            try {
                text = new String(bytes, StandardCharsets.UTF_8);
            } catch (Throwable t) {
                text = "<bin>";
            }
            if (text.contains("|") || looksLikeRegisterJson(text) || bytes.length < 8192) {
                MariBankCaptureUtil.logBytes(label, bytes);
            }
        }
    }
}
