package net.geedge.api.controller;

import cn.hutool.core.codec.Base32Codec;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.collection.ListUtil;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.thread.ThreadUtil;
import cn.hutool.core.util.ArrayUtil;
import cn.hutool.log.Log;
import jakarta.servlet.http.HttpServletResponse;
import net.geedge.api.entity.EnvApiYml;
import net.geedge.api.util.AdbUtil;
import net.geedge.api.util.CommandExec;
import net.geedge.api.util.PlaybookRunnable;
import net.geedge.common.*;
import net.lingala.zip4j.ZipFile;
import net.lingala.zip4j.model.ZipParameters;
import net.lingala.zip4j.model.enums.CompressionLevel;
import net.lingala.zip4j.model.enums.CompressionMethod;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;

import java.io.*;
import java.nio.file.*;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.*;
import java.util.regex.Pattern;

@RestController
@RequestMapping("/api/v1/env")
public class APIController {

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

    private final AdbUtil adbUtil;

    @Autowired
    public APIController(EnvApiYml envApiYml) {
        this.adbUtil = AdbUtil.getInstance(envApiYml.getAdb(), new CommandExec(null));
    }

    @Autowired
    private  EnvApiYml apiYml;

    @GetMapping("/status")
    public R status() {
        return R.ok(adbUtil.status());
    }

    @PostMapping("/file")
    public R push(@RequestParam(value = "file") MultipartFile file, @RequestParam String path) throws IOException {
        File tempFile = null;
        try {
            tempFile = T.FileUtil.file(Constant.TEMP_PATH, file.getOriginalFilename());
            file.transferTo(tempFile);

            AdbUtil.CommandResult result = adbUtil.push(tempFile.getAbsolutePath(), path);
            if (0 != result.exitCode()) {
                return R.error(result.output());
            }
        } finally {
            T.FileUtil.del(tempFile);
        }
        return R.ok();
    }

    @GetMapping("/file/{fileId}")
    public void pull(@PathVariable String fileId, HttpServletResponse response) throws IOException {
        byte[] decode = Base32Codec.Base32Decoder.DECODER.decode(fileId);
        String filePath = T.StrUtil.str(decode, T.CharsetUtil.CHARSET_UTF_8);
        String fileName = T.FileUtil.getName(filePath);

        File tempFile = T.FileUtil.file(Constant.TEMP_PATH, fileName);
        try {
            AdbUtil.CommandResult result = adbUtil.pull(filePath, tempFile.getAbsolutePath());
            if (0 != result.exitCode()) {
                throw new APIException(result.output());
            }
            if (T.FileUtil.isDirectory(tempFile)) {
                File zip = T.ZipUtil.zip(tempFile);
                try {
                    T.ResponseUtil.downloadFile(response, zip.getName(), T.FileUtil.readBytes(zip));
                } finally {
                    T.FileUtil.del(zip);
                }
            } else {
                T.ResponseUtil.downloadFile(response, fileName, T.FileUtil.readBytes(tempFile));
            }
        } finally {
            T.FileUtil.del(tempFile);
        }
    }

    @GetMapping("/file")
    public R listDir(@RequestParam(defaultValue = "/") String path) {
        List<Map> listDir = adbUtil.listDir(path);
        Map<Object, Object> data = T.MapUtil.builder()
                .put("path", path)
                .put("records", listDir)
                .build();
        return R.ok(data);
    }

    @GetMapping("/app")
    public R listApp(@RequestParam(required = false) String arg) {
        return R.ok().putData("records", adbUtil.listApp(arg));
    }

    @PostMapping("/app")
    public R install(@RequestParam(value = "file", required = false) MultipartFile file,
                     @RequestParam(required = false) String path) throws IOException {
        if (file != null) {
            File tempFile = null;
            try {
                tempFile = T.FileUtil.file(Constant.TEMP_PATH, file.getOriginalFilename());
                file.transferTo(tempFile);

                AdbUtil.CommandResult result = adbUtil.install(tempFile.getAbsolutePath(), true, true);
                if (0 != result.exitCode()) {
                    throw new APIException(result.output());
                }
                return R.ok();
            } finally {
                T.FileUtil.del(tempFile);
            }
        }

        if (T.StrUtil.isNotEmpty(path)) {
            AdbUtil.CommandResult result = adbUtil.install(path, true, true);
            if (0 != result.exitCode()) {
                throw new APIException(result.output());
            }
            return R.ok();
        }
        return R.error(RCode.BAD_REQUEST);
    }

