package com.miraclegarden.smsmessage.xposed.hook;

import android.app.Activity;
import android.app.Instrumentation;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.app.Application;
import android.app.Dialog;
import android.app.ActivityManager;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.os.Process;
import android.view.Window;
import android.view.WindowManager;

import android.os.Message;

import java.io.File;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.Proxy;
import java.util.Collections;
import java.util.Locale;
import java.util.Set;
import java.util.WeakHashMap;

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;

/**
 * TNG eWallet Root / Promon / JailBroken 检测绕过。
 * 包名：my.com.tngdigital.ewallet（v1.9.9+）
 */
public final class TngRootBypassHook {

    public static final String PACKAGE = "my.com.tngdigital.ewallet";
    private static final String TAG = "notiMessageHook/TngRoot";

    /** Promon 混淆包名：1.9.10 为 vhvlnqgy，旧版为 xwwqazamx。 */
    private static final String[] PROMON_PKG_PREFIXES = {"vhvlnqgy", "xwwqazamx"};

    private static final String SECURITY_ERROR_ACTIVITY =
            "my.com.tngdigital.common.security.ui.SecurityErrorActivity";

    private static final String[] BLOCKED_SUPPORT_MARKERS = {
            "36616543382169-rooting",
            "36616508159769-emulator",
            "36616480108697-malicious",
            "36616444815257-multiboxing",
            "45485757513113-private-space",
            "48493749288857-malware",
            "/articles/",
            "support.tngdigital.com.my/hc/",
    };
    private static volatile long lastBlockedSuicideAt = 0L;
    private static final long SOFT_CRASH_GUARD_MS = 10000L;

    private static final String[] REGISTRATION_FLOW_MARKERS = {
            "GuideActivity",
            "UserRegistrationMobileActivity",
            "UserOtpVerificationActivity",
            "UserRegistrationIdentityActivity",
            "UserRegistrationSixPinActivity",
            "UserRegistrationStrengthenPinActivity",
            "UserRegistrationSecurityQuestionActivity",
            "UserRegistrationSuccessActivity",
            "UserSearchCallingCodeActivity",
            "UserPinActivity",
            "UserSecurePinActivity",
            "UserPinVerifyActivity",
            "EmailOtpVerificationActivity",
    };

    private static final String[] REGISTRATION_RPC_MARKERS = {
            "phonecheck", "com.abl.wallet.phone", "com.abl.wallet.otp",
            "customer.registration", "customer.verify", "customer.login",
            "login.options", "callingcode", "pin.token", "module.whitelist",
            "secauth",
    };

    private TngRootBypassHook() {
    }

    public static boolean isTargetPackage(String packageName) {
        return PACKAGE.equals(packageName);
    }

    public static void install(XC_LoadPackage.LoadPackageParam lpparam) {
        if (!isTargetPackage(lpparam.packageName)) {
            return;
        }
        XposedBridge.log(TAG + " install for " + lpparam.packageName
                + " pid=" + Process.myPid()
                + " proc=" + getProcessName());

        hookEarlyAttachLog(lpparam);
        hookConscryptStatsLogGuard();

        // 登录优先：藏 root + 拦自杀 Java 链 + bl#a/b，不碰 native-bridge 探测短路。
        hookSplashForceLogin(lpparam);
        hookLoginDismissSplash(lpparam);
        // 点区号会走 onCountryClick → i7.l loading；真 show → HWUI setName/gralloc abort
        hookLoadingDialogSkip();
        // setName 一律 noop（A16+Zygisk 下 native setName→dlopen libandroid 易 ART abort）。
        // 区号 Compose 必须 HW：软件绘制会 IllegalArgumentException(hardware bitmaps)。
        hookHardwareRendererSetNameNoop();
        hookCallingCodeAllowHwSurface(lpparam);

        RootBypassHelper.hookFileExists(lpparam);
        RootBypassHelper.hookRuntimeExec(lpparam);
        RootBypassHelper.hookSystemGetProperty(lpparam);
        ProcMapsFilterHook.install(lpparam);

        hookAntiSuicide();
        hookPromonSuicideUpstream(lpparam);
        hookUncaughtPromonException(lpparam);
        hookKillApplicationHandler(lpparam);
        hookBlockSecurityErrorLaunch(lpparam);
        hookPromonNativeGuard(lpparam);
        hookPromonLifecycle(lpparam);
        hookActivityThreadExit(lpparam);
        hookFinishAllActivityAndKillApp(lpparam);
        hookSecurityUrlOpeners(lpparam);
        hookJailBroken(lpparam);
        hookJailBrokenRpc(lpparam);
        hookAppSecurityManager(lpparam);
        hookAppSecurityCallbacks(lpparam);
        hookSecurityErrorActivity(lpparam);
        // 首页强制 eKYC「验证您的帐户」— 测试期直接跳过
        hookHomeEkycVerifySkip(lpparam);
        // seccomp 开着时必须 stub TigerTally init，否则 fork getprop 永不退出 → App.onCreate ANR
        hookTigerTally(lpparam);
        hookTigerTallyAppWrappers(lpparam);
        XposedBridge.log(TAG + " login-first hooks armed");
    }

    /** 进程启动最早打点，便于确认 LSPosed 是否注入（注册闪退常因 hook 未生效）。 */
    private static void hookEarlyAttachLog(XC_LoadPackage.LoadPackageParam lpparam) {
        try {
            XposedHelpers.findAndHookMethod(Application.class, "attachBaseContext", Context.class,
                    new XC_MethodHook() {
                        @Override
                        protected void afterHookedMethod(MethodHookParam param) {
                            Context ctx = (Context) param.args[0];
                            if (ctx != null && PACKAGE.equals(ctx.getPackageName())) {
                                XposedBridge.log(TAG + " attachBaseContext pid="
                                        + Process.myPid());
                            }
                        }
                    });
        } catch (Throwable t) {
            XposedBridge.log(TAG + " early attach log failed: " + t.getMessage());
        }
    }

    /** Splash 卡住救援；新 schedule 会取消旧 Runnable。 */
    private static final Handler FORCE_LOGIN_HANDLER = new Handler(Looper.getMainLooper());
    private static Runnable pendingForceLoginRunnable;
    /** Splash 已离开（进 PIN/首页等）则取消救援。 */
    private static volatile boolean splashNavigationDone = false;

