package com.miraclegarden.smsmessage.xposed.hook;

import android.app.Activity;
import android.app.Dialog;
import android.content.ContentResolver;
import android.content.res.Resources;
import android.os.Process;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import android.widget.Toast;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

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;

/**
 * MariBank / SeaBank Root 检测绕过（PH + SG）。
 * 逆向：SafeMode SDK + SHPSSDK；SG 额外有 USB/无线 ADB 检测（RISK_USB_ADB / RISK_WIFI_ADB）。
 */
public final class MariBankRootBypassHook {

    private static final String TAG = "notiMessageHook/MariBankRoot";
    /** 菲律宾 MariBank / SeaBank PH */
    public static final String PACKAGE = "ph.seabank.seabank";
    /** 新加坡 MariBank */
    public static final String PACKAGE_SG = "sg.com.maribankmobile.digitalbank";

    private static final String[] TARGET_PACKAGES = {PACKAGE, PACKAGE_SG};

    public static boolean isTargetPackage(String packageName) {
        if (packageName == null) {
            return false;
        }
        for (String pkg : TARGET_PACKAGES) {
            if (pkg.equals(packageName)) {
                return true;
            }
        }
        return false;
    }

    /** 服务端注册被拒错误码（logcat 实测）。 */
    private static final int ERROR_CODE_SECURITY_BLOCKED = 4067004;
    private static final int ERROR_CODE_SECURITY_BLOCKED_ALT = 4067012;

    private static final ThreadLocal<String> CURRENT_REQUEST_URL = new ThreadLocal<>();

    /** killProcess 被拦截后的宽限期：此期间阻止 finish 造成「假闪退」。 */
    private static volatile long lastBlockedSuicideAt = 0L;
    private static final long SOFT_CRASH_GUARD_MS = 8000L;

    private static volatile int finishBurstCount = 0;
    private static volatile long finishBurstStartMs = 0L;
    private static final long FINISH_BURST_WINDOW_MS = 800L;
    private static final int FINISH_BURST_THRESHOLD = 2;

    private static final String[] SAFE_MODE_CLASSES = {
            "com.shopee.bke.lib.safemode.b",
            "com.shopee.bke.lib.safemode.catchs.a",
            "com.shopee.bke.lib.safemode.util.c",
            "com.shopee.bke.biz.base.risk.a",
    };

    private static final String[] ERROR_FLOW_CLASSES = {
            "com.shopee.bke.biz.user.errorcodehandler.a",
            "com.shopee.bke.biz.user.errorcodehandler.b",
            "com.shopee.bke.biz.user.rn.helper.ErrorFlowHelper",
            "com.shopee.bke.biz.user.viewmodel.RegisterViewModel",
    };

    private MariBankRootBypassHook() {
    }

    private static volatile boolean deferredHooksInstalled = false;
    private static volatile boolean lateAppHooksInstalled = false;

    public static void install(XC_LoadPackage.LoadPackageParam lpparam) {
        if (!isTargetPackage(lpparam.packageName)) {
            return;
        }

        XposedBridge.log(TAG + " install for " + lpparam.packageName);
        hookAntiSuicide(lpparam);
        hookAntiSoftCrash(lpparam);
        scheduleAppHooks(lpparam);
    }

    /**
     * 必须在 BkeApplication.attachBaseContext 完成之后安装：
     * loadPackage 时 ClassLoader 未绑定 split APK，过早 Hook SHPSSDK 会导致 libsdkutils.so 死循环白屏。
     */
    private static void scheduleAppHooks(XC_LoadPackage.LoadPackageParam lpparam) {
        XC_MethodHook afterAttach = new XC_MethodHook() {
            @Override
            protected void afterHookedMethod(MethodHookParam param) {
                installDeferredHooks(lpparam);
                MariBankShpsNativeHook.installDeferred(lpparam);
            }
        };
        XC_MethodHook afterOnCreate = new XC_MethodHook() {
            @Override
            protected void afterHookedMethod(MethodHookParam param) {
                MariBankShpsNativeHook.installLateNativeHooks(lpparam);
                installLateAppHooks(lpparam);
            }
        };
        try {
            XposedHelpers.findAndHookMethod(
                    "com.shopee.bke.digitalbank.BkeApplication",
                    lpparam.classLoader,
                    "attachBaseContext",
                    "android.content.Context",
                    afterAttach);
            XposedHelpers.findAndHookMethod(
                    "com.shopee.bke.digitalbank.BkeApplication",
                    lpparam.classLoader,
                    "onCreate",
                    afterOnCreate);
            XposedBridge.log(TAG + " waiting attachBaseContext + onCreate for app hooks");
        } catch (Throwable t) {
            XposedBridge.log(TAG + " BkeApplication hook failed, install now: " + t.getMessage());
            installDeferredHooks(lpparam);
            MariBankShpsNativeHook.installDeferred(lpparam);
            MariBankShpsNativeHook.installLateNativeHooks(lpparam);
            installLateAppHooks(lpparam);
        }
    }

    /** onCreate 之后补装：此时 classes11 / SHPSSDK 与 RN SO 均已就绪。 */
    private static void installLateAppHooks(XC_LoadPackage.LoadPackageParam lpparam) {
        if (lateAppHooksInstalled) {
            return;
        }
        lateAppHooksInstalled = true;

        int hooked = 0;
        for (String className : SAFE_MODE_CLASSES) {
            hooked += hookAllBooleanChecks(lpparam, className);
        }
        hooked += hookShpsRisk(lpparam);
        hookShpsToken(lpparam);
        hookErrorFlowLogging(lpparam);
        MariBankSdkUtilsHook.installLate(lpparam);
        MariBankAttestationHook.installLate(lpparam);

        XposedBridge.log(TAG + " late app hooks installed, booleanHooks=" + hooked);
    }

    private static void installDeferredHooks(XC_LoadPackage.LoadPackageParam lpparam) {
        if (deferredHooksInstalled) {
            return;
        }
        deferredHooksInstalled = true;

        hookSafeModeDialog(lpparam);
        hookRootDialogBlock(lpparam);
        hookAdbBypass(lpparam);
        hookNetworkLogging(lpparam);

        XposedBridge.log(TAG + " early app hooks installed");
    }
    private static int hookAllBooleanChecks(XC_LoadPackage.LoadPackageParam lpparam, String className) {
        int count = 0;
        try {
            Class<?> clazz = XposedHelpers.findClass(className, lpparam.classLoader);
            for (Method method : clazz.getDeclaredMethods()) {
                if (java.lang.reflect.Modifier.isStatic(method.getModifiers())) {
                    continue;
                }
                Class<?> returnType = method.getReturnType();
                if (returnType != boolean.class && returnType != Boolean.class
                        && returnType != int.class && returnType != Integer.class) {
                    continue;
                }
                if (method.getParameterTypes().length > 2) {
                    continue;
                }
                XposedBridge.hookMethod(method, new XC_MethodHook() {
                    @Override
                    protected void beforeHookedMethod(MethodHookParam param) {
                        if (returnType == boolean.class || returnType == Boolean.class) {
                            param.setResult(false);
                        } else {
                            param.setResult(0);
                        }
                    }
                });
                count++;
            }
            if (count > 0) {
                XposedBridge.log(TAG + " hooked " + count + " checks in " + className);
            }
        } catch (Throwable t) {
            XposedBridge.log(TAG + " skip " + className + ": " + t.getMessage());
        }
        return count;
    }