    @DeleteMapping("/app")
    public R uninstall(@RequestParam String packageName) {
        AdbUtil.CommandResult result = adbUtil.uninstall(packageName);
        if (0 != result.exitCode()) {
            throw new APIException(result.output());
        }
        return R.ok();
    }

    @GetMapping("/pcap")
    public R listTcpdump() {
        return R.ok().putData("records", adbUtil.listTcpdump());
    }

    @PostMapping("/pcap")
    public R startTcpdump(@RequestParam(required = false, defaultValue = "") String packageName) {
        AdbUtil.CommandResult result = adbUtil.startTcpdump(packageName);
        if (0 != result.exitCode()) {
            throw new APIException("exec tcpdump error");
        }
        return R.ok().putData("id", result.output());
    }

    @DeleteMapping("/pcap")
    public synchronized void stopTcpdump(@RequestParam String id,
                                         @RequestParam(required = false, defaultValue = "false") Boolean returnFile,
                                         HttpServletResponse response) throws IOException {
        AdbUtil.CommandResult result = adbUtil.stopTcpdump(id);
        if (0 != result.exitCode()) {
            throw new APIException(result.output());
        }

        String filePath = result.output();
        try {
            if (returnFile) {
                // response pcap file
                File tempFile = T.FileUtil.file(Constant.TEMP_PATH, id + ".pcap");
                try {
                    if (T.StrUtil.isEmpty(filePath)) {
                        throw new APIException(RCode.NOT_EXISTS);
                    }
                    AdbUtil.CommandResult pulled = adbUtil.pull(filePath, tempFile.getAbsolutePath());
                    if (0 != pulled.exitCode()) {
                        throw new APIException(pulled.output());
                    }
                    T.ResponseUtil.downloadFile(response, tempFile.getName(), T.FileUtil.readBytes(tempFile));
                } finally {
                    T.FileUtil.del(tempFile);
                }
            } else {
                // response taskid
                response.getWriter().write(T.JSONUtil.toJsonStr(R.ok().putData("id", id)));
            }
        } finally {
            if (T.StrUtil.isNotEmpty(filePath)) {
                // remove pcap file
                adbUtil.execShellCommand(String.format("shell rm -rf %s", filePath));
            }
        }
    }

    @PostMapping("/shell")
    public R execShellCmd(@RequestBody Map<String, Object> requestBody) {
        String cmd = T.MapUtil.getStr(requestBody, "cmd", "");
        if (T.StrUtil.isEmpty(cmd)) {
            return R.error(RCode.BAD_REQUEST);
        }

        Integer timeout = T.MapUtil.getInt(requestBody, "timeout", 10);
        return R.ok().putData("result", adbUtil.execShellCommand(cmd, timeout));
    }

    @GetMapping("/acl")
    public R listAcl() {
        return R.ok().putData("records", adbUtil.listAcl());
    }

    @PostMapping("/acl")
    public R addAcl(@RequestBody Map<String, Object> requestBody) {
        String ip = T.MapUtil.getStr(requestBody, "ip");
        String port = T.MapUtil.getStr(requestBody, "port");
        if (T.StrUtil.isAllEmpty(ip, port)) {
            return R.error(RCode.BAD_REQUEST);
        }

        String protocol = T.MapUtil.getStr(requestBody, "protocol", "all");
        if (!T.StrUtil.equalsAny(protocol, "tcp", "udp", "all")) {
            return R.error(RCode.BAD_REQUEST);
        }

        if ("all".equals(protocol) && T.StrUtil.isEmpty(ip)) {
            return R.error(RCode.BAD_REQUEST);
        }

        adbUtil.addAcl(protocol, ip, port);
        return R.ok().putData("records", adbUtil.listAcl());
    }

    @DeleteMapping("/acl")
    public R deleteAcl(@RequestBody Map<String, Object> requestBody) {
        String ip = T.MapUtil.getStr(requestBody, "ip");
        String port = T.MapUtil.getStr(requestBody, "port");
        if (T.StrUtil.isAllEmpty(ip, port)) {
            return R.error(RCode.BAD_REQUEST);
        }

        String protocol = T.MapUtil.getStr(requestBody, "protocol", "all");
        if (!T.StrUtil.equalsAny(protocol, "tcp", "udp", "all")) {
            return R.error(RCode.BAD_REQUEST);
        }

        if ("all".equals(protocol) && T.StrUtil.isEmpty(ip)) {
            return R.error(RCode.BAD_REQUEST);
        }

        adbUtil.deleteAcl(protocol, ip, port);
        return R.ok().putData("records", adbUtil.listAcl());
    }

