package net.geedge.api.util;

import cn.hutool.core.codec.Base32Codec;
import cn.hutool.core.thread.NamedThreadFactory;
import cn.hutool.log.Log;
import net.geedge.api.entity.EnvApiYml;
import net.geedge.common.APIException;
import net.geedge.common.Constant;
import net.geedge.common.RCode;
import net.geedge.common.T;

import java.io.*;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.*;
import java.util.concurrent.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

public class AdbUtil {

    private static final Log log = Log.get();

    private static AdbUtil instance;

    private static String DEFAULT_DROIDVNC_NG_PKG_NAME = "net.christianbeier.droidvnc_ng";
    private static String DEFAULT_DROIDVNC_NG_APK_PATH = "./lib/droidvnc-np-2.6.0.apk";
    private static String DEFAULT_DROIDVNC_NG_DEFAULTS_JSON_PATH = "./lib/droidvnc-np-defaults.json";
    private static String ANDROID_LAUNCHER  = "./lib/android-launcher.py";

    private String serial;
    private String host;
    private Integer port;

    private Integer vncPort;
    
    private CommandExec commandExec;

    private boolean interrupt;

    private ExecutorService threadPool;


    public String getSerial() {
        return T.StrUtil.isNotEmpty(this.serial) ? serial : String.format("%s:%s", this.host, this.port);
    }

    public void setInterrupt(boolean interrupt) {
        this.interrupt = interrupt;
    }

    public record CommandResult(Integer exitCode, String output) {
    }

    public AdbUtil(EnvApiYml.Adb adb, CommandExec commandExec) {
        this.serial = T.StrUtil.emptyToDefault(adb.getSerial(), "");
        this.host = adb.getHost();
        this.port = adb.getPort();
        this.vncPort = adb.getVncPort();
        this.commandExec = commandExec;
        // adb connect
        if (!this.connect()) {
            log.error("[connect error, program exit]");
            Runtime.getRuntime().halt(1);
        }
        // init
        this.init(true);
    }

    public static AdbUtil getInstance() {
        if (instance == null) {
            throw new IllegalArgumentException("Object has not been instantiated.");
        }
        return instance;
    }

    public static AdbUtil getInstance(EnvApiYml.Adb connInfo, CommandExec commandExec) {
        if (instance == null) {
            synchronized (AdbUtil.class) {
                if (instance == null) {
                    instance = new AdbUtil(connInfo, commandExec);
                }
            }
        }
        return instance;
    }

    /**
     * connect
     */
    public boolean connect() {
        if (T.StrUtil.isNotEmpty(this.serial)) {
            // local
            AdbDevice adbDevice = this.getAdbDevice();
            log.info("[connect] [result: {}]", T.JSONUtil.toJsonStr(adbDevice));
            if (null == adbDevice || !adbDevice.isAvailable()) {
                return false;
            }
        } else {
            // remote
            String result = commandExec.exec(AdbCommandBuilder.builder()
                    .buildConnectCommand(this.host, this.port)
                    .build());
            log.info("[connect] [result: {}]", result);
            if (!T.StrUtil.contains(result, "connected")) {
                return false;
            }
        }
        return true;
    }

    /**
     * init
     * su root
     * install droidVNC NG
     */
    public void init(boolean install) {
        // adb root
        String result = commandExec.exec(AdbCommandBuilder.builder()
                .serial(this.getSerial())
                .buildRootCommand()
                .build()
        );
        log.info("[init] [adb root] [result: {}]", result);

        if (install) {
            // install droidVNC NG
            CommandResult installed = this.install(DEFAULT_DROIDVNC_NG_APK_PATH, true, true);
            log.info("[init] [install droidVNC NG] [result: {}]", installed);

            // 上传默认配置
            this.execShellCommand("shell mkdir -p /storage/emulated/0/Android/data/net.christianbeier.droidvnc_ng/files");
            this.push(DEFAULT_DROIDVNC_NG_DEFAULTS_JSON_PATH, "/storage/emulated/0/Android/data/net.christianbeier.droidvnc_ng/files/defaults.json");

            // 无障碍权限
            this.execShellCommand("shell settings put secure enabled_accessibility_services net.christianbeier.droidvnc_ng/.InputService:$(settings get secure enabled_accessibility_services)");
            // 存储空间权限
            this.execShellCommand("shell pm grant net.christianbeier.droidvnc_ng android.permission.WRITE_EXTERNAL_STORAGE");
            // 屏幕录制权限
            this.execShellCommand("shell appops set net.christianbeier.droidvnc_ng PROJECT_MEDIA allow");

            // ACTION_STOP
            this.execShellCommand("shell am start-foreground-service -n net.christianbeier.droidvnc_ng/.MainService -a net.christianbeier.droidvnc_ng.ACTION_STOP --es net.christianbeier.droidvnc_ng.EXTRA_ACCESS_KEY d042e2b5d5f348588a4e1a243eb7a9a0");
        }

        // ACTION_START
        this.execShellCommand("shell am start-foreground-service -n net.christianbeier.droidvnc_ng/.MainService -a net.christianbeier.droidvnc_ng.ACTION_START --es net.christianbeier.droidvnc_ng.EXTRA_ACCESS_KEY d042e2b5d5f348588a4e1a243eb7a9a0");

        // 添加自定义链
        this.addAswOutputChain();
    }

