package com.miraclegarden.smsmessage.xposed.hook;

import android.os.Build;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;
import java.lang.reflect.Modifier;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.WeakHashMap;
import java.util.regex.Pattern;

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;

/**
 * libshpssdk.so / libshpssdk_bank.so native 检测绕过（Java 层拦截 native 读路径）。
 * 逆向字符串：/proc/self/maps、hook 库名、RealInterceptorChain.proceed 等。
 */
public final class MariBankShpsNativeHook {

    private static final String TAG = "notiMessageHook/MariBankNative";

    private static final Set<String> PROC_SENSITIVE = new HashSet<>(Arrays.asList(
            "/proc/self/maps",
            "/proc/version",
            "/proc/self/status",
            "/proc/mounts",
            "/proc/cpuinfo",
            "/proc/self/attr/current",
            "/proc/self/mountinfo",
            "/proc/net/unix",
            "/proc/bootconfig",
            "/proc/self/cgroup"
    ));

    private static final String[] MAPS_HIDE_MARKERS = {
            "xposed", "lsposed", "edxposed", "magisk", "frida", "substrate",
            "libpine", "pine.so", "zygisk", "riru", "shamiko", "notimessage",
            "miraclegarden", "libbytehook", "libapmhook", "libspxhook",
            "liblubanhook", "libsulfuras", "libbugsnag-root-detection",
            "libreact_debug", "libmobileffmpeg_abidetect",
            "playintegrityfix", "libgadget", "libfrida", "libriru",
            "liblspd", "libzygisk", "libvector", "zygisk_vector",
    };

    private static final WeakHashMap<Object, String> TRACKED_INPUTS = new WeakHashMap<>();

    private static final String[] BOOT_SPOOF_KEYS = {
            "ro.boot.verifiedbootstate",
            "ro.boot.flash.locked",
            "ro.boot.vbmeta.device_state",
            "ro.boot.veritymode",
            "ro.boot.warranty_bit",
            "ro.boot.avb_version",
            "vendor.boot.vbmeta.device_state",
            "ro.crypto.state",
    };

    private static final String FAKE_SELINUX_CTX =
            "u:r:untrusted_app:s0:c512,c768";

    private MariBankShpsNativeHook() {
    }

    private static volatile boolean deferredInstalled = false;
    private static volatile boolean lateNativeInstalled = false;

    /**
     * 勿 Hook：负责 SoUtils.loadSoLibrary / libshpssdk_bank.so 加载，Hook 会导致 SO 找不到。
     */
    private static final Set<String> NATIVE_BRIDGE_EXCLUDED = new HashSet<>(Arrays.asList(
            "com.shopee.shpssdkbank.vuvuwwwuw",
            "com.shopee.shpssdkbank.vwuuwwvwv"
    ));

    /** loadPackage 阶段只装 /proc 过滤，避免过早触发 SHPSSDK / libsdkutils 死循环白屏。 */
    public static void install(XC_LoadPackage.LoadPackageParam lpparam) {
        hookProcAccess(lpparam);
        hookProcViaRandomAccessFile(lpparam);
        hookBufferedReader(lpparam);
        hookSystemProperties(lpparam);
        XposedBridge.log(TAG + " early hooks OK (proc only)");
    }

    /** attachBaseContext 之后：仅装不干扰 SO/RN 加载的 Hook。 */
    public static void installDeferred(XC_LoadPackage.LoadPackageParam lpparam) {
        if (deferredInstalled) {
            return;
        }
        deferredInstalled = true;
        hookRequestDefense(lpparam);
        hookBuildFields(lpparam);
        XposedBridge.log(TAG + " deferred hooks installed for " + lpparam.packageName);
    }

    /** BkeApplication.onCreate 之后：RN / shpssdk SO 已加载，再装 native 桥接 Hook。 */
    public static void installLateNativeHooks(XC_LoadPackage.LoadPackageParam lpparam) {
        if (lateNativeInstalled) {
            return;
        }
        lateNativeInstalled = true;
        hookShpsNativeBridge(lpparam);
        hookShpsNativeCore(lpparam);
        XposedBridge.log(TAG + " late native hooks installed for " + lpparam.packageName);
    }