    @DeleteMapping("/acl/flush")
    public R flushAcl() {
        AdbUtil.CommandResult result = adbUtil.flushAcl();
        if (0 != result.exitCode()) {
            return R.error(result.output());
        }
        return R.ok();
    }

    @PostMapping("/playbook")
    public R execPlaybook(@RequestParam("file") MultipartFile file,
                          @RequestParam("packageName") String packageName,
                          @RequestParam("id") String id,
                          @RequestParam("type") String type,
                          @RequestParam("reInstall") Boolean reInstall,
                          @RequestParam("clearCache") Boolean clearCache,
                          @RequestParam("unInstall") Boolean unInstall) {
        File apkFile = null;
        File scriptPath = null;
        File destination = null;
        try {
            File playbookDir = T.FileUtil.file(Constant.TEMP_PATH, id);
            destination = T.FileUtil.file(Constant.TEMP_PATH, id, file.getName());
            T.FileUtil.writeBytes(file.getInputStream().readAllBytes(), destination);

            // unzip file
            T.ZipUtil.unzip(destination, playbookDir);

            // apk
            apkFile = Arrays.stream(playbookDir.listFiles(new FilenameFilter() {
                @Override
                public boolean accept(File dir, String name) {
                    return name.endsWith(".apk");
                }
            })).findFirst().get();

            // playbook zip
            File playbook = Arrays.stream(playbookDir.listFiles(new FilenameFilter() {
                @Override
                public boolean accept(File dir, String name) {
                    return name.endsWith(".zip") && !name.equals(file.getName());
                }
            })).findFirst().get();

            // unzip playbook zip
            if (T.StrUtil.equals(type, "python")){
                playbookDir = T.FileUtil.file(Constant.TEMP_PATH, id, "main");
                T.ZipUtil.unzip(playbook, playbookDir);
                scriptPath = Arrays.stream(playbookDir.listFiles(new FileFilter() {
                    @Override
                    public boolean accept(File pathname) {
                        return pathname.getName().equals("main.py");
                    }
                })).findFirst().get();
            }else {
                T.ZipUtil.unzip(playbook, playbookDir);
                scriptPath = Arrays.stream(playbookDir.listFiles(new FileFilter() {
                    @Override
                    public boolean accept(File pathname) {
                        return pathname.getName().endsWith(".air");
                    }
                })).findFirst().get();
            }
        } catch (Exception e) {
            log.error(e.getMessage());
            throw new APIException(RCode.ERROR);
        } finally {
            T.FileUtil.del(destination);
        }

        PlaybookRunnable playbookRunnable = new PlaybookRunnable(apiYml, apkFile, scriptPath, id, packageName, type, reInstall, clearCache, unInstall);
        ThreadUtil.execAsync(playbookRunnable);
        return R.ok();
    }

    @GetMapping("/playbook/{id}")
    public R checkJobResult(@PathVariable("id") String id){
        if (T.StrUtil.isEmpty(id)) {
            throw new APIException(RCode.BAD_REQUEST);
        }
        File statusFile = FileUtil.file(Constant.TEMP_PATH, id, "result.json");
        String status = T.FileUtil.readString(statusFile, "UTF-8");
        return R.ok().putData(status);
    }

    @DeleteMapping("/playbook/{id}")
    public R cancel(@PathVariable("id") String id){
        if (T.StrUtil.isEmpty(id)) {
            throw new APIException(RCode.BAD_REQUEST);
        }
        if (CollUtil.isNotEmpty(Constant.ACTIVE_TASKS)) {
            Constant.ACTIVE_TASKS.stream().forEach(thread -> {
                log.info(String.format("playbook thread: %s has been canceled", id));
                thread.interrupt();
            });
        }
        return R.ok();
    }