    /**
     * status
     *
     * @return
     */
    public Map<Object, Object> status() {
        Map<Object, Object> m = T.MapUtil.builder()
                .put("platform", "android")
                .build();
        AdbDevice device = this.getAdbDevice();
        m.put("status", device.isAvailable() ? "online" : "offline");

        Map<String, String> prop = this.getProp();
        m.put("name", T.MapUtil.getStr(prop, "ro.product.name", ""));
        m.put("brand", T.MapUtil.getStr(prop, "ro.product.brand", ""));
        m.put("model", T.MapUtil.getStr(prop, "ro.product.model", ""));
        m.put("version", T.MapUtil.getStr(prop, "ro.build.version.release", ""));
        m.put("resolution", T.MapUtil.getStr(prop, "wm.size", ""));

        // 默认为真机
        String type = "device";
        for (Map.Entry<String, String> entry : prop.entrySet()) {
            // 根据 ro.build.* 这一组配置值判定是否为模拟器，如果包含 emulator、sdk 则为模拟器
            if (entry.getKey().contains("ro.build")) {
                String value = entry.getValue();
                if (T.StrUtil.containsAnyIgnoreCase(value, "emulator", "sdk")) {
                    type = "emulator";
                    break;
                }
            }
        }
        m.put("type", type);

        // check root
        String checkRootResult =  commandExec.exec(AdbCommandBuilder.builder()
                .serial(this.getSerial())
                .buildCheckRootCommand()
                .build()
        );
        m.put("root", !T.StrUtil.containsIgnoreCase(checkRootResult, "Permission denied"));
        return m;
    }

    /**
     * getAdbDevice
     * adb devices -l
     *
     * @return
     */
    private AdbDevice getAdbDevice() {
        String result =  commandExec.exec(AdbCommandBuilder.builder()
                .buildDevicesCommand()
                .build()
        );
        List<AdbDevice> list = T.ListUtil.list(true);
        String[] lines = result.split("\\n");
        for (String line : lines) {
            if (line.startsWith("*") || line.startsWith("List") || T.StrUtil.isEmpty(line))
                continue;
            list.add(new AdbDevice(line));
        }
        AdbDevice adbDevice = list.stream()
                .filter(pojo -> T.StrUtil.equals(pojo.getSerial(), this.getSerial()))
                .findFirst()
                .orElse(null);
        return adbDevice;
    }

    /**
     * getProp
     * adb shell getprop
     *
     * @return
     */
    private Map<String, String> getProp() {
        String result =  commandExec.exec(AdbCommandBuilder.builder()
                .serial(this.getSerial())
                .buildGetpropCommand()
                .build()
        );
        Map<String, String> prop = new LinkedHashMap<>();

        Pattern pattern = Pattern.compile("\\[(.*?)\\]: \\[(.*?)\\]");
        Matcher matcher = pattern.matcher(result);

        while (matcher.find()) {
            String key = matcher.group(1).trim();
            String value = matcher.group(2).trim();
            prop.put(key, value);
        }

        // 分辨率 Physical size: 1440x3040
        String wmSize =  commandExec.exec(AdbCommandBuilder.builder()
                .serial(this.getSerial())
                .buildWmSizeCommand()
                .build()
        );
        prop.put("wm.size", T.StrUtil.trim(wmSize.replaceAll("Physical size: ", "")));
        return prop;
    }


    /**
     * md5sum
     */
    private CommandResult md5sum(String path) {
        String result =  commandExec.exec(AdbCommandBuilder.builder()
                .serial(this.getSerial())
                .buildMd5sumCommand(path)
                .build()
        );
        log.info("[md5sum] [path: {}] [result: {}]", path, result);
        if (T.StrUtil.isNotEmpty(result)) {
            String md5 = result.split("\\s+")[0];
            return new CommandResult(0, md5);
        }
        return new CommandResult(1, "");
    }