    /** 阻止检测到 Root 后 Process.killProcess / System.exit 自杀。 */
    private static void hookAntiSuicide(XC_LoadPackage.LoadPackageParam lpparam) {
        try {
            XposedHelpers.findAndHookMethod(
                    Process.class,
                    "killProcess",
                    int.class,
                    new XC_MethodHook() {
                        @Override
                        protected void beforeHookedMethod(MethodHookParam param) {
                            int pid = (Integer) param.args[0];
                            if (pid == Process.myPid()) {
                                lastBlockedSuicideAt = System.currentTimeMillis();
                                XposedBridge.log(TAG + " blocked killProcess(self)");
                                param.setResult(null);
                            }
                        }
                    }
            );
        } catch (Throwable t) {
            XposedBridge.log(TAG + " killProcess hook failed: " + t.getMessage());
        }

        try {
            XposedHelpers.findAndHookMethod(
                    System.class,
                    "exit",
                    int.class,
                    new XC_MethodHook() {
                        @Override
                        protected void beforeHookedMethod(MethodHookParam param) {
                            lastBlockedSuicideAt = System.currentTimeMillis();
                            XposedBridge.log(TAG + " blocked System.exit");
                            param.setResult(null);
                        }
                    }
            );
        } catch (Throwable t) {
            XposedBridge.log(TAG + " System.exit hook failed: " + t.getMessage());
        }

        try {
            XposedHelpers.findAndHookMethod(
                    Runtime.class,
                    "exit",
                    int.class,
                    new XC_MethodHook() {
                        @Override
                        protected void beforeHookedMethod(MethodHookParam param) {
                            lastBlockedSuicideAt = System.currentTimeMillis();
                            XposedBridge.log(TAG + " blocked Runtime.exit");
                            param.setResult(null);
                        }
                    }
            );
        } catch (Throwable t) {
            XposedBridge.log(TAG + " Runtime.exit hook failed: " + t.getMessage());
        }

        try {
            XposedHelpers.findAndHookMethod(
                    Process.class,
                    "sendSignal",
                    int.class,
                    int.class,
                    new XC_MethodHook() {
                        @Override
                        protected void beforeHookedMethod(MethodHookParam param) {
                            int pid = (Integer) param.args[0];
                            int signal = (Integer) param.args[1];
                            if (pid == Process.myPid() && (signal == 9 || signal == 15)) {
                                lastBlockedSuicideAt = System.currentTimeMillis();
                                XposedBridge.log(TAG + " blocked sendSignal(self, " + signal + ")");
                                param.setResult(null);
                            }
                        }
                    }
            );
        } catch (Throwable t) {
            XposedBridge.log(TAG + " sendSignal hook failed: " + t.getMessage());
        }
    }

    /** 阻止 killProcess 失败后通过 finish / finishAffinity 把界面关掉（用户感知为闪退，进程其实还在）。 */
    private static void hookAntiSoftCrash(XC_LoadPackage.LoadPackageParam lpparam) {
        XC_MethodHook blockFinishHook = new XC_MethodHook() {
            @Override
            protected void beforeHookedMethod(MethodHookParam param) {
                Activity activity = (Activity) param.thisObject;
                String methodName = param.method.getName();
                if (!shouldBlockFinish(activity, methodName)) {
                    return;
                }
                XposedBridge.log(TAG + " blocked " + methodName
                        + " after suicide attempt: " + activity.getClass().getSimpleName());
                param.setResult(null);
            }
        };

        try {
            XposedHelpers.findAndHookMethod(Activity.class, "finish", blockFinishHook);
        } catch (Throwable t) {
            XposedBridge.log(TAG + " finish hook failed: " + t.getMessage());
        }

        try {
            XposedHelpers.findAndHookMethod(Activity.class, "finishAfterTransition", blockFinishHook);
        } catch (Throwable t) {
            XposedBridge.log(TAG + " finishAfterTransition hook failed: " + t.getMessage());
        }

        try {
            XposedHelpers.findAndHookMethod(
                    Activity.class,
                    "finishAffinity",
                    new XC_MethodHook() {
                        @Override
                        protected void beforeHookedMethod(MethodHookParam param) {
                            Activity activity = (Activity) param.thisObject;
                            if (!shouldBlockFinish(activity, "finishAffinity")) {
                                return;
                            }
                            XposedBridge.log(TAG + " blocked finishAffinity after suicide attempt: "
                                    + activity.getClass().getSimpleName());
                            param.setResult(null);
                        }
                    }
            );
        } catch (Throwable t) {
            XposedBridge.log(TAG + " finishAffinity hook failed: " + t.getMessage());
        }

        try {
            XposedHelpers.findAndHookMethod(
                    Activity.class,
                    "finishAndRemoveTask",
                    new XC_MethodHook() {
                        @Override
                        protected void beforeHookedMethod(MethodHookParam param) {
                            Activity activity = (Activity) param.thisObject;
                            if (!shouldBlockFinish(activity, "finishAndRemoveTask")) {
                                return;
                            }
                            XposedBridge.log(TAG + " blocked finishAndRemoveTask after suicide attempt: "
                                    + activity.getClass().getSimpleName());
                            param.setResult(null);
                        }
                    }
            );
        } catch (Throwable t) {
            XposedBridge.log(TAG + " finishAndRemoveTask hook failed: " + t.getMessage());
        }
        try {
            XposedHelpers.findAndHookMethod(Activity.class, "moveTaskToBack", boolean.class, new XC_MethodHook() {
                @Override
                protected void beforeHookedMethod(MethodHookParam param) {
                    Activity activity = (Activity) param.thisObject;
                    if (!shouldBlockFinish(activity, "moveTaskToBack")) {
                        return;
                    }
                    XposedBridge.log(TAG + " blocked moveTaskToBack after suicide attempt");
                    param.setResult(null);
                }
            });
        } catch (Throwable t) {
            XposedBridge.log(TAG + " moveTaskToBack hook failed: " + t.getMessage());
        }
    }

    private static boolean shouldBlockSoftCrash() {
        return System.currentTimeMillis() - lastBlockedSuicideAt < SOFT_CRASH_GUARD_MS;
    }

    private static boolean isBkeActivity(Activity activity) {
        String name = activity.getClass().getName();
        return name.startsWith("com.shopee.bke") || name.startsWith("com.shopee.bke.digitalbank");
    }

    private static boolean isUserBackNavigation() {
        for (StackTraceElement frame : Thread.currentThread().getStackTrace()) {
            String method = frame.getMethodName();
            if ("onBackPressed".equals(method) || "onBackInvoked".equals(method)) {
                return true;
            }
        }
        return false;
    }

    private static void trackFinishBurst() {
        long now = System.currentTimeMillis();
        if (now - finishBurstStartMs > FINISH_BURST_WINDOW_MS) {
            finishBurstCount = 0;
            finishBurstStartMs = now;
        }
        finishBurstCount++;
        if (finishBurstCount >= FINISH_BURST_THRESHOLD) {
            lastBlockedSuicideAt = now;
        }
    }

    /**
     * finish / finishAffinity 往往先于 killProcess；
     * finishAffinity 为 Root 检测自杀常用路径，对 bke Activity 直接拦截（保留返回键）。
     */
    private static boolean shouldBlockFinish(Activity activity, String methodName) {
        String name = activity.getClass().getName();
        if (name.contains("SafeModeRecoverActivity")) {
            return false;
        }
        if (isUserBackNavigation()) {
            return false;
        }
        if (!isBkeActivity(activity)) {
            return false;
        }
        if ("finishAffinity".equals(methodName) || "finishAndRemoveTask".equals(methodName)) {
            return true;
        }
        trackFinishBurst();
        if (shouldBlockSoftCrash()) {
            return true;
        }
        if (finishBurstCount >= FINISH_BURST_THRESHOLD) {
            return true;
        }
        return isRiskRelatedStackTrace();
    }

    private static boolean isRiskRelatedStackTrace() {
        for (StackTraceElement frame : Thread.currentThread().getStackTrace()) {
            String cn = frame.getClassName();
            if (cn.contains("safemode")
                    || cn.contains("shpssdk")
                    || cn.contains("com.shopee.bke")
                    || cn.contains("bke.biz.base.risk")
                    || cn.contains("errorcodehandler")
                    || cn.contains("ErrorFlow")) {
                return true;
            }
        }
        return false;
    }

    /** 记录注册/OTP 错误码路径，便于 logcat 定位 -1201 来源。 */
    private static void hookErrorFlowLogging(XC_LoadPackage.LoadPackageParam lpparam) {
        for (String className : ERROR_FLOW_CLASSES) {
            try {
                Class<?> clazz = XposedHelpers.findClass(className, lpparam.classLoader);
                for (Method method : clazz.getDeclaredMethods()) {
                    XposedBridge.hookMethod(method, new XC_MethodHook() {
                        @Override
                        protected void beforeHookedMethod(MethodHookParam param) {
                            if (param.args == null || param.args.length == 0) {
                                return;
                            }
                            String args = Arrays.toString(param.args);
                            if (args.contains("1201") || args.contains("-1201")
                                    || args.contains("406")
                                    || args.toLowerCase().contains("error")
                                    || args.toLowerCase().contains("unavailable")) {
                                XposedBridge.log(TAG + " " + className + "."
                                        + method.getName() + " args=" + args);
                            }
                        }
                    });
                }
            } catch (Throwable t) {
                XposedBridge.log(TAG + " skip error flow " + className + ": " + t.getMessage());
            }
        }
    }

