package com.miraclegarden.smsmessage.xposed.hook;

import android.os.Build;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.util.Arrays;
import java.util.HashSet;
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;

/**
 * 过滤 /proc/self/maps 等敏感路径，隐藏 Xposed / Magisk / Zygisk 库名。
 */
public final class ProcMapsFilterHook {

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

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

    private static final String[] MAPS_HIDE_MARKERS = {
            "xposed", "lsposed", "edxposed", "magisk", "frida", "substrate",
            "libpine", "pine.so", "zygisk", "riru", "shamiko", "notimessage",
            "miraclegarden", "libbytehook", "libgadget", "libfrida", "libriru",
            "liblspd", "libzygisk", "libvector", "zygisk_vector", "vector",
    };

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

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

    private static volatile boolean installed = false;

    private ProcMapsFilterHook() {
    }

    public static void install(XC_LoadPackage.LoadPackageParam lpparam) {
        if (installed) {
            return;
        }
        installed = true;
        hookProcAccess(lpparam);
        hookProcViaRandomAccessFile(lpparam);
        hookBufferedReader(lpparam);
        hookSystemProperties(lpparam);
        XposedBridge.log(TAG + " installed for " + lpparam.packageName);
    }

    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 norm = normalizeProcPath(String.valueOf(param.args[0]));
                                if (norm != null) {
                                    param.setResult(filterProcBytesAll(
                                            norm, (byte[]) param.getResult()));
                                }
                            }
                        }
                );
            } catch (Throwable ignored) {
            }
        }
    }

    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 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 "";
    }

    private static void hookSystemProperties(XC_LoadPackage.LoadPackageParam lpparam) {
        try {
            Class<?> sp = XposedHelpers.findClass("android.os.SystemProperties", lpparam.classLoader);
            for (java.lang.reflect.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 ("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.vbmeta.device_state".equals(key)
                || "vendor.boot.vbmeta.device_state".equals(key)) {
            return "locked";
        }
        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;
    }
}