    /**
     * push
     * 0 success; !0 failed
     */
    public CommandResult push(String local, String remote) {
        String result =  commandExec.exec(AdbCommandBuilder.builder()
                .serial(this.getSerial())
                .buildPushCommand(local, remote)
                .build()
        );
        log.info("[push] [local: {}] [remote: {}] [result: {}]", local, remote, result);
        return new CommandResult(T.StrUtil.contains(result, "failed") ? 1 : 0, result);
    }

    /**
     * pull
     * 0 success; !0 failed
     */
    public CommandResult pull(String remote, String local) {
        String result =  commandExec.exec(AdbCommandBuilder.builder()
                .serial(this.getSerial())
                .buildPullCommand(remote, local)
                .build()
        );
        log.info("[pull] [remote: {}] [local: {}] [result: {}]", remote, local, result);
        return new CommandResult(T.StrUtil.containsAny(result, "file pulled", "files pulled") ? 0 : 1, result);
    }

    /**
     * list dir
     * ls -l
     * stat filename
     */
    public List<Map> listDir(String path) {
        String result = commandExec.exec(AdbCommandBuilder.builder()
                .serial(this.getSerial())
                .buildLsDirCommand(path)
                .build()
        );
        if (T.StrUtil.contains(result, "No such file or directory")) {
            log.warn("[listDir] [path: {}] [result: {}]", path, result);
            throw new APIException(RCode.NOT_EXISTS);
        }

        if (T.StrUtil.contains(result, "Permission denied")) {
            log.warn("[listDir] [path: {}] [result: {}]", path, result);
            throw new APIException(RCode.NOT_PERMISSION);
        }

        List<CompletableFuture<Map>> futureList = T.ListUtil.list(false);

        List<Map> listDir = T.ListUtil.list(true);
        String[] lines = result.split("\\n");
        boolean isDir = false;
        for (String line : lines) {
            if (line.startsWith("total")) {
                isDir = true;
                continue;
            }
            String[] split = line.split("\\s+");
            String name;
            // link file|dir
            if (10 == split.length) {
                name = split[7];
            } else {
                name = split[split.length - 1];
            }

            String statFilePath = isDir ? Paths.get(path).resolve(name).toString() : path;
            String statCommand = "shell stat -c \"'%N %A %g %u %s %a %X %Y'\" " + statFilePath;
            futureList.add(
                    CompletableFuture.supplyAsync(() -> {
                        String statResult = commandExec.exec(AdbCommandBuilder.builder()
                                .serial(this.getSerial())
                                .buildShellCommand(statCommand.replaceAll("\\\\", "/"))
                                .build()
                        );
                        // reverse result
                        List<String> list = Arrays.asList(statResult.split("\\s+"));
                        Collections.reverse(list);

                        String fullName = list.get(7).replaceAll("'|`", "");
                        Map<String, Object> relMap = T.MapUtil.newHashMap();
                        relMap.put("name", name);
                        relMap.put("value", T.MapUtil.builder()
                                .put("id", Base32Codec.Base32Encoder.ENCODER.encode(fullName.getBytes()))
                                .put("fullName", fullName)
                                .put("gid", Long.parseLong(list.get(5)))
                                .put("uid", Long.parseLong(list.get(4)))
                                .put("size", Long.parseLong(list.get(3)))
                                .put("permissions", Long.parseLong(list.get(2)))
                                .put("cts", Long.parseLong(list.get(1)))
                                .put("uts", Long.parseLong(list.get(0)))
                                .put("isDir", list.get(6).startsWith("d"))
                                .put("isBlk", list.get(6).startsWith("b"))
                                .put("isFifo", list.get(6).startsWith("p"))
                                .put("isLink", list.get(6).startsWith("l"))
                                .put("isReg", list.get(6).startsWith("-"))
                                .put("isSock", list.get(6).startsWith("s"))
                                .build());
                        return relMap;
                    }, getThreadPool())
            );

            Map<Object, Object> m = T.MapUtil.builder()
                    .put("name", name)
                    .build();
            listDir.add(m);
        }

        try {
            CompletableFuture.allOf(futureList.toArray(new CompletableFuture[0])).get();
            futureList.forEach(f -> {
                Map map = f.getNow(null);
                if (T.MapUtil.isNotEmpty(map)) {
                    String name = T.MapUtil.getStr(map, "name");
                    Map fileAttr = listDir.stream().filter(m -> T.MapUtil.getStr(m, "name").equals(name)).findFirst().get();
                    fileAttr.putAll(T.MapUtil.get(map, "value", Map.class));
                }
            });
        } catch (Exception e) {
            log.warn(e);
        }
        return listDir;
    }