    @GetMapping("/playbook/{id}/log")
    public R getJobResultLog(@PathVariable("id") String id,
                             @RequestParam("offset") Integer offset){
        if (T.StrUtil.isEmpty(id)) {
            throw new APIException(RCode.BAD_REQUEST);
        }
        // log file
        File logFile = T.FileUtil.file(Constant.TEMP_PATH, id, "result.log");
        HashMap<Object, Object> result = T.MapUtil.newHashMap(false);

        try (RandomAccessFile raf = new RandomAccessFile(logFile, "r")) {
            if (offset < raf.length()) {
                raf.seek(offset);
                byte[] bytes = new byte[(int)raf.length() - offset];
                raf.readFully(bytes);
                String content = new String(bytes);
                result.put("content", content);
                result.put("length", bytes.length);
                result.put("offset", offset + bytes.length);
            }
        } catch (IOException e) {
            log.error("getJobResultLog error", e);
            throw new APIException(RCode.ERROR);
        }
        return R.ok().putData(result);
    }


    @GetMapping("/playbook/{id}/artifact")
    public void getJobResultArtifact(@PathVariable("id") String id, @RequestParam(value = "artifacts",required = false) String[] artifacts , HttpServletResponse response) throws IOException {
        if (T.StrUtil.isEmpty(id)) {
            throw new APIException(RCode.BAD_REQUEST);
        }
        // job dir
        File jobResult = T.FileUtil.file(Constant.TEMP_PATH, id);
        File[] files = jobResult.listFiles(new FilenameFilter() {
            @Override
            public boolean accept(File dir, String name) {
                return name.endsWith(".log") || name.endsWith(".pcap");
            }
        });

        // artifact
        List<File> artifactFiles = ListUtil.list(false);
        File playbookDir = Arrays.stream(jobResult.listFiles(new FilenameFilter() {
            @Override
            public boolean accept(File dir, String name) {
                return name.endsWith(".air") || name.equals("main");
            }
        })).toList().getFirst();

        if (ArrayUtil.isNotEmpty(artifacts)) {
            for (String artifact : artifacts) {
                if (containsRegex(artifact)) {
                    int lastSeparator = artifact.lastIndexOf(FileSystems.getDefault().getSeparator());
                    String regex = (lastSeparator >= 0) ? artifact.substring(lastSeparator + 1) : artifact;
                    String parent = (lastSeparator >= 0) ? artifact.substring(0, lastSeparator) : "";

                    // Resolve parent directory
                    Path parentPath = parent.isEmpty() ? Paths.get(playbookDir.getPath()) : Paths.get(playbookDir.getPath(), parent).normalize();

                    // Compile regex pattern
                    Pattern pattern = Pattern.compile(regex);

                    // Find matching files
                    artifactFiles = findMatchingFiles(parentPath, pattern);

                } else {
                    Path resolvedPath = Paths.get(artifact);
                    if (!resolvedPath.isAbsolute()) {
                        resolvedPath = Paths.get(playbookDir.getPath(), artifact).normalize();
                    }
                    if (Files.exists(resolvedPath)) {
                        artifactFiles.add(resolvedPath.toFile());
                    }
                }
            }
        }
        ZipFile zip = null;
        try {
            File zipFile = T.FileUtil.file(Constant.TEMP_PATH, id, T.StrUtil.concat(true, id, ".zip"));
            zip = new ZipFile(zipFile);

            ZipParameters parameters = new ZipParameters();
            parameters.setCompressionMethod(CompressionMethod.DEFLATE); // 压缩方法
            parameters.setCompressionLevel(CompressionLevel.FASTEST);   // 压缩级别，选项有 FASTEST、ULTRA 等

            // 添加文件到 ZIP
            for (File file : files) {
                zip.addFile(file, parameters);
            }
            for (File artifactFile : artifactFiles) {
                zip.addFile(artifactFile, parameters);
            }
            T.ResponseUtil.downloadFile(response, zipFile.getName(), T.FileUtil.readBytes(zipFile.getPath()));
        } finally {
            zip.close();
        }
    }

    private static boolean containsRegex(String path) {
        return path.contains("*") || path.contains("?") || path.contains("[") || path.contains("]");
    }

    private static List<File> findMatchingFiles(Path directory, Pattern pattern) throws IOException {
        List<File> artifactFiles = new ArrayList<>();
        Files.walkFileTree(directory, new SimpleFileVisitor<>() {
            @Override
            public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
                if (pattern.matcher(file.getFileName().toString()).matches()) {
                    artifactFiles.add(file.toFile());
                }
                return FileVisitResult.CONTINUE;
            }

            @Override
            public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {
                if (pattern.matcher(dir.getFileName().toString()).matches()) {
                    artifactFiles.add(dir.toFile());
                }
                return FileVisitResult.CONTINUE;
            }
        });
        return artifactFiles;
    }
}