    /** SHPSSDK 风控：仅 Hook 返回 boolean 的实例方法。 */
    private static int hookShpsRisk(XC_LoadPackage.LoadPackageParam lpparam) {
        int count = 0;
        String[] riskClasses = {
                "com.shopee.shpssdkbank.SPSAssessRisk",
                "com.shopee.shpssdk.SPSAssessRisk",
        };
        for (String className : riskClasses) {
            try {
                Class<?> clazz = XposedHelpers.findClass(className, lpparam.classLoader);
                for (Method method : clazz.getDeclaredMethods()) {
                    if (java.lang.reflect.Modifier.isStatic(method.getModifiers())) {
                        continue;
                    }
                    Class<?> returnType = method.getReturnType();
                    if (returnType != boolean.class && returnType != Boolean.class) {
                        continue;
                    }
                    XposedBridge.hookMethod(method, new XC_MethodHook() {
                        @Override
                        protected void beforeHookedMethod(MethodHookParam param) {
                            param.setResult(false);
                        }
                    });
                    count++;
                }
                if (count > 0) {
                    XposedBridge.log(TAG + " hooked SHPS boolean checks in " + className);
                }
            } catch (Throwable t) {
                XposedBridge.log(TAG + " skip SHPS " + className + ": " + t.getMessage());
            }
        }
        return count;
    }

    /**
     * SHPSSDK 风控 token：清空本地 risk 列表，避免 Root/Hook 标记写入 token 上报服务端。
     * 逆向：getRiskSync / getRiskAsync / assessRisk → List&lt;SPSAssessRisk&gt;，RISK_ROOT=1, RISK_HOOK=4 ...
     */
    private static void hookShpsToken(XC_LoadPackage.LoadPackageParam lpparam) {
        final String contextClass = "android.content.Context";
        String[] shpsSdkClasses = {
                "com.shopee.shpssdk.SHPSSDK",
                "com.shopee.shpssdkbank.SHPSSDK",
        };
        for (String className : shpsSdkClasses) {
            hookEmptyRiskList(lpparam, className, "getRiskSync", contextClass);
            hookEmptyRiskList(lpparam, className, "getExtRiskSync", contextClass);
            hookRiskAsyncCallback(lpparam, className, "getRiskAsync", contextClass,
                    className.contains("bank")
                            ? "com.shopee.shpssdkbank.SPSResultCallback"
                            : "com.shopee.shpssdk.SPSResultCallback");
            hookRiskAsyncCallback(lpparam, className, "getExtRiskAsync", contextClass,
                    className.contains("bank")
                            ? "com.shopee.shpssdkbank.SPSExtResultCallback"
                            : "com.shopee.shpssdk.SPSExtResultCallback");
            hookRiskTokenAsync(lpparam, className, contextClass);
            try {
                XposedHelpers.findAndHookMethod(
                        className,
                        lpparam.classLoader,
                        "getRiskToken",
                        contextClass,
                        new XC_MethodHook() {
                            @Override
                            protected void afterHookedMethod(MethodHookParam param) {
                                Object token = param.getResult();
                                if (token instanceof String) {
                                    String sanitized = MariBankRiskTokenUtil.sanitizeRiskToken((String) token);
                                    param.setResult(sanitized);
                                    XposedBridge.log(TAG + " getRiskToken len="
                                            + sanitized.length() + " tail="
                                            + MariBankRiskTokenUtil.tail(sanitized));
                                }
                            }
                        }
                );
            } catch (Throwable t) {
                XposedBridge.log(TAG + " skip " + className + ".getRiskToken: " + t.getMessage());
            }
            hookTokenStringMethod(lpparam, className, "getLongToken");
            hookTokenStringMethod(lpparam, className, "getShortToken");
            hookShpsSecData(lpparam, className, contextClass);
            try {
                XposedHelpers.findAndHookMethod(
                        className,
                        lpparam.classLoader,
                        "getSoftToken",
                        String.class,
                        new XC_MethodHook() {
                            @Override
                            protected void afterHookedMethod(MethodHookParam param) {
                                Object token = param.getResult();
                                if (token instanceof String) {
                                    String sanitized = MariBankRiskTokenUtil.sanitizeRiskToken((String) token);
                                    param.setResult(sanitized);
                                    logTokenResult("getSoftToken", sanitized);
                                }
                            }
                        }
                );
            } catch (Throwable t) {
                XposedBridge.log(TAG + " skip " + className + ".getSoftToken: " + t.getMessage());
            }
        }

        try {
            XposedHelpers.findAndHookMethod(
                    "com.shopee.shpssdkbank.SHPSSDK",
                    lpparam.classLoader,
                    "assessRisk",
                    int.class,
                    new XC_MethodHook() {
                        @Override
                        protected void beforeHookedMethod(MethodHookParam param) {
                            param.setResult(new ArrayList<>());
                            XposedBridge.log(TAG + " assessRisk -> empty");
                        }
                    }
            );
        } catch (Throwable ignored) {
        }

        String[] assessRiskClasses = {
                "com.shopee.shpssdkbank.SPSAssessRisk",
                "com.shopee.shpssdk.SPSAssessRisk",
        };
        for (String className : assessRiskClasses) {
            try {
                XposedHelpers.findAndHookMethod(
                        className,
                        lpparam.classLoader,
                        "getType",
                        new XC_MethodHook() {
                            @Override
                            protected void beforeHookedMethod(MethodHookParam param) {
                                param.setResult(0);
                            }
                        }
                );
            } catch (Throwable ignored) {
            }
        }

        String[] callbackAdapters = {
                "com.shopee.shpssdk.SPSCallbackAdapter",
                "com.shopee.shpssdkbank.SPSCallbackAdapter",
        };
        for (String className : callbackAdapters) {
            try {
                XposedHelpers.findAndHookMethod(
                        className,
                        lpparam.classLoader,
                        "onGetRiskTokenFail",
                        int.class,
                        String.class,
                        new XC_MethodHook() {
                            @Override
                            protected void beforeHookedMethod(MethodHookParam param) {
                                XposedBridge.log(TAG + " suppressed onGetRiskTokenFail: " + param.args[1]);
                                param.setResult(null);
                            }
                        }
                );
            } catch (Throwable ignored) {
            }
        }

        hookShpsTokenCore(lpparam);
    }

    /**
     * classes11 真实 token 生成链（早于 SHPSSDK 门面）：
     * getRiskToken → vvuuuuvvv.wwvuwuwvu(Context)
     * getRiskSync  → vvuuuuvvv.uuuuuuwvw(Context)
     */
    private static void hookShpsTokenCore(XC_LoadPackage.LoadPackageParam lpparam) {
        final String coreClass = "com.shopee.shpssdkbank.uwuvuvvww.vvuuuuvvv";
        final String contextClass = "android.content.Context";
        int hooked = 0;

        try {
            XposedHelpers.findAndHookMethod(
                    coreClass,
                    lpparam.classLoader,
                    "wwvuwuwvu",
                    contextClass,
                    new XC_MethodHook() {
                        @Override
                        protected void afterHookedMethod(MethodHookParam param) {
                            Object token = param.getResult();
                            if (token instanceof String) {
                                String sanitized = MariBankRiskTokenUtil.sanitizeRiskToken((String) token);
                                param.setResult(sanitized);
                                XposedBridge.log(TAG + " core.wwvuwuwvu len="
                                        + sanitized.length() + " tail="
                                        + MariBankRiskTokenUtil.tail(sanitized));
                            }
                        }
                    });
            hooked++;
        } catch (Throwable t) {
            XposedBridge.log(TAG + " skip core.wwvuwuwvu: " + t.getMessage());
        }

        try {
            XposedHelpers.findAndHookMethod(
                    coreClass,
                    lpparam.classLoader,
                    "uuuuuuwvw",
                    contextClass,
                    new XC_MethodHook() {
                        @Override
                        protected void beforeHookedMethod(MethodHookParam param) {
                            param.setResult(new ArrayList<>());
                            XposedBridge.log(TAG + " core.uuuuuuwvw -> empty");
                        }
                    });
            hooked++;
        } catch (Throwable t) {
            XposedBridge.log(TAG + " skip core.uuuuuuwvw: " + t.getMessage());
        }

        try {
            XposedHelpers.findAndHookMethod(
                    coreClass,
                    lpparam.classLoader,
                    "wwwuvwwuu",
                    contextClass,
                    String.class,
                    boolean.class,
                    boolean.class,
                    new XC_MethodHook() {
                        @Override
                        protected void afterHookedMethod(MethodHookParam param) {
                            Object result = param.getResult();
                            if (result instanceof String) {
                                String sanitized = MariBankRiskTokenUtil.sanitizeAllInText((String) result);
                                param.setResult(sanitized);
                                XposedBridge.log(TAG + " core.wwwuvwwuu len=" + sanitized.length());
                            }
                        }
                    });
            hooked++;
        } catch (Throwable t) {
            XposedBridge.log(TAG + " skip core.wwwuvwwuu: " + t.getMessage());
        }

        if (hooked > 0) {
            XposedBridge.log(TAG + " shps token core hooks=" + hooked);
        }
    }