    /**
     * listApp
     * adb shell pm list packages
     *
     * @return
     */
    public List<Map> listApp(String arg) {
        String result = commandExec.exec(AdbCommandBuilder.builder()
                .serial(this.getSerial())
                .buildPmListPackagesCommand(arg)
                .build()
        );

        List<Map> listApp = T.ListUtil.list(true);

        List<CompletableFuture<Map>> futureList = T.ListUtil.list(false);

        String prefix = "package:";
        String[] lines = result.split("\\n");
        for (String line : lines) {
            String packageName = T.StrUtil.trim(line.substring(prefix.length()));
            if (T.StrUtil.equals(DEFAULT_DROIDVNC_NG_PKG_NAME, packageName)) continue;

            String dumpsysResult = commandExec.exec(AdbCommandBuilder.builder()
                    .serial(this.getSerial())
                    .buildShellCommand("shell dumpsys package " + packageName)
                    .build()
            );
            String[] split = dumpsysResult.split("\\n");
            String version = "", apkPath = "";
            for (String s : split) {
                if (s.contains("versionName=")) {
                    version = T.StrUtil.trim(s).replaceAll("versionName=", "");
                }
                if (s.contains("path: ")) {
                    apkPath = T.StrUtil.trim(s).replaceAll("path: ", "");
                }
            }
            if (T.StrUtil.isNotEmpty(apkPath)) {
                String finalApkPath = apkPath;
                futureList.add(
                        CompletableFuture.supplyAsync(() -> {
                            try {
                                CommandResult md5sumRes = this.md5sum(finalApkPath);
                                String md5Value = md5sumRes.output();
                                File localApk = T.FileUtil.file(Constant.TEMP_PATH, md5Value + ".apk");
                                if (!T.FileUtil.exist(localApk)) {
                                    CommandResult pulled = this.pull(finalApkPath, localApk.getAbsolutePath());
                                    if (0 != pulled.exitCode()) {
                                        log.warn("[listApp] [pull apk error] [pkg: {}]", packageName);
                                        return null;
                                    }
                                }
                                ApkUtil apkUtil = new ApkUtil();
                                ApkInfo apkInfo = apkUtil.parseApk(localApk.getAbsolutePath());
                                String appName = apkInfo.getLabel();
                                // String iconFilename = apkInfo.getIcon();
                                // String base64IconDate = apkUtil.extractFileFromApk(localApk.getAbsolutePath(), iconFilename);
                                // if (T.StrUtil.isNotEmpty(base64IconDate)) {
                                //     base64IconDate = "data:image/jpeg;base64," + base64IconDate;
                                // }

                                Map<String, Object> relMap = T.MapUtil.newHashMap();
                                relMap.put("pkg", packageName);
                                relMap.put("value", T.MapUtil.builder()
                                        .put("name", appName)
                                        // .put("icon", base64IconDate)
                                        .build());
                                return relMap;
                            } catch (Exception e) {
                                log.error(e, "[listApp] [parse apk] [pkg: {}]", packageName);
                            }
                            return null;
                        }, getThreadPool())
                );
            }

            Map<Object, Object> m = T.MapUtil.builder()
                    .put("packageName", packageName)
                    .put("version", version)
                    .build();
            listApp.add(m);
        }

        try {
            CompletableFuture.allOf(futureList.toArray(new CompletableFuture[0])).get();
            futureList.forEach(f -> {
                Map map = f.getNow(null);
                if (T.MapUtil.isNotEmpty(map)) {
                    String pkg = T.MapUtil.getStr(map, "pkg");
                    Map appAttr = listApp.stream().filter(m -> T.MapUtil.getStr(m, "packageName").equals(pkg)).findFirst().get();
                    appAttr.putAll(T.MapUtil.get(map, "value", Map.class));
                }
            });
        } catch (Exception e) {
            log.warn(e);
        }
        return listApp;
    }

    /**
     * install app
     * adb install apk
     */
    public CommandResult install(String localFilePath, boolean isDebugApk, boolean isReInstall) {
        String result = commandExec.exec(AdbCommandBuilder.builder()
                .serial(this.getSerial())
                .buildInstallCommand(localFilePath, isDebugApk, isReInstall)
                .build());

        log.info("[install] [localFilePath: {}] [isDebugApk: {}] [isReInstall: {}] [result: {}]", localFilePath, isDebugApk, isReInstall, result);
        return new CommandResult(T.StrUtil.containsAny(result, "Success") ? 0 : 1, result);
    }