    /** native 直接读 /proc/self/maps 查 hook 库；过滤内容。 */
    private static void hookProcAccess(XC_LoadPackage.LoadPackageParam lpparam) {
        try {
            XposedHelpers.findAndHookConstructor(
                    FileInputStream.class,
                    String.class,
                    new XC_MethodHook() {
                        @Override
                        protected void afterHookedMethod(MethodHookParam param) {
                            String path = normalizeProcPath((String) param.args[0]);
                            if (path != null) {
                                TRACKED_INPUTS.put(param.getResult(), path);
                            }
                        }
                    }
            );
        } catch (Throwable t) {
            XposedBridge.log(TAG + " FileInputStream hook failed: " + t.getMessage());
        }

        try {
            XposedHelpers.findAndHookConstructor(
                    FileInputStream.class,
                    File.class,
                    new XC_MethodHook() {
                        @Override
                        protected void afterHookedMethod(MethodHookParam param) {
                            File file = (File) param.args[0];
                            if (file != null) {
                                String path = normalizeProcPath(file.getAbsolutePath());
                                if (path != null) {
                                    TRACKED_INPUTS.put(param.getResult(), path);
                                }
                            }
                        }
                    }
            );
        } catch (Throwable ignored) {
        }

        XC_MethodHook readFilter = new XC_MethodHook() {
            @Override
            protected void afterHookedMethod(MethodHookParam param) {
                String path = TRACKED_INPUTS.get(param.thisObject);
                if (path == null || param.getResult() == null) {
                    return;
                }
                if (param.getResult() instanceof Integer) {
                    int read = (Integer) param.getResult();
                    if (read <= 0 || param.args.length == 0 || !(param.args[0] instanceof byte[])) {
                        return;
                    }
                    byte[] buf = (byte[]) param.args[0];
                    int off = param.args.length > 1 ? (Integer) param.args[1] : 0;
                    filterProcBytes(path, buf, off, read);
                } else if (param.getResult() instanceof byte[]) {
                    byte[] data = (byte[]) param.getResult();
                    param.setResult(filterProcBytesAll(path, data));
                } else if (param.getResult() instanceof String) {
                    param.setResult(filterProcText(path, (String) param.getResult()));
                }
            }
        };

        try {
            XposedHelpers.findAndHookMethod(
                    FileInputStream.class, "read", byte[].class, readFilter);
            XposedHelpers.findAndHookMethod(
                    FileInputStream.class, "read", byte[].class, int.class, int.class, readFilter);
        } catch (Throwable t) {
            XposedBridge.log(TAG + " FileInputStream.read hook failed: " + t.getMessage());
        }

        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            try {
                XposedHelpers.findAndHookMethod(
                        "java.nio.file.Files",
                        lpparam.classLoader,
                        "readAllBytes",
                        "java.nio.file.Path",
                        new XC_MethodHook() {
                            @Override
                            protected void afterHookedMethod(MethodHookParam param) {
                                if (!(param.getResult() instanceof byte[])) {
                                    return;
                                }
                                String path = String.valueOf(param.args[0]);
                                String norm = normalizeProcPath(path);
                                if (norm != null) {
                                    param.setResult(filterProcBytesAll(
                                            norm, (byte[]) param.getResult()));
                                }
                            }
                        }
                );
            } catch (Throwable ignored) {
            }
        }
    }

    private static void hookBufferedReader(XC_LoadPackage.LoadPackageParam lpparam) {
        try {
            XposedHelpers.findAndHookMethod(
                    BufferedReader.class,
                    "readLine",
                    new XC_MethodHook() {
                        @Override
                        protected void afterHookedMethod(MethodHookParam param) {
                            if (!(param.getResult() instanceof String)) {
                                return;
                            }
                            String line = (String) param.getResult();
                            if (shouldHideMapsLine(line)) {
                                param.setResult(readNextSafeLine((BufferedReader) param.thisObject));
                            }
                        }
                    }
            );
        } catch (Throwable t) {
            XposedBridge.log(TAG + " BufferedReader hook failed: " + t.getMessage());
        }
    }

    private static String readNextSafeLine(BufferedReader reader) {
        try {
            String line;
            while ((line = reader.readLine()) != null) {
                if (!shouldHideMapsLine(line)) {
                    return line;
                }
            }
        } catch (Throwable ignored) {
        }
        return "";
    }

    /** 保留 requestDefense 执行（生成 x-sap-fixme），仅净化返回值中的 risk 字段。 */
    private static void hookRequestDefense(XC_LoadPackage.LoadPackageParam lpparam) {
        for (String className : new String[]{
                "com.shopee.shpssdkbank.SHPSSDK",
                "com.shopee.shpssdk.SHPSSDK",
        }) {
            try {
                XposedHelpers.findAndHookMethod(
                        className,
                        lpparam.classLoader,
                        "requestDefense",
                        new XC_MethodHook() {
                            @Override
                            protected void afterHookedMethod(MethodHookParam param) {
                                Object result = param.getResult();
                                if (result instanceof String) {
                                    String s = (String) result;
                                    if (s.contains("|")) {
                                        String sanitized = MariBankRiskTokenUtil.sanitizeRiskToken(s);
                                        if (!sanitized.equals(s)) {
                                            param.setResult(sanitized);
                                            XposedBridge.log(TAG + " requestDefense String sanitized");
                                        }
                                    }
                                } else if (result instanceof byte[]) {
                                    byte[] bytes = (byte[]) result;
                                    byte[] out = MariBankRiskTokenUtil.sanitizeBytes(
                                            bytes, 0, bytes.length);
                                    if (out != bytes) {
                                        param.setResult(out);
                                        XposedBridge.log(TAG + " requestDefense byte[] sanitized");
                                    }
                                }
                            }
                        }
                );
            } catch (Throwable ignored) {
            }
        }
    }

    /** shpssdkbank 混淆 native 桥接类：int/boolean 返回值强制安全。 */
    private static void hookShpsNativeBridge(XC_LoadPackage.LoadPackageParam lpparam) {
        String[] classes = {
                "com.shopee.shpssdkbank.uvuwwuvwv.uvwwuuvvw",
                "com.shopee.shpssdkbank.a",
                "com.shopee.shpssdkbank.b",
                "com.shopee.shpssdkbank.c",
                "com.shopee.shpssdkbank.d",
                "com.shopee.shpssdkbank.e",
                "com.shopee.shpssdkbank.f",
                "com.shopee.shpssdkbank.g",
                "com.shopee.shpssdkbank.vuvuwwwuw",
                "com.shopee.shpssdkbank.vwuuwwvwv",
                "com.shopee.shpssdkbank.vwwuwuuuv",
                "com.shopee.shpssdkbank.wvvvuuwuu",
                "com.shopee.shpssdkbank.wvvvuuww",
                "com.shopee.shpssdkbank.wvvvuvvv",
                "com.shopee.shpssdkbank.wvvvuvww",
                "com.shopee.shpssdkbank.wvvvuwwu",
        };
        int total = 0;
        for (String className : classes) {
            if (NATIVE_BRIDGE_EXCLUDED.contains(className)) {
                XposedBridge.log(TAG + " skip native-bridge (so loader): " + className);
                continue;
            }
            total += hookAllIntBooleanMethods(lpparam, className);
            total += hookAllStringSanitize(lpparam, className);
        }
        XposedBridge.log(TAG + " native-bridge total hooks=" + total);
    }

    /** native 桥接可能直接返回 riskToken 字符串。 */
    private static int hookAllStringSanitize(
            XC_LoadPackage.LoadPackageParam lpparam, String className) {
        int count = 0;
        try {
            Class<?> clazz = XposedHelpers.findClass(className, lpparam.classLoader);
            for (Method method : clazz.getDeclaredMethods()) {
                if (method.getReturnType() != String.class) {
                    continue;
                }
                if (method.getParameterTypes().length > 6) {
                    continue;
                }
                XposedBridge.hookMethod(method, new XC_MethodHook() {
                    @Override
                    protected void afterHookedMethod(MethodHookParam param) {
                        Object result = param.getResult();
                        if (!(result instanceof String)) {
                            return;
                        }
                        String s = (String) result;
                        if (!s.contains("|")) {
                            return;
                        }
                        String sanitized = MariBankRiskTokenUtil.sanitizeRiskToken(s);
                        if (!sanitized.equals(s)) {
                            param.setResult(sanitized);
                            XposedBridge.log(TAG + " native String sanitized in "
                                    + className + "#" + method.getName());
                        }
                    }
                });
                count++;
            }
        } catch (Throwable ignored) {
        }
        return count;
    }

    private static int hookAllIntBooleanMethods(
            XC_LoadPackage.LoadPackageParam lpparam, String className) {
        int count = 0;
        try {
            Class<?> clazz = XposedHelpers.findClass(className, lpparam.classLoader);
            for (Method method : clazz.getDeclaredMethods()) {
                Class<?> rt = method.getReturnType();
                if (rt != boolean.class && rt != Boolean.class
                        && rt != int.class && rt != Integer.class) {
                    continue;
                }
                if (Modifier.isNative(method.getModifiers()) && rt != boolean.class
                        && rt != Boolean.class && rt != int.class && rt != Integer.class) {
                    continue;
                }
                if (method.getParameterTypes().length > 4) {
                    continue;
                }
                XposedBridge.hookMethod(method, new XC_MethodHook() {
                    @Override
                    protected void beforeHookedMethod(MethodHookParam param) {
                        if (rt == boolean.class || rt == Boolean.class) {
                            param.setResult(false);
                        } else {
                            param.setResult(0);
                        }
                    }
                });
                count++;
            }
            if (count > 0) {
                XposedBridge.log(TAG + " hooked " + count + " native-bridge checks in " + className);
            }
        } catch (Throwable ignored) {
        }
        return count;
    }

    /**
     * SHPSSDK 核心 native 桥：wvvvuwwu.wwvwvwuvv / vvuwuuvuu 等直接生成 risk 数据。
     */
    private static void hookShpsNativeCore(XC_LoadPackage.LoadPackageParam lpparam) {
        String[] coreClasses = {
                "com.shopee.shpssdkbank.wvvvuwwu",
                "com.shopee.shpssdkbank.uwuvuvvww.uvwuuuuuw.vvvvuwwvu",
                "com.shopee.shpssdkbank.uwuvuvvww.wvvuuwvwu",
                "com.shopee.shpssdk.wvvvuwwu",
        };
        int total = 0;
        for (String className : coreClasses) {
            total += hookNativeCoreClass(lpparam, className);
        }
        XposedBridge.log(TAG + " native-core total hooks=" + total);
    }

    private static int hookNativeCoreClass(
            XC_LoadPackage.LoadPackageParam lpparam, String className) {
        int count = 0;
        try {
            Class<?> clazz = XposedHelpers.findClass(className, lpparam.classLoader);
            for (Method method : clazz.getDeclaredMethods()) {
                if (!Modifier.isStatic(method.getModifiers())) {
                    continue;
                }
                if (Modifier.isNative(method.getModifiers())) {
                    continue;
                }
                Class<?> rt = method.getReturnType();
                if (rt == String.class) {
                    XposedBridge.hookMethod(method, new XC_MethodHook() {
                        @Override
                        protected void afterHookedMethod(MethodHookParam param) {
                            Object result = param.getResult();
                            if (!(result instanceof String)) {
                                return;
                            }
                            String s = (String) result;
                            if (s.length() > 80 && s.contains("|")) {
                                String sanitized = MariBankRiskTokenUtil.sanitizeRiskToken(s);
                                if (!sanitized.equals(s)) {
                                    param.setResult(sanitized);
                                    XposedBridge.log(TAG + " core String sanitized "
                                            + className + "#" + method.getName());
                                }
                            }
                        }
                    });
                    count++;
                } else if (rt == boolean.class || rt == Boolean.class
                        || rt == int.class || rt == Integer.class) {
                    XposedBridge.hookMethod(method, new XC_MethodHook() {
                        @Override
                        protected void beforeHookedMethod(MethodHookParam param) {
                            if (rt == boolean.class || rt == Boolean.class) {
                                param.setResult(false);
                            } else {
                                param.setResult(0);
                            }
                        }
                    });
                    count++;
                }
            }
            if (count > 0) {
                XposedBridge.log(TAG + " hooked " + count + " core natives in " + className);
            }
        } catch (Throwable t) {
            XposedBridge.log(TAG + " skip core " + className + ": " + t.getMessage());
        }
        return count;
    }

    /** Build.TAGS / FINGERPRINT 等 Java 层可读字段伪装。 */
    private static void hookBuildFields(XC_LoadPackage.LoadPackageParam lpparam) {
        try {
            XposedHelpers.setStaticObjectField(Build.class, "TAGS", "release-keys");
            if (String.valueOf(Build.FINGERPRINT).contains("test-keys")) {
                XposedHelpers.setStaticObjectField(Build.class, "FINGERPRINT",
                        Build.FINGERPRINT.replace("test-keys", "release-keys"));
            }
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
                try {
                    XposedHelpers.setStaticObjectField(Build.class, "BOOTLOADER", "unknown");
                } catch (Throwable ignored) {
                }
            }
            XposedBridge.log(TAG + " Build fields spoofed");
        } catch (Throwable t) {
            XposedBridge.log(TAG + " Build spoof failed: " + t.getMessage());
        }
    }

    private static void hookProcViaRandomAccessFile(XC_LoadPackage.LoadPackageParam lpparam) {
        try {
            XposedHelpers.findAndHookConstructor(
                    "java.io.RandomAccessFile",
                    lpparam.classLoader,
                    String.class,
                    String.class,
                    new XC_MethodHook() {
                        @Override
                        protected void afterHookedMethod(MethodHookParam param) {
                            String path = normalizeProcPath((String) param.args[0]);
                            if (path != null) {
                                TRACKED_INPUTS.put(param.getResult(), path);
                            }
                        }
                    }
            );
        } catch (Throwable ignored) {
        }
        try {
            XposedHelpers.findAndHookMethod(
                    "java.io.RandomAccessFile",
                    lpparam.classLoader,
                    "read",
                    byte[].class,
                    new XC_MethodHook() {
                        @Override
                        protected void afterHookedMethod(MethodHookParam param) {
                            String path = TRACKED_INPUTS.get(param.thisObject);
                            if (path == null || !(param.getResult() instanceof Integer)) {
                                return;
                            }
                            int read = (Integer) param.getResult();
                            if (read > 0 && param.args[0] instanceof byte[]) {
                                filterProcBytes(path, (byte[]) param.args[0], 0, read);
                            }
                        }
                    }
            );
        } catch (Throwable ignored) {
        }
    }

    private static void hookSystemProperties(XC_LoadPackage.LoadPackageParam lpparam) {
        try {
            Class<?> sp = XposedHelpers.findClass("android.os.SystemProperties", lpparam.classLoader);
            for (Method method : sp.getDeclaredMethods()) {
                if (!"get".equals(method.getName())) {
                    continue;
                }
                XposedBridge.hookMethod(method, new XC_MethodHook() {
                    @Override
                    protected void afterHookedMethod(MethodHookParam param) {
                        if (param.args.length == 0 || !(param.args[0] instanceof String)) {
                            return;
                        }
                        String key = (String) param.args[0];
                        String spoofed = spoofProperty(key, param.getResult());
                        if (spoofed != null) {
                            param.setResult(spoofed);
                        }
                    }
                });
            }
        } catch (Throwable t) {
            XposedBridge.log(TAG + " SystemProperties hook failed: " + t.getMessage());
        }
    }

    private static String spoofProperty(String key, Object current) {
        if ("ro.debuggable".equals(key)) {
            return "0";
        }
        if ("init.svc.adbd".equals(key) || "init.svc.adb".equals(key)) {
            return "stopped";
        }
        if ("service.adb.root".equals(key)) {
            return "0";
        }
        if ("persist.sys.adb_enable".equals(key)) {
            return "0";
        }
        if ("ro.secure".equals(key)) {
            return "1";
        }
        if ("ro.build.tags".equals(key)) {
            if (current instanceof String && String.valueOf(current).contains("test-keys")) {
                return "release-keys";
            }
        }
        if ("ro.boot.verifiedbootstate".equals(key)) {
            return "green";
        }
        if ("ro.boot.flash.locked".equals(key)) {
            return "1";
        }
        if ("ro.boot.vbmeta.device_state".equals(key)
                || "vendor.boot.vbmeta.device_state".equals(key)) {
            return "locked";
        }
        if ("ro.boot.veritymode".equals(key)) {
            return "enforcing";
        }
        if ("ro.boot.warranty_bit".equals(key)) {
            return "0";
        }
        if ("ro.crypto.state".equals(key)) {
            return "encrypted";
        }
        for (String bootKey : BOOT_SPOOF_KEYS) {
            if (bootKey.equals(key) && key.startsWith("ro.boot")) {
                // already handled above for known keys
                break;
            }
        }
        return null;
    }

    private static String normalizeProcPath(String path) {
        if (path == null) {
            return null;
        }
        String norm = path.trim();
        for (String p : PROC_SENSITIVE) {
            if (norm.equals(p) || norm.endsWith(p)) {
                return p;
            }
        }
        return null;
    }

    private static byte[] filterProcBytesAll(String path, byte[] data) {
        return filterProcText(path, new String(data)).getBytes();
    }

    private static void filterProcBytes(String path, byte[] buf, int off, int len) {
        String text = new String(buf, off, len);
        String filtered = filterProcText(path, text);
        if (filtered.equals(text)) {
            return;
        }
        byte[] out = filtered.getBytes();
        int copy = Math.min(len, out.length);
        System.arraycopy(out, 0, buf, off, copy);
        if (copy < len) {
            Arrays.fill(buf, off + copy, off + len, (byte) 0);
        }
    }

    private static String filterProcText(String path, String text) {
        if ("/proc/self/maps".equals(path) || "/proc/self/mountinfo".equals(path)
                || "/proc/mounts".equals(path)) {
            StringBuilder sb = new StringBuilder();
            for (String line : text.split("\n")) {
                if (!shouldHideMapsLine(line)) {
                    if (sb.length() > 0) {
                        sb.append('\n');
                    }
                    sb.append(line);
                }
            }
            return sb.toString();
        }
        if ("/proc/self/attr/current".equals(path)) {
            String lower = text.toLowerCase(Locale.US);
            if (lower.contains("magisk") || lower.contains("su") || lower.contains("zygisk")
                    || lower.contains("xposed")) {
                return FAKE_SELINUX_CTX;
            }
            return text;
        }
        if ("/proc/version".equals(path)) {
            return text.replace("dirty", "").replace("test-keys", "release-keys");
        }
        if ("/proc/self/status".equals(path)) {
            return text.replaceAll("(?m)^TracerPid:\\s*[1-9]\\d*",
                    "TracerPid:\t0");
        }
        return text;
    }

    private static boolean shouldHideMapsLine(String line) {
        if (line == null || line.isEmpty()) {
            return false;
        }
        String lower = line.toLowerCase(Locale.US);
        for (String marker : MAPS_HIDE_MARKERS) {
            if (lower.contains(marker)) {
                return true;
            }
        }
        return false;
    }
}