    private static void hookEmptyRiskList(
            XC_LoadPackage.LoadPackageParam lpparam,
            String className,
            String methodName,
            String contextClassName) {
        try {
            XposedHelpers.findAndHookMethod(
                    className,
                    lpparam.classLoader,
                    methodName,
                    contextClassName,
                    new XC_MethodHook() {
                        @Override
                        protected void beforeHookedMethod(MethodHookParam param) {
                            param.setResult(new ArrayList<>());
                            XposedBridge.log(TAG + " " + methodName + " -> empty");
                        }
                    });
        } catch (Throwable t) {
            XposedBridge.log(TAG + " skip " + className + "." + methodName + ": " + t.getMessage());
        }
    }

    private static void hookRiskAsyncCallback(
            XC_LoadPackage.LoadPackageParam lpparam,
            String sdkClass,
            String methodName,
            String contextClassName,
            String callbackClassName) {
        try {
            Class<?> callbackClass = XposedHelpers.findClass(callbackClassName, lpparam.classLoader);
            XposedHelpers.findAndHookMethod(sdkClass, lpparam.classLoader, methodName,
                    contextClassName, callbackClass, new XC_MethodHook() {
                        @Override
                        protected void beforeHookedMethod(MethodHookParam param) {
                            Object original = param.args[1];
                            if (original == null) {
                                return;
                            }
                            param.args[1] = wrapRiskCallback(lpparam.classLoader, callbackClass, original);
                        }
                    });
        } catch (Throwable t) {
            XposedBridge.log(TAG + " skip async " + sdkClass + "." + methodName + ": " + t.getMessage());
        }
    }