    /**
     * uninstall app
     * adb uninstall package_name
     */
    public CommandResult uninstall(String packageName) {
        String result =  commandExec.exec(AdbCommandBuilder.builder()
                .serial(this.getSerial())
                .buildUnInstallCommand(packageName)
                .build()
        );
        log.info("[uninstall] [packageName: {}] [result: {}]", packageName, result);
        return new CommandResult(T.StrUtil.containsAny(result, "Success") ? 0 : 1, result);
    }

    /**
     * stop app
     * adb shell am force-stop package_name
     */
    public CommandResult stopApp(String packageName) {
        String result = commandExec.exec(AdbCommandBuilder.builder()
                .serial(this.getSerial())
                .buildStopAppCommand(packageName)
                .build()
        );
        log.info("[stopApp] [packageName: {}] [result: {}]", packageName, result);
        return new CommandResult(T.StrUtil.isEmpty(result) ? 0 : 1, result);
    }

    /**
     * iptables -F
     * iptables -X
     */
    @Deprecated
    private void cleanIptables() {
        // Delete all rules in chain or all chains
        commandExec.exec(AdbCommandBuilder.builder()
                .serial(this.getSerial())
                .buildShellCommand("shell iptables -F")
                .build()
        );
        // Delete user-defined chain
        commandExec.exec(AdbCommandBuilder.builder()
                .serial(this.getSerial())
                .buildShellCommand("shell iptables -X")
                .build()
        );
    }

    /**
     * list tcpdump
     */
    public List<Map> listTcpdump() {
        String result =  commandExec.exec(AdbCommandBuilder.builder()
                .serial(this.getSerial())
                .buildShellCommand(String.format("shell \"ps -ef | grep tcpdump | grep -v grep | grep capture_ | awk '{print $NF}' \""))
                .build());

        List<Map> list = T.ListUtil.list(true);

        String[] lines = result.split("\\n");
        for (String line : lines) {
            try {
                String fileName = T.FileUtil.mainName(line);
                String taskId = "", packageName = "";
                if (fileName.contains("capture_all_")) {
                    taskId = fileName.replaceAll("capture_all_", "");
                } else {
                    String[] split = fileName.split("_");
                    packageName = split[2];
                    taskId = split[split.length - 1];
                }
                Map<Object, Object> m = T.MapUtil.builder()
                        .put("id", taskId)
                        .put("packageName", packageName)
                        .build();
                list.add(m);
            } catch (Exception e) {
                log.warn(e, "[listTcpdump] [get task info error] [line: {}]", line);
            }
        }
        return list;
    }

    /**
     * start Tcpdump
     * tcpdump pcap
     */
    public CommandResult startTcpdump(String packageName) {
        String taskId = T.IdUtil.fastSimpleUUID();
        if (T.StrUtil.isNotEmpty(packageName)) {
            log.info("[startTcpdump] [capture app package] [pkg: {}]", packageName);
            String dumpsysResult =  commandExec.exec(AdbCommandBuilder.builder()
                    .serial(this.getSerial())
                    .buildShellCommand("shell dumpsys package " + packageName)
                    .build()
            );
            String[] lines = dumpsysResult.split("\\n");
            String userId = Arrays.stream(lines)
                    .filter(s -> T.StrUtil.contains(s, "userId="))
                    .findFirst()
                    .map(s -> T.StrUtil.trim(s).replaceAll("userId=", ""))
                    .orElseThrow(() -> new APIException("Not found userId by package name. package name: " + packageName));

            commandExec.exec(AdbCommandBuilder.builder()
                    .serial(this.getSerial())
                    .buildShellCommand(String.format("shell iptables -A OUTPUT -m owner --uid-owner %s -j CONNMARK --set-mark %s", userId, userId))
                    .build());
            commandExec.exec(AdbCommandBuilder.builder()
                    .serial(this.getSerial())
                    .buildShellCommand(String.format("shell iptables -A INPUT -m connmark --mark %s -j NFLOG --nflog-group %s", userId, userId))
                    .build());
            commandExec.exec(AdbCommandBuilder.builder()
                    .serial(this.getSerial())
                    .buildShellCommand(String.format("shell iptables -A OUTPUT -m connmark --mark %s -j NFLOG --nflog-group %s", userId, userId))
                    .build());

            String ruleList = commandExec.exec(AdbCommandBuilder.builder()
                    .serial(this.getSerial())
                    .buildShellCommand("shell iptables -L")
                    .build());
            log.info("[startTcpdump] [iptables -L] [result: {}]", ruleList);

            // pcap 格式：capture_{userId}_{pcakageName}_{taskId}.pcap
            String pcapFilePath = "/data/local/tmp/capture_" + userId + "_" + packageName + "_" + taskId + ".pcap";
            commandExec.execForProcess(AdbCommandBuilder.builder()
                    .serial(this.getSerial())
                    .buildShellCommand(String.format("shell tcpdump -i nflog:%s -w %s &", userId, pcapFilePath))
                    .build());
        } else {
            log.info("[startTcpdump] [capture all package]");
            // pcap 格式：capture_all_{taskId}.pcap
            String pcapFilePath = "/data/local/tmp/capture_all_" + taskId + ".pcap";
            commandExec.execForProcess(AdbCommandBuilder.builder()
                    .serial(this.getSerial())
                    .buildShellCommand(String.format("shell tcpdump not port %s -w %s &", this.vncPort, pcapFilePath))
                    .build());
        }

        String result =  commandExec.exec(AdbCommandBuilder.builder()
                .serial(this.getSerial())
                .buildShellCommand(String.format("shell \"ps -ef | grep tcpdump | grep -v grep | grep %s | awk '{print $2}' \"", taskId))
                .build());
        log.info("[startTcpdump] [taskId: {}] [tcpdump pid: {}]", taskId, result);
        return new CommandResult(T.StrUtil.isNotEmpty(result) ? 0 : 1, taskId);
    }