    /**
     * 仅当 Splash 超时仍停在自身时才救援，避免已登录冷启动被强拉回 UserLogin。
     * 有本地会话痕迹 → 优先 UserPin；否则 → UserLogin。
     */
    private static void scheduleSplashStuckRescue(
            final Context appCtx, final Activity splashAct, final String reason, final long delayMs) {
        if (appCtx == null) {
            return;
        }
        splashNavigationDone = false;
        if (pendingForceLoginRunnable != null) {
            FORCE_LOGIN_HANDLER.removeCallbacks(pendingForceLoginRunnable);
        }
        pendingForceLoginRunnable = new Runnable() {
            @Override
            public void run() {
                pendingForceLoginRunnable = null;
                try {
                    if (splashNavigationDone) {
                        XposedBridge.log(TAG + " skip splash rescue (" + reason + ", already left)");
                        return;
                    }
                    if (isTopActivityRegistrationFlow(appCtx)) {
                        XposedBridge.log(TAG + " skip splash rescue (" + reason + ", registration flow)");
                        return;
                    }
                    String top = getTopActivityClassName(appCtx);
                    if (top != null && !isSplashActivityName(top)) {
                        XposedBridge.log(TAG + " skip splash rescue (" + reason + ", on " + top + ")");
                        splashNavigationDone = true;
                        return;
                    }
                    if (!isUiVisibleForForceLogin(appCtx)) {
                        XposedBridge.log(TAG + " skip splash rescue (" + reason + ", no visible UI / BAL)");
                        return;
                    }
                    boolean hasSession = hasLocalLoginSession(appCtx);
                    String target = hasSession
                            ? "my.com.tngdigital.user.view.UserPinActivity"
                            : "my.com.tngdigital.user.view.UserLoginActivity";
                    Intent intent = new Intent();
                    intent.setClassName(PACKAGE, target);
                    intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP
                            | Intent.FLAG_ACTIVITY_SINGLE_TOP);
                    if (splashAct != null && !splashAct.isFinishing()) {
                        splashAct.startActivity(intent);
                        try {
                            splashAct.finish();
                        } catch (Throwable ignored) {
                        }
                        XposedBridge.log(TAG + " splash stuck → " + shortActivityName(target)
                                + " (" + reason + ", session=" + hasSession + ", from Splash)");
                    } else {
                        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
                        appCtx.startActivity(intent);
                        XposedBridge.log(TAG + " splash stuck → " + shortActivityName(target)
                                + " (" + reason + ", session=" + hasSession + ", from AppCtx)");
                    }
                    splashNavigationDone = true;
                } catch (Throwable t) {
                    XposedBridge.log(TAG + " splash rescue failed (" + reason + "): " + t.getMessage());
                }
            }
        };
        FORCE_LOGIN_HANDLER.postDelayed(pendingForceLoginRunnable, delayMs);
    }

    private static String shortActivityName(String className) {
        if (className == null) {
            return "?";
        }
        int dot = className.lastIndexOf('.');
        return dot >= 0 ? className.substring(dot + 1) : className;
    }

    private static boolean isSplashActivityName(String className) {
        return className != null
                && (className.endsWith(".SplashActivity") || className.contains(".SplashActivity"));
    }

    private static String getTopActivityClassName(Context ctx) {
        try {
            ActivityManager am = (ActivityManager) ctx.getSystemService(Context.ACTIVITY_SERVICE);
            if (am == null) {
                return null;
            }
            for (ActivityManager.AppTask task : am.getAppTasks()) {
                ActivityManager.RecentTaskInfo info = task.getTaskInfo();
                if (info == null || info.topActivity == null) {
                    continue;
                }
                if (PACKAGE.equals(info.topActivity.getPackageName())) {
                    return info.topActivity.getClassName();
                }
            }
        } catch (Throwable ignored) {
        }
        return null;
    }

    /**
     * 粗判本地是否已有登录痕迹（有则冷启动应走 PIN，而不是登录页）。
     * SharedPreferences 文件名/键含 session、token、user、pin、login 等即视为已登录。
     */
    private static boolean hasLocalLoginSession(Context ctx) {
        try {
            File prefsDir = new File(ctx.getApplicationInfo().dataDir, "shared_prefs");
            if (!prefsDir.isDirectory()) {
                return false;
            }
            File[] files = prefsDir.listFiles();
            if (files == null) {
                return false;
            }
            for (File f : files) {
                String name = f.getName().toLowerCase(Locale.US);
                if (!name.endsWith(".xml")) {
                    continue;
                }
                if (name.contains("session") || name.contains("token") || name.contains("user")
                        || name.contains("login") || name.contains("account")
                        || name.contains("auth") || name.contains("pin")
                        || name.contains("credential") || name.contains("wallet")) {
                    if (f.length() > 64) {
                        return true;
                    }
                }
                // 扫键名：任意 prefs 里出现登录相关 key
                if (prefsXmlLooksLikeLoggedIn(f)) {
                    return true;
                }
            }
        } catch (Throwable t) {
            XposedBridge.log(TAG + " hasLocalLoginSession failed: " + t.getMessage());
        }
        return false;
    }

    private static boolean prefsXmlLooksLikeLoggedIn(File xmlFile) {
        java.io.BufferedReader reader = null;
        try {
            reader = new java.io.BufferedReader(new java.io.FileReader(xmlFile));
            String line;
            int lines = 0;
            while ((line = reader.readLine()) != null && lines < 200) {
                lines++;
                String lower = line.toLowerCase(Locale.US);
                if ((lower.contains("name=\"") || lower.contains("name='"))
                        && (lower.contains("token") || lower.contains("session")
                        || lower.contains("userid") || lower.contains("user_id")
                        || lower.contains("loginid") || lower.contains("mobile")
                        || lower.contains("phonenumber") || lower.contains("islogin")
                        || lower.contains("logged") || lower.contains("access_token"))) {
                    // 排除空值
                    if (lower.contains(">true<") || lower.contains("value=\"true\"")
                            || (lower.contains("value=\"") && !lower.contains("value=\"\"")
                            && !lower.contains("value=\"0\"") && !lower.contains("value=\"false\""))
                            || (lower.contains(">") && lower.contains("</string>")
                            && !lower.contains("><"))) {
                        return true;
                    }
                    if (lower.contains("<string") && lower.contains("</string>")) {
                        int a = lower.indexOf('>');
                        int b = lower.lastIndexOf("</string>");
                        if (a >= 0 && b > a + 1 && (b - a) > 8) {
                            return true;
                        }
                    }
                    if (lower.contains("<boolean") && lower.contains("value=\"true\"")) {
                        return true;
                    }
                }
            }
        } catch (Throwable ignored) {
        } finally {
            if (reader != null) {
                try {
                    reader.close();
                } catch (Throwable ignored) {
                }
            }
        }
        return false;
    }

    /** 后台 Service 重启（如 Firebase SessionLifecycle）无可见 Activity，强拉会被 BAL 拦截。 */
    private static boolean isUiVisibleForForceLogin(Context ctx) {
        ActivityManager.RunningAppProcessInfo state = new ActivityManager.RunningAppProcessInfo();
        ActivityManager.getMyMemoryState(state);
        if (state.importance > ActivityManager.RunningAppProcessInfo.IMPORTANCE_VISIBLE) {
            return false;
        }
        try {
            ActivityManager am = (ActivityManager) ctx.getSystemService(Context.ACTIVITY_SERVICE);
            if (am != null) {
                for (ActivityManager.AppTask task : am.getAppTasks()) {
                    ActivityManager.RecentTaskInfo info = task.getTaskInfo();
                    if (info != null && info.topActivity != null
                            && PACKAGE.equals(info.topActivity.getPackageName())) {
                        return true;
                    }
                }
            }
        } catch (Throwable ignored) {
        }
        return state.importance <= ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND;
    }

    /**
     * Zygisk/命名空间下 android.util.StatsLog native 常 UnsatisfiedLinkError，
     * Conscrypt TLS 指标线程一写就炸 → ART fatal。直接 noop 指标写入。
     */
    private static void hookConscryptStatsLogGuard() {
        int hooked = 0;
        for (String className : new String[]{
                "com.android.org.conscrypt.metrics.StatsLogImpl",
                "com.android.org.conscrypt.metrics.ConscryptStatsLog",
                "com.google.android.gms.org.conscrypt.metrics.StatsLogImpl",
        }) {
            try {
                Class<?> clazz = Class.forName(className);
                for (Method method : clazz.getDeclaredMethods()) {
                    String n = method.getName();
                    if (!n.startsWith("write")
                            && !n.startsWith("report")
                            && !n.startsWith("count")
                            && !"startWriterThread".equals(n)) {
                        continue;
                    }
                    hookMethodNoopSilent(method);
                    hooked++;
                }
            } catch (Throwable ignored) {
            }
        }
        try {
            Class<?> statsLog = Class.forName("android.util.StatsLog");
            for (Method method : statsLog.getDeclaredMethods()) {
                String n = method.getName();
                if ("loadNativeLibrary".equals(n) || n.startsWith("write")) {
                    hookMethodNoopSilent(method);
                    hooked++;
                }
            }
        } catch (Throwable ignored) {
        }
        XposedBridge.log(TAG + " conscrypt/StatsLog guard hooked n=" + hooked);
    }

    /**
     * 区号 Compose Activity 生命周期标记。
     * 必须在 execStart/onCreate 前设为 true，供 enableHardwareAcceleration 放行。
     */
    private static volatile boolean callingCodeSurfaceActive = false;

    private static boolean isCaptchaDialog(String owner) {
        return owner != null
                && (owner.contains("CaptchaWebViewDialog") || owner.contains("com.aliyun.captcha"));
    }

    private static boolean isLoadingDialog(String owner) {
        return owner != null
                && (owner.contains("i7.") || owner.contains("Loading") || owner.contains("Progress"));
    }

    private static boolean isCallingCodeActivity(String className) {
        return className != null && className.contains("UserSearchCallingCodeActivity");
    }

    private static boolean isCallingCodeIntent(Intent intent) {
        if (intent == null) {
            return false;
        }
        if (intent.getComponent() != null) {
            String cn = intent.getComponent().getClassName();
            if (isCallingCodeActivity(cn)) {
                return true;
            }
        }
        String action = intent.getAction();
        return action != null && action.contains("CallingCode");
    }

    private static String shortStack(int maxFrames) {
        StackTraceElement[] stack = Thread.currentThread().getStackTrace();
        StringBuilder sb = new StringBuilder();
        int n = 0;
        for (StackTraceElement el : stack) {
            String cn = el.getClassName();
            if (cn.startsWith("java.") || cn.startsWith("dalvik.")
                    || cn.startsWith("android.os.") || cn.startsWith("de.robv.android.xposed")
                    || cn.contains("TngRootBypassHook") || cn.contains("LSPosed")
                    || cn.contains("XposedBridge")) {
                continue;
            }
            if (n > 0) {
                sb.append(" <- ");
            }
            sb.append(el.getClassName()).append("#").append(el.getMethodName())
                    .append(":").append(el.getLineNumber());
            if (++n >= maxFrames) {
                break;
            }
        }
        return sb.length() == 0 ? "(empty)" : sb.toString();
    }

    /** 已 skip show 的 loading Dialog，让 isShowing=true 避免业务层卡死。 */
    private static final Set<Dialog> skippedLoadingDialogs =
            Collections.newSetFromMap(new WeakHashMap<>());

    /**
     * i7.l 等 loading：一律 skip show + 伪装 isShowing。
     * Captcha 弹窗清 HW flag，避免 Android 16 gralloc abort。
     */
    private static void hookLoadingDialogSkip() {
        try {
            XposedHelpers.findAndHookMethod(Dialog.class, "show", new XC_MethodHook() {
                @Override
                protected void beforeHookedMethod(MethodHookParam param) {
                    Dialog dialog = (Dialog) param.thisObject;
                    String owner = dialog.getClass().getName();
                    if (isLoadingDialog(owner)) {
                        skippedLoadingDialogs.add(dialog);
                        XposedBridge.log(TAG + " Dialog.show skip loading " + owner
                                + " callingCode=" + callingCodeSurfaceActive);
                        param.setResult(null);
                        return;
                    }
                    if (isCaptchaDialog(owner)) {
                        try {
                            Window cw = dialog.getWindow();
                            if (cw != null) {
                                cw.clearFlags(WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED);
                            }
                        } catch (Throwable ignored) {
                        }
                        XposedBridge.log(TAG + " Dialog.show captcha software " + owner);
                    }
                }
            });
            XposedBridge.log(TAG + " hooked Dialog.show (skip loading only)");
        } catch (Throwable t) {
            XposedBridge.log(TAG + " Dialog.show hook failed: " + t.getMessage());
        }
        try {
            XposedHelpers.findAndHookMethod(Dialog.class, "isShowing", new XC_MethodHook() {
                @Override
                protected void beforeHookedMethod(MethodHookParam param) {
                    if (skippedLoadingDialogs.contains(param.thisObject)) {
                        param.setResult(true);
                    }
                }
            });
            XposedBridge.log(TAG + " hooked Dialog.isShowing (skipped loading)");
        } catch (Throwable t) {
            XposedBridge.log(TAG + " Dialog.isShowing hook failed: " + t.getMessage());
        }
        try {
            XposedHelpers.findAndHookMethod(Dialog.class, "dismiss", new XC_MethodHook() {
                @Override
                protected void beforeHookedMethod(MethodHookParam param) {
                    String owner = param.thisObject.getClass().getName();
                    if (isLoadingDialog(owner)) {
                        skippedLoadingDialogs.remove(param.thisObject);
                    }
                }
            });
            XposedBridge.log(TAG + " hooked Dialog.dismiss (loading cleanup)");
        } catch (Throwable t) {
            XposedBridge.log(TAG + " Dialog.dismiss hook failed: " + t.getMessage());
        }
    }

    /**
     * 一律跳过 HardwareRenderer.setName。
     * Pixel/A16 + Zygisk 命名空间下 native setName → dlopen("libandroid.so") 失败会 ART abort。
     */
    private static void hookHardwareRendererSetNameNoop() {
        try {
            XposedHelpers.findAndHookMethod(
                    "android.graphics.HardwareRenderer", null, "setName", String.class,
                    new XC_MethodHook() {
                        @Override
                        protected void beforeHookedMethod(MethodHookParam param) {
                            param.setResult(null);
                        }
                    });
            XposedBridge.log(TAG + " hooked HardwareRenderer.setName (always noop)");
        } catch (Throwable t) {
            XposedBridge.log(TAG + " HardwareRenderer.setName noop failed: " + t.getMessage());
        }
    }

    /**
     * 区号 Compose：标记 surface + 保证 HW flag（勿清、勿拦 enableHW）。
     * 软件绘制会崩：Software rendering doesn't support hardware bitmaps。
     */
    private static void hookCallingCodeAllowHwSurface(XC_LoadPackage.LoadPackageParam lpparam) {
        XC_MethodHook markOn = new XC_MethodHook() {
            @Override
            protected void beforeHookedMethod(MethodHookParam param) {
                Intent intent = extractIntent(param.args);
                if (isCallingCodeIntent(intent)) {
                    callingCodeSurfaceActive = true;
                    XposedBridge.log(TAG + " callingCode surface ON (HW path)");
                }
            }
        };
        try {
            XposedHelpers.findAndHookMethod(
                    Activity.class, "startActivity", Intent.class, markOn);
            XposedHelpers.findAndHookMethod(
                    Activity.class, "startActivity", Intent.class, Bundle.class, markOn);
        } catch (Throwable t) {
            XposedBridge.log(TAG + " startActivity callingCode mark failed: " + t.getMessage());
        }
        try {
            XposedHelpers.findAndHookMethod(
                    Instrumentation.class,
                    "execStartActivity",
                    Context.class,
                    android.os.IBinder.class,
                    android.os.IBinder.class,
                    Activity.class,
                    Intent.class,
                    int.class,
                    Bundle.class,
                    new XC_MethodHook() {
                        @Override
                        protected void beforeHookedMethod(MethodHookParam param) {
                            Intent intent = (Intent) param.args[4];
                            if (isCallingCodeIntent(intent)) {
                                callingCodeSurfaceActive = true;
                                XposedBridge.log(TAG
                                        + " callingCode surface ON (execStart HW)");
                            }
                        }
                    });
        } catch (Throwable t) {
            XposedBridge.log(TAG + " execStart callingCode mark failed: " + t.getMessage());
        }
        try {
            XposedHelpers.findAndHookMethod(
                    Instrumentation.class,
                    "callActivityOnCreate",
                    Activity.class,
                    Bundle.class,
                    new XC_MethodHook() {
                        @Override
                        protected void beforeHookedMethod(MethodHookParam param) {
                            Activity activity = (Activity) param.args[0];
                            if (activity == null) {
                                return;
                            }
                            if (isCallingCodeActivity(activity.getClass().getName())) {
                                callingCodeSurfaceActive = true;
                                try {
                                    Window window = activity.getWindow();
                                    if (window != null) {
                                        window.addFlags(
                                                WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED);
                                    }
                                } catch (Throwable ignored) {
                                }
                                XposedBridge.log(TAG
                                        + " callingCode onCreate — keep HW for Compose");
                            }
                        }
                    });
            XposedHelpers.findAndHookMethod(
                    Instrumentation.class,
                    "callActivityOnDestroy",
                    Activity.class,
                    new XC_MethodHook() {
                        @Override
                        protected void afterHookedMethod(MethodHookParam param) {
                            Activity activity = (Activity) param.args[0];
                            if (activity != null
                                    && isCallingCodeActivity(activity.getClass().getName())) {
                                callingCodeSurfaceActive = false;
                                XposedBridge.log(TAG + " callingCode surface OFF");
                            }
                        }
                    });
        } catch (Throwable t) {
            XposedBridge.log(TAG + " callingCode lifecycle mark failed: " + t.getMessage());
        }
        XposedBridge.log(TAG + " callingCode allow-HW surface armed");
    }

    private static boolean isRegistrationFlowActivity(String className) {
        if (className == null) {
            return false;
        }
        for (String marker : REGISTRATION_FLOW_MARKERS) {
            if (className.endsWith("." + marker) || className.contains(marker)) {
                return true;
            }
        }
        return false;
    }

    private static String getProcessName() {
        try {
            Class<?> activityThread = Class.forName("android.app.ActivityThread");
            Method current = activityThread.getDeclaredMethod("currentProcessName");
            current.setAccessible(true);
            Object name = current.invoke(null);
            return name != null ? String.valueOf(name) : "?";
        } catch (Throwable t) {
            return "?";
        }
    }

    private static boolean isTopActivityRegistrationFlow(Context ctx) {
        try {
            android.app.ActivityManager am =
                    (android.app.ActivityManager) ctx.getSystemService(Context.ACTIVITY_SERVICE);
            if (am == null) {
                return false;
            }
            for (android.app.ActivityManager.AppTask task : am.getAppTasks()) {
                android.app.ActivityManager.RecentTaskInfo info = task.getTaskInfo();
                if (info == null || info.topActivity == null) {
                    continue;
                }
                if (isRegistrationFlowActivity(info.topActivity.getClassName())) {
                    return true;
                }
            }
        } catch (Throwable ignored) {
        }
        return false;
    }

    /**
     * 强拉 Login 后系统 Splash 遮罩常挂在 UserLogin 上（windows=Splash Screen），
     * 导致复进「未响应」。onCreate/onResume 强制 dismiss。
     */
    private static void hookLoginDismissSplash(XC_LoadPackage.LoadPackageParam lpparam) {
        final String login = "my.com.tngdigital.user.view.UserLoginActivity";
        try {
            XC_MethodHook dismissHook = new XC_MethodHook() {
                @Override
                protected void afterHookedMethod(MethodHookParam param) {
                    Activity activity = (Activity) param.args[0];
                    if (activity == null) {
                        return;
                    }
                    String name = activity.getClass().getName();
                    if (!login.equals(name) && !name.endsWith(".UserLoginActivity")) {
                        return;
                    }
                    dismissSplashScreen(activity);
                }
            };
            XposedHelpers.findAndHookMethod(
                    Instrumentation.class, "callActivityOnCreate",
                    Activity.class, Bundle.class, dismissHook);
            XposedHelpers.findAndHookMethod(
                    Instrumentation.class, "callActivityOnResume",
                    Activity.class, dismissHook);
            XposedBridge.log(TAG + " hooked UserLogin splash dismiss (onCreate+onResume)");
        } catch (Throwable t) {
            XposedBridge.log(TAG + " login splash dismiss hook failed: " + t.getMessage());
        }
    }

    /** 移除 Android 12+ / androidx 启动页遮罩，避免挡在 Login 前导致 ANR。 */
    private static void dismissSplashScreen(Activity activity) {
        activity.runOnUiThread(new Runnable() {
            @Override
            public void run() {
                try {
                    activity.reportFullyDrawn();
                } catch (Throwable ignored) {
                }
                if (Build.VERSION.SDK_INT >= 31) {
                    try {
                        android.window.SplashScreen ss = activity.getSplashScreen();
                        if (ss != null) {
                            ss.setOnExitAnimationListener(
                                    splashScreenView -> splashScreenView.remove());
                        }
                    } catch (Throwable ignored) {
                    }
                }
                try {
                    ClassLoader cl = activity.getClassLoader();
                    Class<?> splashCl = XposedHelpers.findClass(
                            "androidx.core.splashscreen.SplashScreen", cl);
                    Object splash = XposedHelpers.callStaticMethod(
                            splashCl, "installSplashScreen", activity);
                    Class<?> condCl = XposedHelpers.findClass(
                            "androidx.core.splashscreen.SplashScreen$KeepOnScreenCondition", cl);
                    Object keepOff = Proxy.newProxyInstance(
                            cl, new Class<?>[] { condCl },
                            (proxy, method, args) -> false);
                    XposedHelpers.callMethod(splash, "setKeepOnScreenCondition", keepOff);
                    Class<?> exitCl = XposedHelpers.findClass(
                            "androidx.core.splashscreen.SplashScreen$OnExitAnimationListener", cl);
                    Object exitListener = Proxy.newProxyInstance(
                            cl, new Class<?>[] { exitCl },
                            (proxy, method, args) -> {
                                if (args != null && args.length > 0 && args[0] != null) {
                                    XposedHelpers.callMethod(args[0], "remove");
                                }
                                return null;
                            });
                    XposedHelpers.callMethod(splash, "setOnExitAnimationListener", exitListener);
                } catch (Throwable t) {
                    XposedBridge.log(TAG + " dismissSplashScreen compat: " + t.getMessage());
                }
            }
        });
    }

    /**
     * Splash 卡住救援：不立刻强拉 Login。
     * onCreate 只调度延迟检查；若已自行跳到 PIN/首页则取消。
     * 超时仍停在 Splash（Promon 堵死）才救援。
     */
    private static void hookSplashForceLogin(XC_LoadPackage.LoadPackageParam lpparam) {
        final String splash = "my.com.tngdigital.ewallet.ui.SplashActivity";
        try {
            XposedHelpers.findAndHookMethod(
                    Instrumentation.class,
                    "callActivityOnCreate",
                    Activity.class,
                    Bundle.class,
                    new XC_MethodHook() {
                        @Override
                        protected void beforeHookedMethod(MethodHookParam param) {
                            final Activity activity = (Activity) param.args[0];
                            if (activity == null) {
                                return;
                            }
                            String name = activity.getClass().getName();
                            if (isSplashActivityName(name)) {
                                XposedBridge.log(TAG
                                        + " Splash.onCreate enter — schedule stuck rescue @4s");
                                scheduleSplashStuckRescue(
                                        activity.getApplicationContext(),
                                        activity,
                                        "Splash/beforeOnCreate",
                                        4000L);
                                return;
                            }
                            // 任何非 Splash Activity 创建 → 取消 Splash 救援
                            if (PACKAGE.equals(activity.getPackageName())) {
                                markSplashNavigationDone(name);
                            }
                        }

                        @Override
                        protected void afterHookedMethod(MethodHookParam param) {
                            final Activity activity = (Activity) param.args[0];
                            if (activity == null) {
                                return;
                            }
                            String name = activity.getClass().getName();
                            if (!isSplashActivityName(name)) {
                                return;
                            }
                            try {
                                activity.reportFullyDrawn();
                            } catch (Throwable ignored) {
                            }
                            dismissSplashScreen(activity);
                        }
                    });
            XposedHelpers.findAndHookMethod(
                    Instrumentation.class,
                    "callActivityOnResume",
                    Activity.class,
                    new XC_MethodHook() {
                        @Override
                        protected void afterHookedMethod(MethodHookParam param) {
                            Activity activity = (Activity) param.args[0];
                            if (activity == null) {
                                return;
                            }
                            String name = activity.getClass().getName();
                            if (PACKAGE.equals(activity.getPackageName())
                                    && !isSplashActivityName(name)) {
                                markSplashNavigationDone(name);
                            }
                        }
                    });
            XposedBridge.log(TAG + " hooked Splash stuck-rescue (not always→Login)");
        } catch (Throwable t) {
            XposedBridge.log(TAG + " Splash force hook failed: " + t.getMessage());
        }
    }

    private static void markSplashNavigationDone(String activityName) {
        if (splashNavigationDone) {
            return;
        }
        splashNavigationDone = true;
        if (pendingForceLoginRunnable != null) {
            FORCE_LOGIN_HANDLER.removeCallbacks(pendingForceLoginRunnable);
            pendingForceLoginRunnable = null;
            XposedBridge.log(TAG + " cancel splash rescue — now on " + activityName);
        }
    }

    private static volatile long lastSuicideLogAt = 0L;
    private static volatile int suicideBlockCount = 0;

    private static void hookAntiSuicide() {
        XC_MethodHook blockSelfKill = new XC_MethodHook() {
            @Override
            protected void beforeHookedMethod(MethodHookParam param) {
                if (!shouldBlockThisSuicideCall(param)) {
                    return;
                }
                lastBlockedSuicideAt = System.currentTimeMillis();
                suicideBlockCount++;
                long now = lastBlockedSuicideAt;
                if (now - lastSuicideLogAt > 3000L) {
                    lastSuicideLogAt = now;
                    XposedBridge.log(TAG + " blocked suicide #" + suicideBlockCount
                            + " " + param.method.getDeclaringClass().getSimpleName()
                            + "#" + param.method.getName()
                            + argsSummary(param.args)
                            + " thread=" + Thread.currentThread().getName()
                            + " stack=" + shortStack(6));
                }
                param.setResult(null);
                // 非主线程：打断自杀循环线程，避免只拦 kill 却卡死 UI
                freezeSuicideCallerThread();
            }
        };
        try {
            XposedHelpers.findAndHookMethod(Process.class, "killProcess", int.class, blockSelfKill);
        } catch (Throwable t) {
            XposedBridge.log(TAG + " killProcess hook failed: " + t.getMessage());
        }
        try {
            XposedHelpers.findAndHookMethod(System.class, "exit", int.class, blockSelfKill);
        } catch (Throwable t) {
            XposedBridge.log(TAG + " System.exit hook failed: " + t.getMessage());
        }
        try {
            XposedHelpers.findAndHookMethod(Runtime.class, "exit", int.class, blockSelfKill);
        } catch (Throwable t) {
            XposedBridge.log(TAG + " Runtime.exit hook failed: " + t.getMessage());
        }
        try {
            XposedHelpers.findAndHookMethod(Runtime.class, "halt", int.class, blockSelfKill);
        } catch (Throwable t) {
            XposedBridge.log(TAG + " Runtime.halt 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()) {
                                return;
                            }
                            if (signal != 9 && signal != 15 && signal != 6) {
                                return;
                            }
                            lastBlockedSuicideAt = System.currentTimeMillis();
                            suicideBlockCount++;
                            if (lastBlockedSuicideAt - lastSuicideLogAt > 3000L) {
                                lastSuicideLogAt = lastBlockedSuicideAt;
                                XposedBridge.log(TAG + " blocked sendSignal(self," + signal
                                        + ") #" + suicideBlockCount);
                            }
                            param.setResult(null);
                            freezeSuicideCallerThread();
                        }
                    });
        } catch (Throwable t) {
            XposedBridge.log(TAG + " sendSignal hook failed: " + t.getMessage());
        }

        XC_MethodHook blockFinish = new XC_MethodHook() {
            @Override
            protected void beforeHookedMethod(MethodHookParam param) {
                Activity activity = (Activity) param.thisObject;
                if (!isSecurityRelatedActivity(activity)) {
                    return;
                }
                if (System.currentTimeMillis() - lastBlockedSuicideAt > SOFT_CRASH_GUARD_MS) {
                    return;
                }
                XposedBridge.log(TAG + " blocked " + param.method.getName()
                        + " on " + activity.getClass().getSimpleName());
                param.setResult(null);
            }
        };
        try {
            XposedHelpers.findAndHookMethod(Activity.class, "finish", blockFinish);
            XposedHelpers.findAndHookMethod(Activity.class, "finishAffinity", blockFinish);
            XposedHelpers.findAndHookMethod(Activity.class, "finishAndRemoveTask", blockFinish);
        } catch (Throwable t) {
            XposedBridge.log(TAG + " finish hooks failed: " + t.getMessage());
        }
        XposedBridge.log(TAG + " anti-suicide armed (rate-limited + freeze caller)");
    }

    private static boolean shouldBlockThisSuicideCall(XC_MethodHook.MethodHookParam param) {
        // 真崩溃（KillApplicationHandler）放行，避免僵尸进程卡 Splash/黑屏
        if (stackHasClass("com.android.internal.os.RuntimeInit$KillApplicationHandler")) {
            return false;
        }
        String name = param.method.getName();
        if ("killProcess".equals(name)) {
            return ((Integer) param.args[0]) == Process.myPid();
        }
        // System/Runtime.exit/halt：一律拦（TNG 正常退出极少走这里）
        return true;
    }

    private static boolean stackHasClass(String className) {
        try {
            for (StackTraceElement e : Thread.currentThread().getStackTrace()) {
                if (className.equals(e.getClassName())) {
                    return true;
                }
            }
        } catch (Throwable ignored) {
        }
        return false;
    }

    private static String argsSummary(Object[] args) {
        if (args == null || args.length == 0) {
            return "()";
        }
        StringBuilder sb = new StringBuilder("(");
        for (int i = 0; i < args.length; i++) {
            if (i > 0) {
                sb.append(",");
            }
            sb.append(args[i]);
        }
        return sb.append(")").toString();
    }

    private static void freezeSuicideCallerThread() {
        Thread t = Thread.currentThread();
        if (t.getId() == Looper.getMainLooper().getThread().getId()) {
            return;
        }
        // 栈里有 Promon 包才打断，避免误伤业务线程
        if (!stackHasPromon()) {
            return;
        }
        try {
            t.interrupt();
        } catch (Throwable ignored) {
        }
    }

    private static boolean stackHasPromon() {
        try {
            for (StackTraceElement e : Thread.currentThread().getStackTrace()) {
                if (isPromonPackageClass(e.getClassName())) {
                    return true;
                }
            }
        } catch (Throwable ignored) {
        }
        return false;
    }

    /**
     * 自杀链上游：只补 AppSecurityManager 杀进程路径 noop。
     * 勿拦 Handler.post / 勿 short-circuit G/H 等：会弄死 ContentProvider 初始化，Splash 僵尸卡住。
     * bl/R/a 仍由 hookPromonNativeGuard 处理。
     */
    private static void hookPromonSuicideUpstream(XC_LoadPackage.LoadPackageParam lpparam) {
        hookAppSecurityKillPaths(lpparam);
        XposedBridge.log(TAG + " promon suicide upstream armed (kill-path only)");
    }

    /** AppSecurityManager 里明确杀进程/退出的路径直接 noop（勿匹配 destroy/shutdown）。 */
    private static void hookAppSecurityKillPaths(XC_LoadPackage.LoadPackageParam lpparam) {
        try {
            Class<?> clazz = XposedHelpers.findClass(
                    "my.com.tngdigital.common.security.shielding.AppSecurityManager",
                    lpparam.classLoader);
            int hooked = 0;
            for (Method method : clazz.getDeclaredMethods()) {
                String lower = method.getName().toLowerCase(Locale.US);
                if (!lower.contains("kill")
                        && !lower.contains("exit")
                        && !lower.contains("force")
                        && !lower.contains("die")
                        && !lower.contains("suicide")
                        && !lower.contains("terminate")) {
                    continue;
                }
                hookMethodNoopSilent(method);
                hooked++;
            }
            XposedBridge.log(TAG + " AppSecurityManager kill-path noop n=" + hooked);
        } catch (Throwable t) {
            XposedBridge.log(TAG + " AppSecurityManager kill-path failed: " + t.getMessage());
        }
    }

    private static boolean isSecurityRelatedActivity(Activity activity) {
        String name = activity.getClass().getName();
        return name.contains("SecurityError")
                || name.contains("security.ui")
                || name.contains("Promon");
    }

    private static void hookBlockSecurityErrorLaunch(XC_LoadPackage.LoadPackageParam lpparam) {
        XC_MethodHook blockLaunch = new XC_MethodHook() {
            @Override
            protected void beforeHookedMethod(MethodHookParam param) {
                Intent intent = extractIntent(param.args);
                if (intent != null && shouldBlockIntent(intent)) {
                    XposedBridge.log(TAG + " blocked intent via "
                            + param.method.getDeclaringClass().getSimpleName()
                            + "#" + param.method.getName()
                            + " data=" + intent.getDataString());
                    param.setResult(null);
                }
            }
        };

        hookStartActivityOverloads("android.app.Activity", null, blockLaunch);
        hookStartActivityOverloads("android.content.ContextWrapper", null, blockLaunch);
        hookStartActivityOverloads("android.app.ContextImpl", null, blockLaunch);

        try {
            XposedHelpers.findAndHookMethod(
                    Instrumentation.class,
                    "execStartActivity",
                    Context.class,
                    android.os.IBinder.class,
                    android.os.IBinder.class,
                    Activity.class,
                    Intent.class,
                    int.class,
                    Bundle.class,
                    new XC_MethodHook() {
                        @Override
                        protected void beforeHookedMethod(MethodHookParam param) {
                            Intent intent = (Intent) param.args[4];
                            if (shouldBlockIntent(intent)) {
                                XposedBridge.log(TAG + " blocked execStartActivity "
                                        + intent.getDataString());
                                param.setResult(-1);
                            }
                        }
                    });
        } catch (Throwable t) {
            XposedBridge.log(TAG + " execStartActivity hook failed: " + t.getMessage());
        }
    }

    /** Promon Shield 安全回调由 handle*Callback / bl short-circuit 处理，lifecycle 回调勿拦。 */

    private static void hookStartActivityOverloads(
            String className, ClassLoader classLoader, XC_MethodHook blockLaunch) {
        try {
            if ("android.app.Activity".equals(className)) {
                XposedHelpers.findAndHookMethod(
                        Activity.class, "startActivity", Intent.class, blockLaunch);
                XposedHelpers.findAndHookMethod(
                        Activity.class, "startActivity", Intent.class, Bundle.class, blockLaunch);
                return;
            }
            if (classLoader == null) {
                XposedHelpers.findAndHookMethod(
                        className, null, "startActivity", Intent.class, blockLaunch);
                XposedHelpers.findAndHookMethod(
                        className, null, "startActivity", Intent.class, Bundle.class, blockLaunch);
                return;
            }
            XposedHelpers.findAndHookMethod(
                    className, classLoader, "startActivity", Intent.class, blockLaunch);
            XposedHelpers.findAndHookMethod(
                    className, classLoader, "startActivity", Intent.class, Bundle.class, blockLaunch);
        } catch (Throwable t) {
            XposedBridge.log(TAG + " startActivity hooks failed for " + className + ": "
                    + t.getMessage());
        }
    }

    private static Intent extractIntent(Object[] args) {
        if (args == null) {
            return null;
        }
        for (Object arg : args) {
            if (arg instanceof Intent) {
                return (Intent) arg;
            }
        }
        return null;
    }

    private static boolean shouldBlockIntent(Intent intent) {
        if (intent == null) {
            return false;
        }
        if (isSecurityErrorIntent(intent) || isHomeEkycVerifyIntent(intent)) {
            return true;
        }
        Uri data = intent.getData();
        if (data != null && isBlockedSupportUrl(data.toString())) {
            return true;
        }
        String action = intent.getAction();
        if (Intent.ACTION_VIEW.equals(action) && data != null) {
            return isBlockedSupportUrl(data.toString());
        }
        return false;
    }

    private static boolean isHomeEkycVerifyIntent(Intent intent) {
        if (intent == null) {
            return false;
        }
        if (intent.getComponent() != null) {
            String cls = intent.getComponent().getClassName();
            if (cls != null && cls.contains("HomeEkycVerify")) {
                return true;
            }
        }
        return false;
    }

    private static boolean isBlockedSupportUrl(String url) {
        if (url == null || url.isEmpty()) {
            return false;
        }
        String lower = url.toLowerCase(Locale.US);
        if (!lower.contains("support.tngdigital.com.my")) {
            return false;
        }
        for (String marker : BLOCKED_SUPPORT_MARKERS) {
            if (lower.contains(marker)) {
                return true;
            }
        }
        return lower.contains("rooting") || lower.contains("jailbroken")
                || lower.contains("emulator") || lower.contains("malware");
    }

    /** 阻止 Promon 混淆层抛出 W/bd:16 并触发浏览器 fallback。 */
    private static void hookPromonNativeGuard(XC_LoadPackage.LoadPackageParam lpparam) {
        hookPromonBlSwallowExceptions(lpparam);
        for (String simple : new String[]{"W", "A", "bd"}) {
            hookPromonExceptionClass(lpparam, simple);
        }
        hookPromonRunnable(lpparam);
    }

    /**
     * Promon lifecycle：1.9.10 用 vhvlnqgy.u，旧版用 w。
     * 实测 afterHook 仍跑 native → DeleteLocalRef 损坏 → ART Runtime abort (signal 6)。
     * 登录优先：onActivity* / onApplication* 全部 noop，不调原 native。
     */
    private static void hookPromonLifecycle(XC_LoadPackage.LoadPackageParam lpparam) {
        for (String simple : new String[]{"w", "u"}) {
            hookPromonLifecycleClass(lpparam, simple);
        }
    }

    private static void hookPromonLifecycleClass(
            XC_LoadPackage.LoadPackageParam lpparam,
            String simpleName) {
        Class<?> lifecycleClass = findPromonClass(lpparam.classLoader, simpleName);
        if (lifecycleClass == null) {
            return;
        }
        final String lifecycleName = lifecycleClass.getName();
        try {
            int hooked = 0;
            for (Method method : lifecycleClass.getDeclaredMethods()) {
                String name = method.getName();
                if (!name.startsWith("onActivity") && !name.startsWith("onApplication")) {
                    continue;
                }
                // 不调原 native：避免 JNI DeleteLocalRef → ART abort
                XposedBridge.hookMethod(method, new XC_MethodHook() {
                    @Override
                    protected void beforeHookedMethod(MethodHookParam param) {
                        setSafeHookResult(param, method);
                    }
                });
                hooked++;
            }
            XposedBridge.log(TAG + " hooked " + hooked + " " + lifecycleName
                    + " lifecycle (noop)");
        } catch (Throwable t) {
            XposedBridge.log(TAG + " skip lifecycle " + lifecycleName + ": " + t.getMessage());
        }
    }

    /** 吞掉 :tools 进程里 Promon 抛出的未捕获 W:16。 */
    private static void hookUncaughtPromonException(XC_LoadPackage.LoadPackageParam lpparam) {
        try {
            XposedHelpers.findAndHookMethod(
                    Thread.class,
                    "dispatchUncaughtException",
                    Throwable.class,
                    new XC_MethodHook() {
                        @Override
                        protected void beforeHookedMethod(MethodHookParam param) {
                            Throwable t = (Throwable) param.args[0];
                            if (t == null) {
                                return;
                            }
                            if (isPromonThrowableName(t.getClass().getName())
                                    || isStatsLogNoise(t)) {
                                XposedBridge.log(TAG + " swallowed uncaught " + t.getClass().getSimpleName()
                                        + " in " + lpparam.processName
                                        + " msg=" + t.getMessage());
                                param.setResult(null);
                            }
                        }
                    });
            XposedBridge.log(TAG + " hooked Thread.dispatchUncaughtException");
        } catch (Throwable t) {
            XposedBridge.log(TAG + " dispatchUncaughtException hook failed: " + t.getMessage());
        }
    }

    /**
     * 登录优先：只 short-circuit bl#a / bl#b（文档有效路径）。全拦 bl/R 六个方法会破坏
     * u.onActivityCreated JNI → ART abort；a.run 仍单独 noop。
     */
    private static void hookPromonBlSwallowExceptions(XC_LoadPackage.LoadPackageParam lpparam) {
        for (String simple : new String[]{"bl", "R"}) {
            hookPromonClassShortCircuit(lpparam, simple);
        }
    }

    private static void hookPromonClassShortCircuit(
            XC_LoadPackage.LoadPackageParam lpparam, String simpleName) {
        Class<?> clazz = findPromonClass(lpparam.classLoader, simpleName);
        if (clazz == null) {
            return;
        }
        try {
            int hooked = 0;
            for (Method method : clazz.getDeclaredMethods()) {
                String methodName = method.getName();
                if (!"a".equals(methodName) && !"b".equals(methodName)) {
                    continue;
                }
                XposedBridge.hookMethod(method, new XC_MethodHook() {
                    @Override
                    protected void beforeHookedMethod(MethodHookParam param) {
                        setSafeHookResult(param, method);
                    }
                });
                hooked++;
            }
            if (hooked > 0) {
                XposedBridge.log(TAG + " hooked " + hooked + " " + clazz.getName()
                        + " method(s), a/b short-circuit only");
            }
        } catch (Throwable t) {
            XposedBridge.log(TAG + " " + clazz.getName() + " short-circuit failed: "
                    + t.getMessage());
        }
    }

    /** Promon 后台 Runnable（bl#b 检测线程），beforeHook 直接 noop，禁止跑 native。 */
    private static void hookPromonRunnable(XC_LoadPackage.LoadPackageParam lpparam) {
        Class<?> runnableClass = findPromonClass(lpparam.classLoader, "a");
        if (runnableClass == null) {
            XposedBridge.log(TAG + " Promon a runnable not found");
            return;
        }
        try {
            XposedHelpers.findAndHookMethod(runnableClass, "run", new XC_MethodHook() {
                @Override
                protected void beforeHookedMethod(MethodHookParam param) {
                    param.setResult(null);
                }
            });
            XposedBridge.log(TAG + " hooked " + runnableClass.getName() + ".run (short-circuit)");
        } catch (Throwable t) {
            XposedBridge.log(TAG + " Promon a.run hook failed: " + t.getMessage());
        }
    }

    private static void hookPromonExceptionClass(
            XC_LoadPackage.LoadPackageParam lpparam, String simpleName) {
        Class<?> promonExc = findPromonClass(lpparam.classLoader, simpleName);
        if (promonExc == null) {
            return;
        }
        final String className = promonExc.getName();
        try {
            for (Method method : promonExc.getDeclaredMethods()) {
                XposedBridge.hookMethod(method, new XC_MethodHook() {
                    @Override
                    protected void beforeHookedMethod(MethodHookParam param) {
                        XposedBridge.log(TAG + " swallowed " + className + "#" + method.getName());
                        setSafeHookResult(param, method);
                    }
                });
            }
            try {
                XposedHelpers.findAndHookConstructor(promonExc, int.class, new XC_MethodHook() {
                    @Override
                    protected void beforeHookedMethod(MethodHookParam param) {
                        XposedBridge.log(TAG + " swallowed " + className + "<init>(int)");
                        param.setResult(null);
                    }
                });
            } catch (Throwable ignored) {
                // constructor overload may differ
            }
            XposedBridge.log(TAG + " hooked Promon exception " + className);
        } catch (Throwable t) {
            XposedBridge.log(TAG + " " + className + " hook failed: " + t.getMessage());
        }
    }

    private static boolean isPromonThrowable(Throwable t, Class<?> promonExc) {
        if (t == null) {
            return false;
        }
        if (promonExc != null && promonExc.isInstance(t)) {
            return true;
        }
        return isPromonThrowableName(t.getClass().getName());
    }

    private static boolean isPromonThrowableName(String className) {
        if (className == null) {
            return false;
        }
        if (!isPromonPackageClass(className)) {
            return false;
        }
        // W/A/bd 等 Promon 异常；排除 bl/w/bg/R 等功能类
        int dot = className.lastIndexOf('.');
        if (dot < 0) {
            return false;
        }
        String simple = className.substring(dot + 1);
        if ("bd".equals(simple)) {
            return true;
        }
        return simple.length() == 1;
    }

    private static boolean isStatsLogNoise(Throwable t) {
        for (Throwable cur = t; cur != null; cur = cur.getCause()) {
            String name = cur.getClass().getName();
            String msg = cur.getMessage();
            if (name != null && name.contains("StatsLog")) {
                return true;
            }
            if (msg != null && msg.contains("StatsLog")) {
                return true;
            }
            if (cur instanceof UnsatisfiedLinkError
                    && msg != null
                    && (msg.contains("stats") || msg.contains("Stats"))) {
                return true;
            }
        }
        return false;
    }

    private static boolean isPromonPackageClass(String className) {
        for (String prefix : PROMON_PKG_PREFIXES) {
            if (className.startsWith(prefix + ".")) {
                return true;
            }
        }
        return false;
    }

    private static Class<?> findPromonClass(ClassLoader loader, String simpleName) {
        for (String prefix : PROMON_PKG_PREFIXES) {
            try {
                return XposedHelpers.findClass(prefix + "." + simpleName, loader);
            } catch (Throwable ignored) {
            }
        }
        return null;
    }

    /**
     * Aliyun TigerTally：与 Promon 并行的设备指纹/风控 SDK（libtiger_tally.so）。
     *
     * ANR 根因（2026-07-31）：init 链（TigerTallyAPI.init → initCommon → t.B.genericNt1，
     * native）会 fork 子进程跑 `getprop ro.build.version.sdk` 并用 pipe 等其 stdout。
     * Zygisk tng_exit_guard 的 exit_group seccomp 被 fork 继承 → getprop 永不退出 →
     * fread 永久阻塞 → StartupManager latch 卡死 → Application.onCreate ANR。
     *
     * 对策：对 init 链做 beforeHook 短路（跳过 native），启动不再 fork 等待。
     * 其它方法保留 afterHook 清异常软化。
     */
    private static void hookTigerTally(XC_LoadPackage.LoadPackageParam lpparam) {
        String[] classes = {
                "com.aliyun.TigerTally.TigerTallyAPI",
                "com.aliyun.TigerTally.t.B",
                "com.aliyun.TigerTally.t.C",
                "com.aliyun.TigerTally.s.A",
        };
        for (String className : classes) {
            try {
                Class<?> clazz = XposedHelpers.findClass(className, lpparam.classLoader);
                int hooked = 0;
                int shorted = 0;
                for (Method method : clazz.getDeclaredMethods()) {
                    if (Modifier.isAbstract(method.getModifiers())) {
                        continue;
                    }
                    // 不拦 Object 基础方法
                    String name = method.getName();
                    if ("equals".equals(name) || "hashCode".equals(name)
                            || "toString".equals(name) || "getClass".equals(name)) {
                        continue;
                    }
                    final boolean sc = isTigerShortCircuit(className, name);
                    XposedBridge.hookMethod(method, new XC_MethodHook() {
                        @Override
                        protected void beforeHookedMethod(MethodHookParam param) {
                            if (sc) {
                                XposedBridge.log(TAG + " TigerTally SC "
                                        + className + "#" + method.getName());
                                setSafeHookResult(param, method);
                            }
                        }

                        @Override
                        protected void afterHookedMethod(MethodHookParam param) {
                            if (sc) {
                                return;
                            }
                            if (param.hasThrowable()) {
                                XposedBridge.log(TAG + " TigerTally err "
                                        + className + "#" + method.getName()
                                        + " " + param.getThrowable().getClass().getSimpleName()
                                        + ": " + param.getThrowable().getMessage());
                                param.setThrowable(null);
                                setSafeHookResult(param, method);
                            }
                        }
                    });
                    if (sc) {
                        shorted++;
                    }
                    hooked++;
                }
                XposedBridge.log(TAG + " hooked TigerTally " + className
                        + " methods=" + hooked + " sc=" + shorted);
            } catch (Throwable t) {
                XposedBridge.log(TAG + " TigerTally " + className
                        + " hook skipped: " + t.getMessage());
            }
        }
    }

    /**
     * seccomp 开着时：必须 stub init/initCommon/genericNt1，否则 fork getprop 卡死。
     * seccomp 关且非登录优先：只 stub genericNt1 探测。
     */
    private static boolean isTigerShortCircuit(String className, String methodName) {
        if ("com.aliyun.TigerTally.TigerTallyAPI".equals(className)
                || "com.aliyun.TigerTally.t.B".equals(className)
                || "com.aliyun.TigerTally.t.C".equals(className)) {
            if ("init".equals(methodName) || "initCommon".equals(methodName)
                    || "genericNt1".equals(methodName) || "initialize".equals(methodName)) {
                return true;
            }
        }
        return "com.aliyun.TigerTally.t.B".equals(className)
                && "genericNt1".equals(methodName);
    }

    /** TNG 封装层：CaptchaInitializer / TigerTallyApiWrapper 直接 noop，避免 Startup latch 卡住。 */
    private static void hookTigerTallyAppWrappers(XC_LoadPackage.LoadPackageParam lpparam) {
        String[] classes = {
                "my.com.tngdigital.captcha.TigerTallyApiWrapper",
                "my.com.tngdigital.app.launcher.initializer.CaptchaInitializer",
        };
        for (String className : classes) {
            try {
                Class<?> clazz = XposedHelpers.findClass(className, lpparam.classLoader);
                int n = 0;
                for (Method method : clazz.getDeclaredMethods()) {
                    String name = method.getName();
                    if (!"initialize".equals(name) && !"init".equals(name)
                            && !"create".equals(name)) {
                        continue;
                    }
                    XposedBridge.hookMethod(method, new XC_MethodHook() {
                        @Override
                        protected void beforeHookedMethod(MethodHookParam param) {
                            XposedBridge.log(TAG + " TigerTally wrapper SC "
                                    + className + "#" + method.getName());
                            setSafeHookResult(param, method);
                        }
                    });
                    n++;
                }
                XposedBridge.log(TAG + " hooked TigerTally wrapper " + className + " n=" + n);
            } catch (Throwable t) {
                XposedBridge.log(TAG + " TigerTally wrapper " + className
                        + " skipped: " + t.getMessage());
            }
        }
    }

    /** 拦截 ActivityThread / Handler 触发的应用退出（Promon 常走 native→H.exit）。 */
    private static void hookActivityThreadExit(XC_LoadPackage.LoadPackageParam lpparam) {
        hookActivityThreadExitMethods(null);
        hookActivityThreadExitMethods(lpparam.classLoader);
        hookActivityThreadHandlerExit(lpparam);
        hookShutdownExit();
    }

    private static void hookActivityThreadExitMethods(ClassLoader classLoader) {
        try {
            Class<?> atClass = XposedHelpers.findClass("android.app.ActivityThread", classLoader);
            int hooked = 0;
            for (Method method : atClass.getDeclaredMethods()) {
                String name = method.getName();
                if (!isActivityThreadExitMethod(name)) {
                    continue;
                }
                XposedBridge.hookMethod(method, new XC_MethodHook() {
                    @Override
                    protected void beforeHookedMethod(MethodHookParam param) {
                        XposedBridge.log(TAG + " blocked ActivityThread#" + name);
                        setSafeHookResult(param, method);
                    }
                });
                hooked++;
                XposedBridge.log(TAG + " registered ActivityThread exit hook: " + name);
            }
            if (hooked > 0) {
                XposedBridge.log(TAG + " hooked " + hooked + " ActivityThread exit method(s)"
                        + (classLoader == null ? " [boot]" : " [app]"));
            }
        } catch (Throwable t) {
            XposedBridge.log(TAG + " ActivityThread exit hook failed: " + t.getMessage());
        }
    }

    /** 仅拦截真正执行退出的方法，勿匹配 isHandleSplashScreenExit 等查询方法。 */
    private static boolean isActivityThreadExitMethod(String name) {
        if (name.startsWith("is") || name.startsWith("get") || name.startsWith("has")) {
            return false;
        }
        String lower = name.toLowerCase(Locale.US);
        return lower.contains("exitapplication")
                || lower.equals("exit")
                || lower.contains("handleexit")
                || lower.contains("appexit");
    }

    /** 拦截 EXIT_APPLICATION 等 Handler 消息（Android 16 上 handleExitApplication 签名已变）。 */
    private static void hookActivityThreadHandlerExit(XC_LoadPackage.LoadPackageParam lpparam) {
        try {
            Class<?> handlerClass = XposedHelpers.findClass("android.app.ActivityThread$H", lpparam.classLoader);
            XposedHelpers.findAndHookMethod(
                    handlerClass,
                    "handleMessage",
                    Message.class,
                    new XC_MethodHook() {
                        @Override
                        protected void beforeHookedMethod(MethodHookParam param) {
                            Message msg = (Message) param.args[0];
                            if (msg == null) {
                                return;
                            }
                            int what = msg.what;
                            // AOSP: KILL_APPLICATION=109, EXIT_APPLICATION=111
                            if (what == 109 || what == 111) {
                                XposedBridge.log(TAG + " blocked ActivityThread$H msg.what=" + what);
                                param.setResult(null);
                            }
                        }
                    });
            XposedBridge.log(TAG + " hooked ActivityThread$H.handleMessage");
        } catch (Throwable t) {
            XposedBridge.log(TAG + " ActivityThread$H hook failed: " + t.getMessage());
        }
    }

    private static void hookShutdownExit() {
        try {
            Class<?> shutdownClass = Class.forName("java.lang.Shutdown");
            XposedHelpers.findAndHookMethod(
                    shutdownClass,
                    "exit",
                    int.class,
                    new XC_MethodHook() {
                        @Override
                        protected void beforeHookedMethod(MethodHookParam param) {
                            XposedBridge.log(TAG + " blocked Shutdown.exit(" + param.args[0] + ")");
                            param.setResult(null);
                        }
                    });
            XposedBridge.log(TAG + " hooked Shutdown.exit");
        } catch (Throwable t) {
            XposedBridge.log(TAG + " Shutdown.exit hook failed: " + t.getMessage());
        }
    }

    /** 阻止 RuntimeInit 默认 handler 因 Promon W 异常杀进程。 */
    private static void hookKillApplicationHandler(XC_LoadPackage.LoadPackageParam lpparam) {
        try {
            Class<?> handlerClass = XposedHelpers.findClass(
                    "com.android.internal.os.RuntimeInit$KillApplicationHandler",
                    lpparam.classLoader);
            XposedHelpers.findAndHookMethod(
                    handlerClass,
                    "uncaughtException",
                    Thread.class,
                    Throwable.class,
                    new XC_MethodHook() {
                        @Override
                        protected void beforeHookedMethod(MethodHookParam param) {
                            Throwable t = (Throwable) param.args[1];
                            if (t == null || !isPromonThrowableName(t.getClass().getName())) {
                                return;
                            }
                            XposedBridge.log(TAG + " blocked KillApplicationHandler for "
                                    + t.getClass().getSimpleName()
                                    + " proc=" + lpparam.processName);
                            param.setResult(null);
                        }
                    });
            XposedBridge.log(TAG + " hooked RuntimeInit$KillApplicationHandler");
        } catch (Throwable t) {
            XposedBridge.log(TAG + " KillApplicationHandler hook failed: " + t.getMessage());
        }
    }

    /** Promon / SecurityError 强制退出倒计时。 */
    /**
     * dexdump 定位：UnhandledEvent 走
     * AppSecurityManager.showSecurityScreenForState → _ContextKt.finishAllActivityAndKillApp。
     * 这是 UserLogin 之后 Java 层杀进程的主路径之一。
     */
    private static void hookFinishAllActivityAndKillApp(XC_LoadPackage.LoadPackageParam lpparam) {
        String[] classes = {
                "my.com.tngdigital.common.internal._ContextKt",
                "my.com.tngdigital.common.internal.ContextKt",
        };
        for (String className : classes) {
            try {
                Class<?> clazz = XposedHelpers.findClass(className, lpparam.classLoader);
                int hooked = 0;
                for (Method method : clazz.getDeclaredMethods()) {
                    String name = method.getName();
                    if (!name.toLowerCase(Locale.US).contains("finishall")
                            && !name.toLowerCase(Locale.US).contains("killapp")
                            && !name.equals("finishAllActivityAndKillApp")) {
                        continue;
                    }
                    XposedBridge.hookMethod(method, new XC_MethodHook() {
                        @Override
                        protected void beforeHookedMethod(MethodHookParam param) {
                            XposedBridge.log(TAG + " blocked " + className + "#" + name);
                            param.setResult(null);
                        }
                    });
                    hooked++;
                }
                if (hooked > 0) {
                    XposedBridge.log(TAG + " hooked " + hooked + " kill-app method(s) in " + className);
                }
            } catch (Throwable t) {
                XposedBridge.log(TAG + " skip " + className + ": " + t.getMessage());
            }
        }
    }

    /** 消费 Promon Java 层安全回调，避免检测后走 SecurityError / native fallback 退出链。 */
    private static void hookAppSecurityCallbacks(XC_LoadPackage.LoadPackageParam lpparam) {
        try {
            Class<?> clazz = XposedHelpers.findClass(
                    "my.com.tngdigital.common.security.shielding.AppSecurityManager",
                    lpparam.classLoader);
            int hooked = 0;
            for (Method method : clazz.getDeclaredMethods()) {
                String name = method.getName();
                if (!name.startsWith("handle") || !name.endsWith("Callback")) {
                    continue;
                }
                hookMethodNoopSilent(method);
                hooked++;
            }
            if (hooked > 0) {
                XposedBridge.log(TAG + " hooked " + hooked + " AppSecurityManager callback(s) (silent)");
            }
        } catch (Throwable t) {
            XposedBridge.log(TAG + " AppSecurityManager callbacks hook failed: " + t.getMessage());
        }
    }

    private static void hookSecurityUrlOpeners(XC_LoadPackage.LoadPackageParam lpparam) {
        String[] classes = {
                "my.com.tngdigital.common.security.shielding.AppSecurityManager",
                "my.com.tngdigital.common.security.ui.SecurityErrorBaseActivity",
                "my.com.tngdigital.common.security.ui.SecurityErrorActivity",
                "my.com.tngdigital.common.security.shielding.utils.HelperKt",
                "my.com.tngdigital.app.launcher.initializer.AppSecurityInitializer",
        };
        String[] methods = {
                "openSecurityUrl", "openUrlByBrowser", "openBrowser", "openUrl",
                "openWebUrl",
        };
        for (String className : classes) {
            for (String methodName : methods) {
                hookUrlOrCallbackMethod(lpparam, className, methodName);
            }
        }
    }

    private static void hookUrlOrCallbackMethod(
            XC_LoadPackage.LoadPackageParam lpparam, String className, String methodName) {
        try {
            Class<?> clazz = XposedHelpers.findClass(className, lpparam.classLoader);
            int hooked = 0;
            for (Method method : clazz.getDeclaredMethods()) {
                if (!methodName.equals(method.getName())) {
                    continue;
                }
                XposedBridge.hookMethod(method, new XC_MethodHook() {
                    @Override
                    protected void beforeHookedMethod(MethodHookParam param) {
                        if (param.args != null && param.args.length > 0
                                && param.args[0] instanceof String) {
                            String url = (String) param.args[0];
                            if (isBlockedSupportUrl(url)) {
                                XposedBridge.log(TAG + " blocked " + className
                                        + "#" + methodName + " url=" + url);
                                setSafeHookResult(param, method);
                                return;
                            }
                        }
                        if ("openSecurityUrl".equals(methodName)
                                || "openUrlByBrowser".equals(methodName)
                                || "openBrowser".equals(methodName)) {
                            XposedBridge.log(TAG + " blocked " + className + "#" + methodName);
                            setSafeHookResult(param, method);
                        }
                    }
                });
                hooked++;
            }
            if (hooked > 0) {
                XposedBridge.log(TAG + " hooked " + hooked + " " + className + "#" + methodName);
            }
        } catch (Throwable ignored) {
            // class/method may not exist in this APK split
        }
    }

    /** 高频回调（如 handleTapjackingCallback）禁止逐次打 log，避免复进主线程 ANR。 */
    private static void hookMethodNoopSilent(Method method) {
        XposedBridge.hookMethod(method, new XC_MethodHook() {
            @Override
            protected void beforeHookedMethod(MethodHookParam param) {
                setSafeHookResult(param, method);
            }
        });
    }

    /** methodName 为 null 时 hook 类内全部方法（用于 Promon 混淆类）。 */
    private static void hookAllMethodsNoop(
            XC_LoadPackage.LoadPackageParam lpparam, String className, String methodName) {
        try {
            Class<?> clazz = XposedHelpers.findClass(className, lpparam.classLoader);
            int hooked = 0;
            for (Method method : clazz.getDeclaredMethods()) {
                if (methodName != null && !methodName.equals(method.getName())) {
                    continue;
                }
                XposedBridge.hookMethod(method, new XC_MethodHook() {
                    @Override
                    protected void beforeHookedMethod(MethodHookParam param) {
                        XposedBridge.log(TAG + " blocked " + className + "#" + method.getName());
                        setSafeHookResult(param, method);
                    }
                });
                hooked++;
            }
            if (hooked > 0) {
                XposedBridge.log(TAG + " hooked " + hooked + " method(s) in " + className
                        + (methodName != null ? "#" + methodName : ""));
            }
        } catch (Throwable t) {
            XposedBridge.log(TAG + " skip " + className + ": " + t.getMessage());
        }
    }

    private static boolean isSecurityErrorIntent(Intent intent) {
        if (intent == null) {
            return false;
        }
        if (intent.getComponent() != null) {
            String cls = intent.getComponent().getClassName();
            if (cls != null && cls.contains("SecurityError")) {
                return true;
            }
        }
        String target = intent.getStringExtra("targetActivity");
        return target != null && target.contains("SecurityError");
    }

    private static void hookJailBroken(XC_LoadPackage.LoadPackageParam lpparam) {
        hookNoArgVoid(lpparam, "my.com.tngdigital.common.jailbrkendetect.JailBrokenManager",
                "showJailBrokenAlert");
        hookNoArgVoid(lpparam, "my.com.tngdigital.common.jailbrkendetect.JailBrokenManager",
                "showJailBrokenAlert$lambda$15");

        hookReturnFalse(lpparam, "my.com.tngdigital.common.jailbrkendetect.JailBrokenApiImpl",
                "isJailBroken");
        hookReturnFalse(lpparam, "my.com.tngdigital.common.jailbrkendetect.JailBrokenApiImpl",
                "detectJailBroken");
        hookReturnFalse(lpparam, "my.com.tngdigital.common.jailbrkendetect.JailBrokenManager",
                "isJailBroken");
        hookReturnFalse(lpparam, "my.com.tngdigital.common.jailbrkendetect.JailBrokenManager",
                "detectJailBroken");

        // 以下为 void 回调（非 boolean）；触发时弹窗/杀进程，必须 noop。
        hookNoArgVoid(lpparam, "my.com.tngdigital.common.jailbrkendetect.JailBrokenApiImpl",
                "onBlockStaticCheck");
        hookNoArgVoid(lpparam, "my.com.tngdigital.common.jailbrkendetect.JailBrokenApiImpl",
                "onShowPopupDisable");
        hookNoArgVoid(lpparam, "my.com.tngdigital.common.jailbrkendetect.JailBrokenApiImpl",
                "onEmptyToken");
        hookNoArgVoid(lpparam, "my.com.tngdigital.common.jailbrkendetect.JailBrokenApiImpl",
                "onRpcCheckValid");

        hookJailBrokenResult(lpparam);
    }

    /** 拦截 jail.broken.detect RPC：本地直接回调「未越狱」结果，不发网关。 */
    private static void hookJailBrokenRpc(XC_LoadPackage.LoadPackageParam lpparam) {
        String implClass = "my.com.tngdigital.common.jailbrkendetect.JailBrokenApiImpl";
        String[] shortCircuitMethods = {
                "rpcCheckJailBroken",
                "jailBrokenDetect",
                "rpcCheck",
        };
        try {
            Class<?> clazz = XposedHelpers.findClass(implClass, lpparam.classLoader);
            int hooked = 0;
            for (Method method : clazz.getDeclaredMethods()) {
                String name = method.getName();
                boolean match = false;
                for (String target : shortCircuitMethods) {
                    if (target.equals(name)) {
                        match = true;
                        break;
                    }
                }
                if (!match) {
                    continue;
                }
                XposedBridge.hookMethod(method, new XC_MethodHook() {
                    @Override
                    protected void beforeHookedMethod(MethodHookParam param) {
                        XposedBridge.log(TAG + " skip jail RPC " + implClass + "#" + name);
                        Object clean = buildCleanJailBrokenResult(lpparam.classLoader);
                        invokeJailBrokenCallbacks(param.args, clean);
                        setSafeHookResult(param, method);
                    }
                });
                hooked++;
            }
            if (hooked > 0) {
                XposedBridge.log(TAG + " hooked jail RPC short-circuit methods=" + hooked);
            }
        } catch (Throwable t) {
            XposedBridge.log(TAG + " jail RPC short-circuit failed: " + t.getMessage());
        }

        hookRpcInvocationJailBypass(lpparam);
    }

    private static void hookRpcInvocationJailBypass(XC_LoadPackage.LoadPackageParam lpparam) {
        String[] rpcClasses = {
                "my.com.tngdigital.common.aliservice.quake.TngdRpcInvocationHandlerHost",
                "com.alipay.imobile.network.quake.rpc.RpcInvocationHandler",
        };
        XC_MethodHook rpcHook = new XC_MethodHook() {
            @Override
            protected void beforeHookedMethod(MethodHookParam param) {
                String op = extractRpcOperation(param.args);
                if (op != null && (isRegistrationRpc(op) || op.contains("phoneCheck"))) {
                    XposedBridge.log(TAG + " RPC req op=" + op + " "
                            + describeRpcInvocation(param.args));
                }
                if (!isJailBrokenRpcInvocation(param.args)) {
                    return;
                }
                XposedBridge.log(TAG + " block jail.broken RPC invoke "
                        + describeRpcInvocation(param.args));
                Object clean = buildCleanJailBrokenResult(lpparam.classLoader);
                if (clean != null) {
                    param.setResult(clean);
                    return;
                }
                param.setResult(null);
            }

            @Override
            protected void afterHookedMethod(MethodHookParam param) {
                String op = extractRpcOperation(param.args);
                if (op == null || (!isRegistrationRpc(op) && !op.contains("phoneCheck"))) {
                    return;
                }
                if (param.hasThrowable()) {
                    XposedBridge.log(TAG + " RPC rsp op=" + op + " err "
                            + param.getThrowable().getClass().getSimpleName()
                            + ": " + param.getThrowable().getMessage());
                    return;
                }
                Object result = param.getResult();
                String snippet = result != null ? String.valueOf(result) : "null";
                if (snippet.length() > 800) {
                    snippet = snippet.substring(0, 800) + "...";
                }
                XposedBridge.log(TAG + " RPC rsp op=" + op + " body=" + snippet);
            }
        };
        for (String className : rpcClasses) {
            try {
                Class<?> clazz = XposedHelpers.findClass(className, lpparam.classLoader);
                int hooked = 0;
                for (Method method : clazz.getDeclaredMethods()) {
                    if (!"invoke".equals(method.getName())) {
                        continue;
                    }
                    XposedBridge.hookMethod(method, rpcHook);
                    hooked++;
                }
                if (hooked > 0) {
                    XposedBridge.log(TAG + " hooked RPC guard+log " + className
                            + " invoke=" + hooked);
                }
            } catch (Throwable t) {
                XposedBridge.log(TAG + " skip RPC guard " + className
                        + ": " + t.getMessage());
            }
        }
    }

    private static String extractRpcOperation(Object[] args) {
        if (args == null) {
            return null;
        }
        for (Object arg : args) {
            if (!(arg instanceof String)) {
                continue;
            }
            String text = (String) arg;
            if (text.startsWith("com.abl.")
                    || text.startsWith("ap.tngd")
                    || text.startsWith("alipayplus.")) {
                return text;
            }
        }
        return null;
    }

    private static boolean isRegistrationRpc(String op) {
        if (op == null) {
            return false;
        }
        String lower = op.toLowerCase(Locale.US);
        for (String marker : REGISTRATION_RPC_MARKERS) {
            if (lower.contains(marker)) {
                return true;
            }
        }
        return false;
    }

    private static boolean isJailBrokenRpcInvocation(Object[] args) {
        if (args == null) {
            return false;
        }
        for (Object arg : args) {
            if (arg == null) {
                continue;
            }
            if (arg instanceof Method) {
                String name = ((Method) arg).getName().toLowerCase(Locale.US);
                if (name.contains("jail")) {
                    return true;
                }
                continue;
            }
            if (arg instanceof String) {
                String text = ((String) arg).toLowerCase(Locale.US);
                if (text.contains("jail.broken")
                        || text.contains("jailbroken")
                        || text.contains("jail_broken")) {
                    return true;
                }
                continue;
            }
            // 勿对 RPC 动态代理 toString：会再次进入 invoke 导致栈溢出。
            if (Proxy.isProxyClass(arg.getClass())) {
                continue;
            }
            Class<?> clazz = arg.getClass();
            if (clazz.isArray()) {
                continue;
            }
            String simple = clazz.getSimpleName().toLowerCase(Locale.US);
            if (simple.contains("jailbroken") || simple.contains("jailbrokenrequest")) {
                return true;
            }
        }
        return false;
    }

    private static String describeRpcInvocation(Object[] args) {
        if (args == null || args.length == 0) {
            return "[]";
        }
        StringBuilder sb = new StringBuilder("[");
        for (int i = 0; i < args.length; i++) {
            if (i > 0) {
                sb.append(", ");
            }
            Object arg = args[i];
            if (arg instanceof Method) {
                sb.append("Method=").append(((Method) arg).getName());
            } else if (arg instanceof String) {
                String s = (String) arg;
                sb.append(s.length() > 120 ? s.substring(0, 120) + "..." : s);
            } else if (Proxy.isProxyClass(arg.getClass())) {
                sb.append("Proxy=").append(arg.getClass().getInterfaces()[0].getSimpleName());
            } else {
                sb.append(arg.getClass().getSimpleName());
            }
        }
        sb.append("]");
        return sb.toString();
    }

    private static Object buildCleanJailBrokenResult(ClassLoader loader) {
        try {
            Class<?> clazz = XposedHelpers.findClass(
                    "my.com.tngdigital.common.jailbrkendetect.JailBrokenResult", loader);
            for (Constructor<?> constructor : clazz.getDeclaredConstructors()) {
                Class<?>[] types = constructor.getParameterTypes();
                Object[] args = new Object[types.length];
                boolean ok = true;
                for (int i = 0; i < types.length; i++) {
                    Class<?> type = types[i];
                    if (type == boolean.class || type == Boolean.class) {
                        args[i] = Boolean.FALSE;
                    } else if (type == String.class) {
                        args[i] = "";
                    } else if (type == int.class) {
                        args[i] = 0;
                    } else if (type == Integer.class) {
                        args[i] = Integer.valueOf(0);
                    } else if (!type.isPrimitive()) {
                        args[i] = null;
                    } else {
                        ok = false;
                        break;
                    }
                }
                if (!ok) {
                    continue;
                }
                try {
                    constructor.setAccessible(true);
                    Object result = constructor.newInstance(args);
                    sanitizeJailBrokenResult(result);
                    return result;
                } catch (Throwable ignored) {
                }
            }
        } catch (Throwable t) {
            XposedBridge.log(TAG + " buildCleanJailBrokenResult failed: " + t.getMessage());
        }
        return null;
    }

    private static void sanitizeJailBrokenResult(Object result) {
        if (result == null) {
            return;
        }
        try {
            for (java.lang.reflect.Field field : result.getClass().getDeclaredFields()) {
                field.setAccessible(true);
                String name = field.getName().toLowerCase(Locale.US);
                Class<?> type = field.getType();
                if ((type == boolean.class || type == Boolean.class)
                        && (name.contains("jail") || name.contains("root"))) {
                    field.set(result, Boolean.FALSE);
                } else if (type == String.class && name.contains("msg")) {
                    field.set(result, "");
                }
            }
        } catch (Throwable ignored) {
        }
    }

    private static void invokeJailBrokenCallbacks(Object[] args, Object cleanResult) {
        if (args == null || cleanResult == null) {
            return;
        }
        for (Object arg : args) {
            if (arg == null) {
                continue;
            }
            try {
                for (Method method : arg.getClass().getDeclaredMethods()) {
                    if (method.getParameterTypes().length != 1) {
                        continue;
                    }
                    Class<?> paramType = method.getParameterTypes()[0];
                    if (!paramType.getName().contains("JailBrokenResult")) {
                        continue;
                    }
                    method.setAccessible(true);
                    method.invoke(arg, cleanResult);
                    XposedBridge.log(TAG + " delivered clean JailBrokenResult via "
                            + arg.getClass().getSimpleName() + "#" + method.getName());
                }
            } catch (Throwable ignored) {
            }
            try {
                XposedHelpers.callMethod(arg, "invoke", cleanResult);
            } catch (Throwable ignored) {
            }
        }
    }

    private static void hookJailBrokenResult(XC_LoadPackage.LoadPackageParam lpparam) {
        try {
            Class<?> resultClass = XposedHelpers.findClass(
                    "my.com.tngdigital.common.jailbrkendetect.JailBrokenResult",
                    lpparam.classLoader);
            for (Method method : resultClass.getDeclaredMethods()) {
                String name = method.getName();
                Class<?> returnType = method.getReturnType();
                if (returnType == boolean.class || returnType == Boolean.class) {
                    if (name.toLowerCase(Locale.US).contains("jail")
                            || name.toLowerCase(Locale.US).contains("root")) {
                        XposedBridge.hookMethod(method, new XC_MethodHook() {
                            @Override
                            protected void beforeHookedMethod(MethodHookParam param) {
                                param.setResult(false);
                            }
                        });
                        XposedBridge.log(TAG + " hooked JailBrokenResult#" + name);
                    }
                } else if (returnType == String.class
                        && name.toLowerCase(Locale.US).contains("msg")) {
                    XposedBridge.hookMethod(method, new XC_MethodHook() {
                        @Override
                        protected void beforeHookedMethod(MethodHookParam param) {
                            param.setResult("");
                        }
                    });
                    XposedBridge.log(TAG + " hooked JailBrokenResult#" + name);
                }
            }
            XposedHelpers.findAndHookConstructor(resultClass, new XC_MethodHook() {
                @Override
                protected void afterHookedMethod(MethodHookParam param) {
                    sanitizeJailBrokenResult(param.getResult());
                }
            });
        } catch (Throwable t) {
            XposedBridge.log(TAG + " JailBrokenResult hooks failed: " + t.getMessage());
        }
    }

    private static void hookAppSecurityManager(XC_LoadPackage.LoadPackageParam lpparam) {
        // addIntoQueue 勿拦：拦截后 Promon 会走 native _exit fallback
        hookReturnFalse(lpparam,
                "my.com.tngdigital.common.security.malwarescan.MalwareScanUtils",
                "isShowErrorScreen");
    }

    private static void hookSecurityErrorActivity(XC_LoadPackage.LoadPackageParam lpparam) {
        hookSecurityErrorLaunch(lpparam);
        hookGenericSecurityErrorFinish();
        XC_MethodHook closeSecurityScreen = new XC_MethodHook() {
            @Override
            protected void afterHookedMethod(MethodHookParam param) {
                Activity activity = (Activity) param.thisObject;
                XposedBridge.log(TAG + " closing " + activity.getClass().getSimpleName());
                activity.finish();
            }
        };
        for (String activityClass : new String[]{
                SECURITY_ERROR_ACTIVITY,
                "my.com.tngdigital.common.security.ui.SecurityErrorBaseActivity",
        }) {
            hookAllOnCreateMethods(lpparam, activityClass, closeSecurityScreen);
        }
    }

    private static void hookAllOnCreateMethods(
            XC_LoadPackage.LoadPackageParam lpparam,
            String className,
            XC_MethodHook hook) {
        try {
            Class<?> clazz = XposedHelpers.findClass(className, lpparam.classLoader);
            int hooked = 0;
            for (Method method : clazz.getDeclaredMethods()) {
                if (!"onCreate".equals(method.getName())) {
                    continue;
                }
                XposedBridge.hookMethod(method, hook);
                hooked++;
            }
            if (hooked > 0) {
                XposedBridge.log(TAG + " hooked " + hooked + " onCreate overload(s) in " + className);
            } else {
                XposedBridge.log(TAG + " no onCreate in " + className);
            }
        } catch (Throwable t) {
            XposedBridge.log(TAG + " " + className + " onCreate hook failed: " + t.getMessage());
        }
    }

    /** SecurityErrorActivity 为 Compose，无独立 onCreate；在 Activity 基类统一 finish。 */
    private static void hookGenericSecurityErrorFinish() {
        try {
            XposedHelpers.findAndHookMethod(
                    Activity.class,
                    "onCreate",
                    Bundle.class,
                    new XC_MethodHook() {
                        @Override
                        protected void afterHookedMethod(MethodHookParam param) {
                            Activity activity = (Activity) param.thisObject;
                            String name = activity.getClass().getName();
                            if (name.contains("SecurityError")) {
                                XposedBridge.log(TAG + " finishing " + name);
                                activity.finish();
                            }
                        }
                    });
            XposedBridge.log(TAG + " hooked Activity.onCreate SecurityError finish");
        } catch (Throwable t) {
            XposedBridge.log(TAG + " generic SecurityError finish failed: " + t.getMessage());
        }
    }

    /** 拦截 Security 状态机启动 SecurityErrorActivity。 */
    private static void hookSecurityErrorLaunch(XC_LoadPackage.LoadPackageParam lpparam) {
        // launchProcessNextSecurityStateIfIdle / addIntoQueue 勿拦：
        // 拦截后 Promon 会立刻走 native exit_group(1)。
        // 杀进程改由 finishAllActivityAndKillApp / showSecurityScreenForState 兜底。
        XposedBridge.log(TAG + " skip launchProcessNext noop (avoid native exit fallback)");
    }

    /**
     * 首页强制「验证您的帐户」(HomeEkycVerifyActivity) — 测试期跳过。
     * 拦启动 + onCreate finish；canBypassEkyc=true；enforceEkyc=false。
     */
    private static void hookHomeEkycVerifySkip(XC_LoadPackage.LoadPackageParam lpparam) {
        try {
            XposedHelpers.findAndHookMethod(
                    Activity.class,
                    "onCreate",
                    Bundle.class,
                    new XC_MethodHook() {
                        @Override
                        protected void afterHookedMethod(MethodHookParam param) {
                            Activity activity = (Activity) param.thisObject;
                            String name = activity.getClass().getName();
                            if (name != null && name.contains("HomeEkycVerify")) {
                                XposedBridge.log(TAG + " skip eKYC — finish " + name);
                                activity.finish();
                            }
                        }
                    });
            XposedBridge.log(TAG + " hooked HomeEkycVerify Activity finish");
        } catch (Throwable t) {
            XposedBridge.log(TAG + " HomeEkycVerify finish hook failed: " + t.getMessage());
        }

        // 白名单绕过：返回 true
        hookBooleanMethodsByName(lpparam,
                "my.com.tngdigital.home.viewmodel.BypassEkycWhiteListChecker",
                true, "canBypassEkyc", "bypassEkyc", "isWhitelist", "inWhitelist");
        // 强制 eKYC 开关：返回 false
        for (String className : new String[]{
                "my.com.tngdigital.home.help.HomeEkycCheckHelper",
                "my.com.tngdigital.home.ekyc.KycHomepagePopUpManager",
                "my.com.tngdigital.home.ekyc.HomeEkycVerifyViewModel",
                "my.com.tngdigital.home.viewmodel.HomeListActivityViewModel",
        }) {
            hookBooleanMethodsByName(lpparam, className, false,
                    "getEnforceEkyc", "enforceEkyc", "needShowEkyc", "needForceEkyc",
                    "isForceEkyc", "checkEkyc", "firstCheckEkyc", "needShowEkycCddAudit");
            hookVoidMethodsByName(lpparam, className,
                    "checkEkyc", "checkEkycStatus", "checkEkycRequest",
                    "requestEkycStatus", "showEkyc", "launchEkyc", "openEkyc");
        }
    }

    private static void hookBooleanMethodsByName(
            XC_LoadPackage.LoadPackageParam lpparam,
            String className,
            boolean result,
            String... nameHints) {
        try {
            Class<?> clazz = XposedHelpers.findClass(className, lpparam.classLoader);
            int hooked = 0;
            for (Method method : clazz.getDeclaredMethods()) {
                if (method.getReturnType() != boolean.class && method.getReturnType() != Boolean.class) {
                    continue;
                }
                String n = method.getName();
                boolean match = false;
                for (String hint : nameHints) {
                    if (n.equals(hint) || n.toLowerCase(Locale.US).contains(hint.toLowerCase(Locale.US))) {
                        match = true;
                        break;
                    }
                }
                if (!match) {
                    continue;
                }
                final boolean ret = result;
                XposedBridge.hookMethod(method, new XC_MethodHook() {
                    @Override
                    protected void beforeHookedMethod(MethodHookParam param) {
                        param.setResult(ret);
                    }
                });
                hooked++;
            }
            XposedBridge.log(TAG + " eKYC bool stub " + className + " n=" + hooked + " -> " + result);
        } catch (Throwable t) {
            XposedBridge.log(TAG + " eKYC bool stub skip " + className + ": " + t.getMessage());
        }
    }

    private static void hookVoidMethodsByName(
            XC_LoadPackage.LoadPackageParam lpparam,
            String className,
            String... nameHints) {
        try {
            Class<?> clazz = XposedHelpers.findClass(className, lpparam.classLoader);
            int hooked = 0;
            for (Method method : clazz.getDeclaredMethods()) {
                if (method.getReturnType() != void.class) {
                    continue;
                }
                String n = method.getName();
                boolean match = false;
                for (String hint : nameHints) {
                    if (n.equals(hint) || n.startsWith(hint)) {
                        match = true;
                        break;
                    }
                }
                if (!match) {
                    continue;
                }
                XposedBridge.hookMethod(method, new XC_MethodHook() {
                    @Override
                    protected void beforeHookedMethod(MethodHookParam param) {
                        param.setResult(null);
                    }
                });
                hooked++;
            }
            if (hooked > 0) {
                XposedBridge.log(TAG + " eKYC void noop " + className + " n=" + hooked);
            }
        } catch (Throwable t) {
            XposedBridge.log(TAG + " eKYC void noop skip " + className + ": " + t.getMessage());
        }
    }

    private static void hookReturnFalse(
            XC_LoadPackage.LoadPackageParam lpparam, String className, String methodName) {
        try {
            Class<?> clazz = XposedHelpers.findClass(className, lpparam.classLoader);
            int hooked = 0;
            for (Method method : clazz.getDeclaredMethods()) {
                if (!methodName.equals(method.getName())) {
                    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);
                    }
                });
                hooked++;
            }
            if (hooked > 0) {
                XposedBridge.log(TAG + " hooked " + hooked + " overload(s) "
                        + className + "#" + methodName + " -> false");
            } else {
                XposedBridge.log(TAG + " no boolean method " + className + "#" + methodName);
            }
        } catch (Throwable t) {
            XposedBridge.log(TAG + " skip " + className + "#" + methodName + ": " + t.getMessage());
        }
    }

    private static void hookNoArgVoid(
            XC_LoadPackage.LoadPackageParam lpparam, String className, String methodName) {
        hookAllMethodsNoop(lpparam, className, methodName);
    }

    private static void setSafeHookResult(XC_MethodHook.MethodHookParam param, Method method) {
        Class<?> returnType = method.getReturnType();
        if (returnType == void.class) {
            param.setResult(null);
        } else if (returnType == boolean.class) {
            param.setResult(false);
        } else if (returnType == int.class) {
            param.setResult(0);
        } else if (returnType == long.class) {
            param.setResult(0L);
        } else if (returnType == float.class) {
            param.setResult(0f);
        } else if (returnType == double.class) {
            param.setResult(0d);
        } else if (returnType == byte.class) {
            param.setResult((byte) 0);
        } else if (returnType == short.class) {
            param.setResult((short) 0);
        } else if (returnType == char.class) {
            param.setResult((char) 0);
        } else if (returnType == Boolean.class) {
            param.setResult(Boolean.FALSE);
        } else if (returnType == Integer.class) {
            param.setResult(Integer.valueOf(0));
        } else if (returnType == Long.class) {
            param.setResult(Long.valueOf(0L));
        } else if (returnType == Float.class) {
            param.setResult(Float.valueOf(0f));
        } else if (returnType == Double.class) {
            param.setResult(Double.valueOf(0d));
        } else if (returnType == String.class) {
            param.setResult("");
        } else if (returnType == byte[].class) {
            param.setResult(new byte[0]);
        } else {
            param.setResult(null);
        }
    }
}