    private static Object wrapRiskCallback(
            ClassLoader loader,
            Class<?> callbackClass,
            Object original) {
        return Proxy.newProxyInstance(loader, new Class[]{callbackClass}, new InvocationHandler() {
            @Override
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                if ("result".equals(method.getName()) && args != null && args.length > 0
                        && args[0] instanceof List) {
                    List<?> list = (List<?>) args[0];
                    XposedBridge.log(TAG + " async risk callback cleared size=" + list.size());
                    args[0] = new ArrayList<>();
                }
                return method.invoke(original, args);
            }
        });
    }

    private static void hookRiskTokenAsync(
            XC_LoadPackage.LoadPackageParam lpparam,
            String sdkClass,
            String contextClassName) {
        String callbackClassName = sdkClass.contains("bank")
                ? "com.shopee.shpssdkbank.SPSRiskTokenCallback"
                : "com.shopee.shpssdk.SPSRiskTokenCallback";
        try {
            Class<?> callbackClass = XposedHelpers.findClass(callbackClassName, lpparam.classLoader);
            XposedHelpers.findAndHookMethod(
                    sdkClass,
                    lpparam.classLoader,
                    "getRiskTokenAsync",
                    contextClassName,
                    callbackClass,
                    int.class,
                    new XC_MethodHook() {
                        @Override
                        protected void beforeHookedMethod(MethodHookParam param) {
                            Object original = param.args[1];
                            if (original == null) {
                                return;
                            }
                            param.args[1] = wrapRiskTokenCallback(
                                    lpparam.classLoader, callbackClass, original);
                        }
                    }
            );
        } catch (Throwable t) {
            XposedBridge.log(TAG + " skip " + sdkClass + ".getRiskTokenAsync: " + t.getMessage());
        }
    }

    private static Object wrapRiskTokenCallback(
            ClassLoader loader,
            Class<?> callbackClass,
            Object original) {
        return Proxy.newProxyInstance(loader, new Class[]{callbackClass}, new InvocationHandler() {
            @Override
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                if ("onResult".equals(method.getName()) && args != null && args.length > 0) {
                    if (args[0] instanceof String) {
                        String sanitized = MariBankRiskTokenUtil.sanitizeRiskToken((String) args[0]);
                        XposedBridge.log(TAG + " getRiskTokenAsync onResult len="
                                + sanitized.length() + " tail="
                                + MariBankRiskTokenUtil.tail(sanitized));
                        args[0] = sanitized;
                    }
                }
                return method.invoke(original, args);
            }
        });
    }

    private static void hookShpsSecData(
            XC_LoadPackage.LoadPackageParam lpparam,
            String className,
            String contextClass) {
        String[][] methods = {
                {"getSHPSECData", contextClass, "java.lang.String", "boolean"},
                {"getSHPSECAllData", contextClass, "java.lang.String", "boolean"},
        };
        for (String[] sig : methods) {
            try {
                XposedHelpers.findAndHookMethod(
                        className,
                        lpparam.classLoader,
                        sig[0],
                        sig[1],
                        sig[2],
                        "boolean",
                        new XC_MethodHook() {
                            @Override
                            protected void afterHookedMethod(MethodHookParam param) {
                                if (param.getResult() instanceof String) {
                                    String sanitized = MariBankRiskTokenUtil.sanitizeAllInText(
                                            (String) param.getResult());
                                    param.setResult(sanitized);
                                }
                            }
                        }
                );
            } catch (Throwable ignored) {
            }
        }
    }

    private static void hookTokenStringMethod(
            XC_LoadPackage.LoadPackageParam lpparam,
            String className,
            String methodName) {
        try {
            XposedHelpers.findAndHookMethod(
                    className,
                    lpparam.classLoader,
                    methodName,
                    new XC_MethodHook() {
                        @Override
                        protected void afterHookedMethod(MethodHookParam param) {
                            Object token = param.getResult();
                            if (token instanceof String) {
                                String sanitized = MariBankRiskTokenUtil.sanitizeRiskToken((String) token);
                                param.setResult(sanitized);
                                logTokenResult(methodName, sanitized);
                            }
                        }
                    }
            );
        } catch (Throwable t) {
            XposedBridge.log(TAG + " skip " + className + "." + methodName + ": " + t.getMessage());
        }
    }

    private static void logTokenResult(String methodName, Object token) {
        if (token instanceof String) {
            String s = MariBankRiskTokenUtil.sanitizeRiskToken((String) token);
            XposedBridge.log(TAG + " " + methodName + " len=" + s.length()
                    + " tail=" + MariBankRiskTokenUtil.tail(s));
        }
    }

    /** 记录注册 API 响应，净化 riskToken JSON，定位 4067004 来源 URL。 */
    private static void hookNetworkLogging(XC_LoadPackage.LoadPackageParam lpparam) {
        // 勿 Hook RealInterceptorChain.proceed — libshpssdk.so 字符串硬编码检测该 Hook。
        hookRequestBuilderBuild(lpparam);
        hookRequestBuilderBody(lpparam);
        hookOkHttpNewCall(lpparam);
        hookRealCallExecute(lpparam);
        hookGsonFromJson(lpparam);
        hookRequestBodyWriteTo(lpparam);
        hookOkHttpResponseUrl(lpparam);
        hookOkioBufferWrite(lpparam);
        hookOutgoingRequestBody(lpparam);
        hookOutgoingRequestBytes(lpparam);
        hookJsonRiskTokenPut(lpparam);
        hookGsonRiskToken(lpparam);
        hookRetrofitGsonConverter(lpparam);
        try {
            XposedHelpers.findAndHookMethod(
                    "okhttp3.ResponseBody",
                    lpparam.classLoader,
                    "string",
                    new XC_MethodHook() {
                        @Override
                        protected void afterHookedMethod(MethodHookParam param) {
                            String body = (String) param.getResult();
                            if (body == null) {
                                return;
                            }
                            String sanitized = MariBankRiskTokenUtil.sanitizeAllInText(body);
                            if (!sanitized.equals(body)) {
                                param.setResult(sanitized);
                                body = sanitized;
                            }
                            String lower = body.toLowerCase();
                            String url = CURRENT_REQUEST_URL.get();
                            boolean maribankApi = url != null
                                    && (url.contains("maribank.com") || url.contains("seabank.ph"));
                            boolean interesting = lower.contains("blocked")
                                    || body.contains(String.valueOf(ERROR_CODE_SECURITY_BLOCKED))
                                    || body.contains(String.valueOf(ERROR_CODE_SECURITY_BLOCKED_ALT))
                                    || lower.contains("risktoken")
                                    || lower.contains("\"code\"")
                                    || (maribankApi && (lower.contains("register")
                                    || lower.contains("error") || lower.contains("unavailable")));
                            if (interesting) {
                                String snippet = body.length() > 600
                                        ? body.substring(0, 600) + "..." : body;
                                XposedBridge.log(TAG + " HTTP"
                                        + (url != null ? " " + url : "")
                                        + " body: " + snippet);
                            }
                            CURRENT_REQUEST_URL.remove();
                        }
                    }
            );
        } catch (Throwable t) {
            XposedBridge.log(TAG + " skip okhttp ResponseBody.string: " + t.getMessage());
        }
    }

    /** 出站 Request 构建时记录 URL，并在 post/put 阶段净化 body。 */
    private static void hookRequestBuilderBuild(XC_LoadPackage.LoadPackageParam lpparam) {
        try {
            XposedHelpers.findAndHookMethod(
                    "okhttp3.Request$Builder",
                    lpparam.classLoader,
                    "build",
                    new XC_MethodHook() {
                        @Override
                        protected void afterHookedMethod(MethodHookParam param) {
                            try {
                                Object url = XposedHelpers.callMethod(param.getResult(), "url");
                                if (url != null) {
                                    String urlStr = String.valueOf(url);
                                    CURRENT_REQUEST_URL.set(urlStr);
                                }
                            } catch (Throwable ignored) {
                            }
                        }
                    }
            );
        } catch (Throwable t) {
            XposedBridge.log(TAG + " skip Request.Builder.build: " + t.getMessage());
        }
    }

    /** 在 RequestBody 挂到 Request 时净化（比抽象 writeTo Hook 更可靠）。 */
    private static void hookRequestBuilderBody(XC_LoadPackage.LoadPackageParam lpparam) {
        XC_MethodHook bodyHook = new XC_MethodHook() {
            @Override
            protected void beforeHookedMethod(MethodHookParam param) {
                for (int i = 0; i < param.args.length; i++) {
                    Object arg = param.args[i];
                    if (arg == null || !isRequestBody(lpparam.classLoader, arg)) {
                        continue;
                    }
                    Object sanitized = sanitizeRequestBody(lpparam.classLoader, arg);
                    if (sanitized != arg) {
                        param.args[i] = sanitized;
                        XposedBridge.log(TAG + " Request.Builder body riskToken sanitized");
                    }
                }
            }
        };
        String[][] sigs = {
                {"post", "okhttp3.RequestBody"},
                {"put", "okhttp3.RequestBody"},
                {"patch", "okhttp3.RequestBody"},
        };
        for (String[] sig : sigs) {
            try {
                XposedHelpers.findAndHookMethod(
                        "okhttp3.Request$Builder",
                        lpparam.classLoader,
                        sig[0],
                        sig[1],
                        bodyHook);
            } catch (Throwable ignored) {
            }
        }
        try {
            XposedHelpers.findAndHookMethod(
                    "okhttp3.Request$Builder",
                    lpparam.classLoader,
                    "method",
                    "java.lang.String",
                    "okhttp3.RequestBody",
                    bodyHook);
        } catch (Throwable ignored) {
        }
    }

    private static void hookOkHttpNewCall(XC_LoadPackage.LoadPackageParam lpparam) {
        XC_MethodHook callHook = new XC_MethodHook() {
            @Override
            protected void beforeHookedMethod(MethodHookParam param) {
                try {
                    Object sanitized = sanitizeOkHttpRequest(lpparam.classLoader, param.args[0]);
                    if (sanitized != param.args[0]) {
                        param.args[0] = sanitized;
                    }
                } catch (Throwable ignored) {
                }
            }
        };
        try {
            XposedHelpers.findAndHookMethod(
                    "okhttp3.OkHttpClient",
                    lpparam.classLoader,
                    "newCall",
                    "okhttp3.Request",
                    callHook);
        } catch (Throwable t) {
            XposedBridge.log(TAG + " skip OkHttpClient.newCall: " + t.getMessage());
        }
    }

    /** Retrofit 异步/同步最终走 RealCall.execute/enqueue。 */
    private static void hookRealCallExecute(XC_LoadPackage.LoadPackageParam lpparam) {
        XC_MethodHook execHook = new XC_MethodHook() {
            @Override
            protected void beforeHookedMethod(MethodHookParam param) {
                try {
                    Object request = XposedHelpers.getObjectField(param.thisObject, "originalRequest");
                    if (request == null) {
                        return;
                    }
                    Object sanitized = sanitizeOkHttpRequest(lpparam.classLoader, request);
                    if (sanitized != request) {
                        XposedHelpers.setObjectField(param.thisObject, "originalRequest", sanitized);
                    }
                    try {
                        Object req = XposedHelpers.getObjectField(param.thisObject, "originalRequest");
                        if (req != null) {
                            Object url = XposedHelpers.callMethod(req, "url");
                            if (url != null) {
                                String urlStr = String.valueOf(url);
                                if (urlStr.contains("maribank.com") || urlStr.contains("seabank.ph")
                                        || urlStr.contains("/register")) {
                                    XposedBridge.log(TAG + " outbound " + urlStr);
                                }
                            }
                        }
                    } catch (Throwable ignored) {
                    }
                } catch (Throwable ignored) {
                }
            }
        };
        for (String className : new String[]{"okhttp3.RealCall", "okhttp3.internal.connection.RealCall"}) {
            try {
                XposedHelpers.findAndHookMethod(className, lpparam.classLoader, "execute", execHook);
                XposedHelpers.findAndHookMethod(className, lpparam.classLoader, "enqueue",
                        "okhttp3.Callback", execHook);
                XposedBridge.log(TAG + " hooked " + className);
            } catch (Throwable ignored) {
            }
        }
    }

    private static Object sanitizeOkHttpRequest(ClassLoader loader, Object request) {
        try {
            Object url = XposedHelpers.callMethod(request, "url");
            if (url == null) {
                return request;
            }
            String urlStr = String.valueOf(url);
            CURRENT_REQUEST_URL.set(urlStr);

            Object body = XposedHelpers.callMethod(request, "body");
            if (body == null) {
                if (urlStr.contains("/register")) {
                    XposedBridge.log(TAG + " register request has null body");
                }
                return request;
            }
            String content = readRequestBodyText(loader, body);
            if (content.isEmpty()) {
                if (urlStr.contains("/register")) {
                    XposedBridge.log(TAG + " register request body unreadable (encrypted or one-shot)");
                }
                return request;
            }
            if (urlStr.contains("/register")) {
                int show = Math.min(content.length(), 500);
                XposedBridge.log(TAG + " outbound register body: "
                        + content.substring(0, show)
                        + (content.length() > show ? "..." : ""));
            }
            Object sanitizedBody = sanitizeRequestBody(loader, body);
            if (sanitizedBody == body) {
                return request;
            }
            String method = (String) XposedHelpers.callMethod(request, "method");
            Object builder = XposedHelpers.callMethod(request, "newBuilder");
            XposedHelpers.callMethod(builder, "method", method, sanitizedBody);
            Object newRequest = XposedHelpers.callMethod(builder, "build");
            XposedBridge.log(TAG + " sanitized outbound body for " + urlStr);
            return newRequest;
        } catch (Throwable t) {
            return request;
        }
    }

    /** dfp 响应若走 Gson.fromJson(String) 而非 ResponseBody.string，需净化入参。 */
    private static void hookGsonFromJson(XC_LoadPackage.LoadPackageParam lpparam) {
        XC_MethodHook fromJsonHook = new XC_MethodHook() {
            @Override
            protected void beforeHookedMethod(MethodHookParam param) {
                if (!(param.args[0] instanceof String)) {
                    return;
                }
                String json = (String) param.args[0];
                if (!json.contains("riskToken") && !json.contains("deviceToken")) {
                    return;
                }
                String sanitized = MariBankRiskTokenUtil.sanitizeAllInText(json);
                if (!sanitized.equals(json)) {
                    param.args[0] = sanitized;
                    XposedBridge.log(TAG + " Gson.fromJson riskToken sanitized");
                }
            }
        };
        try {
            XposedHelpers.findAndHookMethod(
                    "com.google.gson.Gson",
                    lpparam.classLoader,
                    "fromJson",
                    String.class,
                    Class.class,
                    fromJsonHook);
        } catch (Throwable ignored) {
        }
        try {
            XposedHelpers.findAndHookMethod(
                    "com.google.gson.Gson",
                    lpparam.classLoader,
                    "fromJson",
                    String.class,
                    "java.lang.reflect.Type",
                    fromJsonHook);
        } catch (Throwable ignored) {
        }
    }

    private static boolean isRequestBody(ClassLoader loader, Object obj) {
        try {
            Class<?> rb = XposedHelpers.findClass("okhttp3.RequestBody", loader);
            return rb.isInstance(obj);
        } catch (Throwable t) {
            return obj.getClass().getName().contains("RequestBody");
        }
    }

    private static Object sanitizeRequestBody(ClassLoader loader, Object body) {
        try {
            ClassLoader effective = loaderFor(body, loader);
            String content = readRequestBodyText(effective, body);
            if (content.isEmpty()) {
                return body;
            }
            String sanitized = MariBankRiskTokenUtil.sanitizeAllInText(content);
            if (sanitized.equals(content)) {
                return body;
            }
            Object mediaType = XposedHelpers.callMethod(body, "contentType");
            Class<?> rbClass = findClassSafe(effective, "okhttp3.RequestBody");
            return XposedHelpers.callStaticMethod(rbClass, "create", mediaType, sanitized);
        } catch (Throwable t) {
            return body;
        }
    }

    private static String readRequestBodyText(ClassLoader loader, Object body) {
        try {
            ClassLoader effective = loaderFor(body, loader);
            Class<?> bufferClass = findClassSafe(effective, "okio.Buffer");
            Object buffer = XposedHelpers.newInstance(bufferClass);
            XposedHelpers.callMethod(body, "writeTo", buffer);
            return (String) XposedHelpers.callMethod(buffer, "readUtf8");
        } catch (Throwable t) {
            return "";
        }
    }

    private static ClassLoader loaderFor(Object obj, ClassLoader fallback) {
        if (obj != null) {
            ClassLoader cl = obj.getClass().getClassLoader();
            if (cl != null) {
                return cl;
            }
        }
        return fallback;
    }

    private static Class<?> findClassSafe(ClassLoader loader, String name) {
        try {
            return XposedHelpers.findClass(name, loader);
        } catch (Throwable first) {
            ClassLoader ctx = Thread.currentThread().getContextClassLoader();
            if (ctx != null && ctx != loader) {
                return XposedHelpers.findClass(name, ctx);
            }
            throw first;
        }
    }

    /**
     * 拦截 RequestBody 写入：注册 JSON 只写一次，create/Buffer Hook 可能漏掉。
     * 读出 body → 净化 riskToken → 写入 sink，跳过原方法。
     */
    private static void hookRequestBodyWriteTo(XC_LoadPackage.LoadPackageParam lpparam) {
        try {
            Class<?> rbClass = findClassSafe(lpparam.classLoader, "okhttp3.RequestBody");
            Class<?> sinkClass = findClassSafe(lpparam.classLoader, "okio.BufferedSink");
            XposedHelpers.findAndHookMethod(
                    rbClass,
                    "writeTo",
                    sinkClass,
                    new XC_MethodHook() {
                        @Override
                        protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
                            ClassLoader cl = loaderFor(param.thisObject, lpparam.classLoader);
                            Class<?> bufferClass = findClassSafe(cl, "okio.Buffer");
                            Object buffer = XposedHelpers.newInstance(bufferClass);
                            XposedBridge.invokeOriginalMethod(
                                    param.method, param.thisObject, new Object[]{buffer});
                            String content = (String) XposedHelpers.callMethod(buffer, "readUtf8");
                            String sanitized = MariBankRiskTokenUtil.sanitizeAllInText(content);
                            String url = CURRENT_REQUEST_URL.get();
                            if (url != null && url.contains("/register") && !content.isEmpty()) {
                                int show = Math.min(content.length(), 400);
                                XposedBridge.log(TAG + " register body(raw): "
                                        + content.substring(0, show)
                                        + (content.length() > show ? "..." : ""));
                            }
                            if (!sanitized.equals(content)) {
                                XposedBridge.log(TAG + " RequestBody.writeTo riskToken sanitized");
                            }
                            XposedHelpers.callMethod(param.args[0], "writeUtf8", sanitized);
                            param.setResult(null);
                        }
                    }
            );
        } catch (Throwable t) {
            XposedBridge.log(TAG + " skip RequestBody.writeTo: " + t.getMessage());
        }
    }

    private static void hookRetrofitGsonConverter(XC_LoadPackage.LoadPackageParam lpparam) {
        try {
            XposedHelpers.findAndHookMethod(
                    "retrofit2.converter.gson.GsonRequestBodyConverter",
                    lpparam.classLoader,
                    "convert",
                    Object.class,
                    new XC_MethodHook() {
                        @Override
                        protected void afterHookedMethod(MethodHookParam param) {
                            Object body = param.getResult();
                            if (body == null) {
                                return;
                            }
                            try {
                                ClassLoader cl = loaderFor(body, lpparam.classLoader);
                                Object buffer = XposedHelpers.newInstance(
                                        findClassSafe(cl, "okio.Buffer"));
                                XposedHelpers.callMethod(body, "writeTo", buffer);
                                String content = (String) XposedHelpers.callMethod(buffer, "readUtf8");
                                String sanitized = MariBankRiskTokenUtil.sanitizeAllInText(content);
                                if (!sanitized.equals(content)) {
                                    Object mediaType = XposedHelpers.callMethod(body, "contentType");
                                    param.setResult(XposedHelpers.callStaticMethod(
                                            XposedHelpers.findClass("okhttp3.RequestBody", lpparam.classLoader),
                                            "create",
                                            mediaType,
                                            sanitized));
                                    XposedBridge.log(TAG + " GsonRequestBodyConverter riskToken sanitized");
                                }
                            } catch (Throwable ignored) {
                            }
                        }
                    }
            );
        } catch (Throwable t) {
            XposedBridge.log(TAG + " skip GsonRequestBodyConverter: " + t.getMessage());
        }
    }

    /** 出站 body 写入 okio.Buffer 时净化 riskToken（不 Hook proceed）。 */
    private static void hookOkioBufferWrite(XC_LoadPackage.LoadPackageParam lpparam) {
        XC_MethodHook sanitizeHook = new XC_MethodHook() {
            @Override
            protected void beforeHookedMethod(MethodHookParam param) {
                if (param.args.length == 0 || !(param.args[0] instanceof String)) {
                    return;
                }
                String s = (String) param.args[0];
                if (!s.contains("|")) {
                    return;
                }
                String sanitized = MariBankRiskTokenUtil.sanitizeAllInText(s);
                if (!sanitized.equals(s)) {
                    param.args[0] = sanitized;
                    XposedBridge.log(TAG + " okio.Buffer write riskToken sanitized");
                }
            }
        };
        try {
            Class<?> bufferClass = findClassSafe(lpparam.classLoader, "okio.Buffer");
            XposedHelpers.findAndHookMethod(
                    bufferClass, "writeUtf8", String.class, sanitizeHook);
        } catch (Throwable t) {
            XposedBridge.log(TAG + " skip okio.Buffer.writeUtf8: " + t.getMessage());
        }
        try {
            Class<?> bufferClass = findClassSafe(lpparam.classLoader, "okio.Buffer");
            XposedHelpers.findAndHookMethod(
                    bufferClass, "writeString",
                    String.class, java.nio.charset.Charset.class, sanitizeHook);
        } catch (Throwable ignored) {
        }
        try {
            Class<?> bufferClass = findClassSafe(lpparam.classLoader, "okio.Buffer");
            XposedHelpers.findAndHookMethod(
                    bufferClass, "write",
                    byte[].class, int.class, int.class,
                    new XC_MethodHook() {
                        @Override
                        protected void beforeHookedMethod(MethodHookParam param) {
                            byte[] data = (byte[]) param.args[0];
                            int off = (int) param.args[1];
                            int len = (int) param.args[2];
                            byte[] sanitized = MariBankRiskTokenUtil.sanitizeBytes(data, off, len);
                            if (sanitized != data) {
                                param.args[0] = sanitized;
                                param.args[1] = 0;
                                param.args[2] = sanitized.length;
                                XposedBridge.log(TAG + " okio.Buffer write bytes riskToken sanitized");
                            }
                        }
                    });
        } catch (Throwable ignored) {
        }
    }

    /** Retrofit 常用 byte[] RequestBody。 */
    private static void hookOutgoingRequestBytes(XC_LoadPackage.LoadPackageParam lpparam) {
        XC_MethodHook byteHook = new XC_MethodHook() {
            @Override
            protected void beforeHookedMethod(MethodHookParam param) {
                for (int i = 0; i < param.args.length; i++) {
                    if (!(param.args[i] instanceof byte[])) {
                        continue;
                    }
                    byte[] data = (byte[]) param.args[i];
                    byte[] sanitized = MariBankRiskTokenUtil.sanitizeBytes(data, 0, data.length);
                    if (sanitized != data) {
                        param.args[i] = sanitized;
                        XposedBridge.log(TAG + " outbound RequestBody bytes riskToken sanitized");
                    }
                }
            }
        };
        try {
            XposedHelpers.findAndHookMethod(
                    "okhttp3.RequestBody",
                    lpparam.classLoader,
                    "create",
                    "okhttp3.MediaType",
                    byte[].class,
                    byteHook);
        } catch (Throwable ignored) {
        }
        try {
            XposedHelpers.findAndHookMethod(
                    "okhttp3.RequestBody",
                    lpparam.classLoader,
                    "create",
                    byte[].class,
                    "okhttp3.MediaType",
                    byteHook);
        } catch (Throwable ignored) {
        }
    }

    private static void hookOkHttpResponseUrl(XC_LoadPackage.LoadPackageParam lpparam) {
        try {
            XposedHelpers.findAndHookMethod(
                    "okhttp3.Response",
                    lpparam.classLoader,
                    "body",
                    new XC_MethodHook() {
                        @Override
                        protected void afterHookedMethod(MethodHookParam param) {
                            try {
                                Object request = XposedHelpers.callMethod(param.thisObject, "request");
                                Object url = XposedHelpers.callMethod(request, "url");
                                if (url != null) {
                                    CURRENT_REQUEST_URL.set(String.valueOf(url));
                                }
                            } catch (Throwable ignored) {
                            }
                        }
                    }
            );
        } catch (Throwable t) {
            XposedBridge.log(TAG + " skip okhttp Response.body url: " + t.getMessage());
        }
    }

    /** 出站 JSON 请求体：注册接口 /uapi/v2/register 会携带 riskToken。 */
    private static void hookOutgoingRequestBody(XC_LoadPackage.LoadPackageParam lpparam) {
        XC_MethodHook sanitizeHook = new XC_MethodHook() {
            @Override
            protected void beforeHookedMethod(MethodHookParam param) {
                for (int i = 0; i < param.args.length; i++) {
                    if (param.args[i] instanceof String) {
                        String body = (String) param.args[i];
                        if (body.contains("riskToken") && body.contains("|")) {
                            String sanitized = MariBankRiskTokenUtil.sanitizeAllInText(body);
                            if (!sanitized.equals(body)) {
                                param.args[i] = sanitized;
                                XposedBridge.log(TAG + " outbound RequestBody riskToken sanitized");
                            }
                        } else if (body.contains("deviceToken") && body.contains("|")) {
                            String sanitized = MariBankRiskTokenUtil.sanitizeAllInText(body);
                            if (!sanitized.equals(body)) {
                                param.args[i] = sanitized;
                                XposedBridge.log(TAG + " outbound RequestBody deviceToken sanitized");
                            }
                        }
                    }
                }
            }
        };
        String[][] createSigs = {
                {"okhttp3.MediaType", "java.lang.String"},
                {"java.lang.String", "okhttp3.MediaType"},
        };
        for (String[] sig : createSigs) {
            try {
                XposedHelpers.findAndHookMethod(
                        "okhttp3.RequestBody",
                        lpparam.classLoader,
                        "create",
                        sig[0],
                        sig[1],
                        sanitizeHook);
            } catch (Throwable ignored) {
            }
        }
    }

    private static void hookGsonRiskToken(XC_LoadPackage.LoadPackageParam lpparam) {
        XC_MethodHook gsonHook = new XC_MethodHook() {
            @Override
            protected void afterHookedMethod(MethodHookParam param) {
                if (!(param.getResult() instanceof String)) {
                    return;
                }
                String json = (String) param.getResult();
                if (json.contains("mobileNo") || json.contains("phoneNo")) {
                    int show = Math.min(json.length(), 500);
                    XposedBridge.log(TAG + " Gson.toJson mobile: "
                            + json.substring(0, show)
                            + (json.length() > show ? "..." : ""));
                }
                if (!json.contains("riskToken") && !json.contains("deviceToken")) {
                    return;
                }
                String sanitized = MariBankRiskTokenUtil.sanitizeAllInText(json);
                if (!sanitized.equals(json)) {
                    param.setResult(sanitized);
                    XposedBridge.log(TAG + " Gson.toJson riskToken sanitized");
                }
            }
        };
        try {
            XposedHelpers.findAndHookMethod(
                    "com.google.gson.Gson",
                    lpparam.classLoader,
                    "toJson",
                    Object.class,
                    gsonHook);
        } catch (Throwable t) {
            XposedBridge.log(TAG + " skip Gson.toJson: " + t.getMessage());
        }
        try {
            XposedHelpers.findAndHookMethod(
                    "com.google.gson.Gson",
                    lpparam.classLoader,
                    "toJson",
                    Object.class,
                    "java.lang.reflect.Type",
                    gsonHook);
        } catch (Throwable ignored) {
        }
    }

    private static void hookJsonRiskTokenPut(XC_LoadPackage.LoadPackageParam lpparam) {
        try {
            XposedHelpers.findAndHookMethod(
                    "org.json.JSONObject",
                    lpparam.classLoader,
                    "put",
                    String.class,
                    Object.class,
                    new XC_MethodHook() {
                        @Override
                        protected void beforeHookedMethod(MethodHookParam param) {
                            if (!"riskToken".equals(param.args[0]) && !"deviceToken".equals(param.args[0])) {
                                return;
                            }
                            if (!(param.args[1] instanceof String)) {
                                return;
                            }
                            param.args[1] = MariBankRiskTokenUtil.sanitizeRiskToken((String) param.args[1]);
                        }
                    }
            );
        } catch (Throwable t) {
            XposedBridge.log(TAG + " skip JSONObject.put riskToken: " + t.getMessage());
        }
    }

    /** SG：ADB / 无线调试检测 + Root 弹窗/Toast/全屏页拦截。 */
    private static void hookAdbBypass(XC_LoadPackage.LoadPackageParam lpparam) {
        hookAdbSettings(lpparam);
        hookAdbActivityEscape(lpparam);
    }

    private static void hookAdbSettings(XC_LoadPackage.LoadPackageParam lpparam) {
        XC_MethodHook fakeDisabled = new XC_MethodHook() {
            @Override
            protected void beforeHookedMethod(MethodHookParam param) {
                if (param.args.length < 2 || !(param.args[1] instanceof String)) {
                    return;
                }
                String key = (String) param.args[1];
                if (!isAdbSettingKey(key)) {
                    return;
                }
                Class<?> ret = ((Method) param.method).getReturnType();
                if (ret == int.class || ret == Integer.class) {
                    param.setResult(0);
                } else if (ret == long.class || ret == Long.class) {
                    param.setResult(0L);
                } else if (ret == String.class) {
                    param.setResult("0");
                }
                XposedBridge.log(TAG + " faked Settings key=" + key);
            }
        };

        String[][] targets = {
                {"android.provider.Settings$Global", "getInt"},
                {"android.provider.Settings$Global", "getLong"},
                {"android.provider.Settings$Global", "getString"},
                {"android.provider.Settings$Secure", "getInt"},
                {"android.provider.Settings$Secure", "getString"},
                {"android.provider.Settings$System", "getInt"},
        };
        for (String[] target : targets) {
            try {
                Class<?> clazz = XposedHelpers.findClass(target[0], lpparam.classLoader);
                for (Method method : clazz.getDeclaredMethods()) {
                    if (!target[1].equals(method.getName())) {
                        continue;
                    }
                    Class<?>[] params = method.getParameterTypes();
                    if (params.length >= 2 && ContentResolver.class.isAssignableFrom(params[0])
                            && params[1] == String.class) {
                        XposedBridge.hookMethod(method, fakeDisabled);
                    }
                }
            } catch (Throwable t) {
                XposedBridge.log(TAG + " skip Settings hook " + target[0] + ": " + t.getMessage());
            }
        }
    }

    /** 若全屏/RN 页已展示 ADB 拦截文案，直接 finish 退出该 Activity。 */
    private static void hookAdbActivityEscape(XC_LoadPackage.LoadPackageParam lpparam) {
        try {
            XposedHelpers.findAndHookMethod(
                    Activity.class,
                    "onResume",
                    new XC_MethodHook() {
                        @Override
                        protected void afterHookedMethod(MethodHookParam param) {
                            Activity activity = (Activity) param.thisObject;
                            if (activity == null || activity.isFinishing()) {
                                return;
                            }
                            String name = activity.getClass().getName();
                            if (name.contains("SafeModeRecoverActivity")) {
                                return;
                            }
                            String text = extractActivityText(activity);
                            if (isAdbBlockText(text)) {
                                XposedBridge.log(TAG + " finish adb block activity: " + name);
                                activity.finish();
                            }
                        }
                    }
            );
        } catch (Throwable t) {
            XposedBridge.log(TAG + " adb activity hook failed: " + t.getMessage());
        }
    }

    private static String extractActivityText(Activity activity) {
        StringBuilder sb = new StringBuilder();
        try {
            CharSequence title = activity.getTitle();
            if (title != null) {
                sb.append(title);
            }
        } catch (Throwable ignored) {
        }
        try {
            if (activity.getWindow() != null) {
                collectTextViews(activity.getWindow().getDecorView(), sb);
            }
        } catch (Throwable ignored) {
        }
        return sb.toString();
    }

    private static boolean isAdbSettingKey(String key) {
        if (key == null) {
            return false;
        }
        String lower = key.toLowerCase();
        return lower.contains("adb")
                || "development_settings_enabled".equals(lower)
                || lower.contains("wireless_debug");
    }

    private static boolean isAdbBlockText(String text) {
        if (text == null || text.isEmpty()) {
            return false;
        }
        String lower = text.toLowerCase();
        return lower.contains("adb/wireless adb")
                || lower.contains("wireless adb detected")
                || lower.contains("usb debugging")
                || lower.contains("wireless debugging")
                || lower.contains("turn off adb")
                || lower.contains("third parties to access")
                || lower.contains("safeguard your banking")
                || (lower.contains("adb") && lower.contains("detect"));
    }

    private static boolean isEnvironmentBlockText(String text) {
        return isRootBlockText(text) || isAdbBlockText(text);
    }

    private static void hookSafeModeDialog(XC_LoadPackage.LoadPackageParam lpparam) {
        try {
            XposedHelpers.findAndHookMethod(
                    "com.shopee.bke.lib.safemode.activity.SafeModeRecoverActivity",
                    lpparam.classLoader,
                    "onCreate",
                    "android.os.Bundle",
                    new XC_MethodHook() {
                        @Override
                        protected void beforeHookedMethod(MethodHookParam param) {
                            XposedHelpers.callMethod(param.thisObject, "finish");
                            param.setResult(null);
                        }
                    }
            );
        } catch (Throwable ignored) {
        }
    }

    /** 拦截 Root/Hook/模拟器 警告弹窗与 Toast（文案见 bke_toast_not_support_*）。 */
    private static void hookRootDialogBlock(XC_LoadPackage.LoadPackageParam lpparam) {
        XC_MethodHook blankRootTextHook = new XC_MethodHook() {
            @Override
            protected void beforeHookedMethod(MethodHookParam param) {
                if (param.args.length > 0 && isEnvironmentBlockText(String.valueOf(param.args[0]))) {
                    param.args[0] = " ";
                    XposedBridge.log(TAG + " blanked env block dialog message");
                }
            }
        };

        String[] messageSetters = {
                "android.app.AlertDialog$Builder",
                "androidx.appcompat.app.AlertDialog$Builder",
                "com.shopee.bke.lib.commonui.widget.CommonDialog$Builder",
        };
        for (String className : messageSetters) {
            try {
                XposedHelpers.findAndHookMethod(
                        className, lpparam.classLoader, "setMessage", CharSequence.class, blankRootTextHook);
            } catch (Throwable ignored) {
            }
            try {
                XposedHelpers.findAndHookMethod(
                        className, lpparam.classLoader, "setTitle", CharSequence.class, blankRootTextHook);
            } catch (Throwable ignored) {
            }
        }

        try {
            XposedHelpers.findAndHookMethod(
                    Resources.class,
                    "getString",
                    int.class,
                    new XC_MethodHook() {
                        @Override
                        protected void afterHookedMethod(MethodHookParam param) {
                            String s = (String) param.getResult();
                            if (isEnvironmentBlockText(s)) {
                                param.setResult(" ");
                                XposedBridge.log(TAG + " blanked env block string resource");
                            }
                        }
                    }
            );
        } catch (Throwable t) {
            XposedBridge.log(TAG + " getString hook failed: " + t.getMessage());
        }

        try {
            XposedHelpers.findAndHookMethod(
                    Dialog.class,
                    "show",
                    new XC_MethodHook() {
                        @Override
                        protected void beforeHookedMethod(MethodHookParam param) {
                            Dialog dialog = (Dialog) param.thisObject;
                            if (isEnvironmentBlockText(extractDialogText(dialog))) {
                                XposedBridge.log(TAG + " blocked env Dialog.show: "
                                        + dialog.getClass().getSimpleName());
                                param.setResult(null);
                            }
                        }
                    }
            );
        } catch (Throwable t) {
            XposedBridge.log(TAG + " Dialog.show hook failed: " + t.getMessage());
        }

        try {
            XposedHelpers.findAndHookMethod(
                    Toast.class,
                    "show",
                    new XC_MethodHook() {
                        @Override
                        protected void beforeHookedMethod(MethodHookParam param) {
                            String text = extractToastText((Toast) param.thisObject);
                            if (isSecurityBlockText(text)) {
                                XposedBridge.log(TAG + " security block Toast: " + text);
                                logBriefStack();
                            }
                            if (isRootBlockText(text)) {
                                XposedBridge.log(TAG + " blocked root Toast.show");
                                param.setResult(null);
                            } else if (isAdbBlockText(text)) {
                                XposedBridge.log(TAG + " blocked adb Toast.show");
                                param.setResult(null);
                            }
                        }
                    }
            );
        } catch (Throwable t) {
            XposedBridge.log(TAG + " Toast.show hook failed: " + t.getMessage());
        }
    }

    private static boolean isRootBlockText(String text) {
        if (text == null || text.isEmpty()) {
            return false;
        }
        String lower = text.toLowerCase();
        return lower.contains("rooted or jailbroken")
                || lower.contains("modified device")
                || lower.contains("magisk/xposed/frida")
                || lower.contains("cannot be accessed on such devices")
                || lower.contains("restore to factory settings")
                || lower.contains("does not support root")
                || lower.contains("support root device");
    }

    private static boolean isSecurityBlockText(String text) {
        if (text == null || text.isEmpty()) {
            return false;
        }
        String lower = text.toLowerCase();
        return lower.contains("temporarily blocked")
                || lower.contains("8424 8050");
    }

    private static void logBriefStack() {
        StackTraceElement[] stack = Thread.currentThread().getStackTrace();
        StringBuilder sb = new StringBuilder();
        int n = 0;
        for (StackTraceElement frame : stack) {
            String cn = frame.getClassName();
            if (cn.contains("miraclegarden") || cn.contains("lsposed") || cn.contains("XposedBridge")) {
                continue;
            }
            if (cn.startsWith("android.widget.") || cn.startsWith("android.view.")) {
                continue;
            }
            sb.append("\n  at ").append(cn).append(".").append(frame.getMethodName());
            if (++n >= 10) {
                break;
            }
        }
        XposedBridge.log(TAG + " stack:" + sb);
    }

    private static String extractDialogText(Dialog dialog) {
        StringBuilder sb = new StringBuilder();
        try {
            Object alert = XposedHelpers.getObjectField(dialog, "mAlert");
            if (alert != null) {
                appendFieldText(sb, alert, "mMessage");
                appendFieldText(sb, alert, "mTitle");
            }
        } catch (Throwable ignored) {
        }
        try {
            if (dialog.getWindow() != null) {
                collectTextViews(dialog.getWindow().getDecorView(), sb);
            }
        } catch (Throwable ignored) {
        }
        return sb.toString();
    }

    private static void appendFieldText(StringBuilder sb, Object target, String field) {
        try {
            Object value = XposedHelpers.getObjectField(target, field);
            if (value != null) {
                sb.append(value);
            }
        } catch (Throwable ignored) {
        }
    }

    private static void collectTextViews(View view, StringBuilder sb) {
        if (view instanceof TextView) {
            CharSequence text = ((TextView) view).getText();
            if (text != null) {
                sb.append(text);
            }
        }
        if (view instanceof ViewGroup) {
            ViewGroup group = (ViewGroup) view;
            for (int i = 0; i < group.getChildCount(); i++) {
                collectTextViews(group.getChildAt(i), sb);
            }
        }
    }

    private static String extractToastText(Toast toast) {
        try {
            View view = toast.getView();
            if (view instanceof TextView) {
                CharSequence text = ((TextView) view).getText();
                return text != null ? text.toString() : "";
            }
            if (view instanceof ViewGroup) {
                StringBuilder sb = new StringBuilder();
                collectTextViews(view, sb);
                return sb.toString();
            }
        } catch (Throwable ignored) {
        }
        try {
            Object text = XposedHelpers.getObjectField(toast, "mText");
            return text != null ? text.toString() : "";
        } catch (Throwable ignored) {
        }
        return "";
    }
}