    /**
     * stop tcpdump
     * kill -INT {pid}
     */
    public CommandResult stopTcpdump(String id) {
        String pcapFilePath = commandExec.exec(AdbCommandBuilder.builder()
                .serial(this.getSerial())
                .buildShellCommand(String.format("shell \"ps -ef | grep tcpdump | grep -v grep | grep %s | awk '{print $NF}' \"", id))
                .build());
        if (T.StrUtil.isNotEmpty(pcapFilePath)) {
            if (!pcapFilePath.contains("capture_all_")) {
                // 删除 iptables rule
                String[] split = T.FileUtil.mainName(pcapFilePath).split("_");
                String userId = split[1];
                log.info("[stopTcpdump] [remove iptables rule] [userId: {}]", userId);
                commandExec.exec(AdbCommandBuilder.builder()
                        .serial(this.getSerial())
                        .buildShellCommand(String.format("shell iptables -D OUTPUT -m owner --uid-owner %s -j CONNMARK --set-mark %s", userId, userId))
                        .build());
                commandExec.exec(AdbCommandBuilder.builder()
                        .serial(this.getSerial())
                        .buildShellCommand(String.format("shell iptables -D INPUT -m connmark --mark %s -j NFLOG --nflog-group %s", userId, userId))
                        .build());
                commandExec.exec(AdbCommandBuilder.builder()
                        .serial(this.getSerial())
                        .buildShellCommand(String.format("shell iptables -D OUTPUT -m connmark --mark %s -j NFLOG --nflog-group %s", userId, userId))
                        .build());
            }
        }
        String result = commandExec.exec(AdbCommandBuilder.builder()
                .serial(this.getSerial())
                .buildShellCommand(String.format("shell \"ps -ef | grep tcpdump | grep -v grep | grep %s | awk '{print $2}' | xargs kill -INT \"", id))
                .build());

        // 等待 tcpdump 资源释放，避免出现错误：The capture file appears to have been cut short in the middle of a packet.
        for (int i = 0; i < 10; i++) {
            T.ThreadUtil.sleep(500);

            String str = commandExec.exec(AdbCommandBuilder.builder()
                    .serial(this.getSerial())
                    .buildShellCommand(String.format("shell \"ps -ef | grep tcpdump | grep -v grep | grep %s \"", id))
                    .build());
            log.info("[stopTcpdump] [id: {}] [is running: {}]", id, str);
            if (T.StrUtil.isEmpty(str)) {
                break;
            }
        }

        log.info("[stopTcpdump] [id: {}] [pcapFilePath: {}] [result: {}]", id, pcapFilePath, result);
        if (T.StrUtil.isEmpty(result)) {
            return new CommandResult(0, pcapFilePath);
        }
        return new CommandResult(1, result);
    }

    /**
     * exec shell command
     */
    public void execShellCommand(String shellCmd) {
        String result = commandExec.exec(AdbCommandBuilder.builder()
                .serial(this.getSerial())
                .buildShellCommand(shellCmd)
                .build());
        log.info("[execShellCommand] [shellCmd: {}] [result: {}]", shellCmd, result);
    }

    /**
     * exec shell command
     */
    public String execShellCommand(String cmd, Integer timeout){
        String result = commandExec.exec(AdbCommandBuilder.builder()
                .serial(this.getSerial())
                .buildShellCommand("shell " + cmd)
                .build());
        return result;
    }

    /**
     * 1. 添加自定义链
     * 2. 自定义链添加到 OUTPUT 链中
     */
    private void addAswOutputChain() {
        // name=ASW_OUTPUT
        this.execShellCommand("shell iptables -N ASW_OUTPUT");

        String outputChainResult = commandExec.exec(AdbCommandBuilder.builder()
                .serial(this.getSerial())
                .buildShellCommand(String.format("shell \"iptables -L OUTPUT --line-numbers | grep ASW_OUTPUT\""))
                .build());
        log.info("[addAswOutputChain] [ASW_OUTPUT in OUTPUT Chain] [exist: {}]", T.StrUtil.isEmpty(outputChainResult));
        if (T.StrUtil.isEmpty(outputChainResult)) {
            // ASW_OUTPUT 添加到 OUTPUT 链中
            this.execShellCommand("shell iptables -A OUTPUT -j ASW_OUTPUT");
        }
    }

    /**
     * ASW_OUTPUT chain rules
     * iptables -nL ASW_OUTPUT --line-numbers
     */
    public List<Map> listAcl() {
        String result = commandExec.exec(AdbCommandBuilder.builder()
                .serial(this.getSerial())
                .buildIptablesLnRulesCommand("ASW_OUTPUT")
                .build());

        List<Map> chainList = T.ListUtil.list(true);

        String[] lines = result.split("\\n");
        for (String line : lines) {
            String[] split = line.split("\\s+");

            String chainIndex = T.StrUtil.trim(split[0]);

            if (T.StrUtil.isNumeric(chainIndex)) {
                String protocol = T.StrUtil.trim(split[2]);
                Map<Object, Object> m = T.MapUtil.builder()
                        .put("num", Integer.valueOf(chainIndex))
                        .put("protocol", protocol)
                        .build();

                String destIp = T.StrUtil.trim(split[5]);
                if (!T.StrUtil.equals("0.0.0.0/0", destIp)) {
                    m.put("ip", destIp);
                }

                if (split.length == 8) {
                    String dpt = T.StrUtil.trim(split[7]);
                    dpt = dpt.replaceAll("dpt:", "");
                    if (T.StrUtil.isNumeric(chainIndex)) {
                        m.put("port", Integer.valueOf(dpt));
                    }
                }
                chainList.add(m);
            }
        }
        return chainList;
    }

    /**
     * add chain rule
     * iptables -A ASW_OUTPUT -p prot -d ip --dport port  -j ACCEPT
     */
    public void addAcl(String protocol, String ip, String port) {
        // add chain
        this.addAswOutputChain();

        // add chain ruls
        String result = commandExec.exec(AdbCommandBuilder.builder()
                .serial(this.getSerial())
                .buildIptablesAddRuleCommand("ASW_OUTPUT", protocol, ip, port)
                .build());
        log.info("[addAcl] [protocol: {}] [ip: {}] [port: {}] [result: {}]", protocol, ip, port, result);
    }


    /**
     * del chain rule
     * iptables -D ASW_OUTPUT -p prot -d ip --dport port  -j ACCEPT
     */
    public void deleteAcl(String protocol, String ip, String port) {
        // add chain ruls
        String result = commandExec.exec(AdbCommandBuilder.builder()
                .serial(this.getSerial())
                .buildIptablesDelRuleCommand("ASW_OUTPUT", protocol, ip, port)
                .build());
        log.info("[deleteAcl] [protocol: {}] [ip: {}] [port: {}] [result: {}]", protocol, ip, port, result);
    }

    public CommandResult clearAppData(String packageName) {
        String result = commandExec.exec(AdbCommandBuilder.builder()
                .serial(this.getSerial())
                .buildClearAppDateCommand(packageName)
                .build());
        log.info("[clearAppData] [packageName: {}]", packageName);
        return new CommandResult(T.StrUtil.containsAny(result, "Success") ? 0 : 1, result);
    }

    /**
     * flushAcl
     * iptables -F ASW_OUTPUT
     */
    public CommandResult flushAcl() {
        String result = commandExec.exec(AdbCommandBuilder.builder()
                .serial(this.getSerial())
                .buildIptablesFlushRuleCommand("ASW_OUTPUT")
                .build());
        log.info("[flushAcl] [result: {}]", result);
        return new CommandResult(T.StrUtil.isNotEmpty(result) ? 1 : 0, result);
    }


    public boolean findPackageInstall(String packageName) {
        String result = commandExec.exec(AdbCommandBuilder.builder()
                .serial(this.getSerial())
                .buildCheckPackage(packageName)
                .build()
        );

       return T.StrUtil.equals(result, packageName);
    }

    public CommandResult startApp(String packageName) {
        String result = commandExec.exec(AdbCommandBuilder.builder()
                .serial(this.getSerial())
                .buildStartAPP(packageName)
                .build()
        );
        log.info("[startApp] [result: {}]", result);
        return new CommandResult(T.StrUtil.isNotEmpty(result) ? 1 : 0, result);
    }


    public void closeOtherApps() {
        commandExec.exec(AdbCommandBuilder.builder()
                .serial(this.getSerial())
                .buildShellCommand(String.format("shell 'for app in $(pm list packages -3 | grep -v \"net.christianbeier.droidvnc_ng\" | cut -f 2 -d \":\"); do am force-stop $app; done'"))
                .build());
    }

    private synchronized ExecutorService getThreadPool() {
        if (threadPool == null) {
            threadPool = new ThreadPoolExecutor(
                    5,
                    10,
                    30,
                    TimeUnit.SECONDS,
                    new ArrayBlockingQueue<Runnable>(10000),
                    new NamedThreadFactory("API-", true));
        }
        return threadPool;
    }


    public CommandResult execPlaybook(String scriptPath, String tid, String packageName, String type, File logFile) {
        log.info("[execPlaybook] [begin!] [serial:{}]", this.getSerial());
        List<String> command;
        File environment = null;
        if (T.StrUtil.equals(type, "python")){
            // check requirements.txt
            Path parent = Paths.get(scriptPath).getParent();
            Path resolve = parent.resolve("requirements.txt");
            command = new PythonCommandBuilder("python")
                    .buildRunPythonScript(scriptPath, tid, packageName, this.getSerial())
                    .build();
            if (T.FileUtil.exist(resolve.toString())){
                // create venv
                environment = T.FileUtil.file(parent.toString(), "environment");
                commandExec.exec(new PythonCommandBuilder("python").buildCreateVenv(environment.getAbsolutePath()).build());
                commandExec.exec(new PythonCommandBuilder(T.StrUtil.concat(true, environment.getAbsolutePath(), "/bin/pip")).buildUpgradePip().build());
                commandExec.exec(new PythonCommandBuilder(T.StrUtil.concat(true, environment.getAbsolutePath(), "/bin/pip")).buildInstallRequirements(resolve.toString()).build());
                command = new PythonCommandBuilder(T.StrUtil.concat(true, environment.getAbsolutePath(), "/bin/python"))
                        .buildRunPythonScript(scriptPath, tid, packageName, this.getSerial())
                        .build();
            }
        }else {
            command = new PythonCommandBuilder("python")
                    .buildRunAirScript(ANDROID_LAUNCHER, scriptPath, tid, packageName, this.getSerial())
                    .build();
        }
        Process process = commandExec.execForProcess(command);

        T.FileUtil.appendString(T.StrUtil.concat(true, "$ ", command.stream().collect(Collectors.joining(" ")), "\n"), logFile, "UTF-8");
        InputStreamReader inputStreamReader = null;
        BufferedReader bufferedReader = null;
        try {
            inputStreamReader = new InputStreamReader(process.getInputStream(), "UTF-8");
            bufferedReader = new BufferedReader(inputStreamReader);
            String line;
            while ((line = bufferedReader.readLine()) != null) {
                if (T.ObjectUtil.isNotNull(interrupt) && interrupt){
                    log.info("[PlaybookRunnable] [execPlaybook] [stop exec playbook]");
                    process.destroyForcibly();
                }
                // 处理每一行输出
                T.FileUtil.appendString(T.StrUtil.concat(true, line, "\n"), logFile, "UTF-8");
            }
            int exitCode = process.waitFor();
            return new CommandResult(exitCode, T.StrUtil.EMPTY);
        } catch (Exception e) {
            process.destroyForcibly();
            throw new APIException(RCode.ERROR);
        }finally {
            T.FileUtil.del(environment);
            T.IoUtil.close(inputStreamReader);
            T.IoUtil.close(bufferedReader);
        }
    }
}