package ai.z.chat;

import android.system.Os;
import android.util.Log;

import com.getcapacitor.JSObject;
import com.getcapacitor.Plugin;
import com.getcapacitor.PluginCall;
import com.getcapacitor.PluginMethod;
import com.getcapacitor.annotation.CapacitorPlugin;
import com.getcapacitor.annotation.Permission;

import java.io.BufferedReader;
import java.io.File;
import java.io.InputStreamReader;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

@CapacitorPlugin(name = "Shell",
    permissions = {
        @Permission(strings = {}, alias = "shell")
    }
)
public class ShellPlugin extends Plugin {
    private static final String TAG = "ShellPlugin";
    private final Map<String, Process> activeProcesses = new ConcurrentHashMap<>();
    private String currentCwd = null;
    private String homeDir = null;
    private String toolsDir = null;
    private String projectsDir = null;
    private String prefixDir = null;
    private String shellPath = null;

    @Override
    public void load() {
        homeDir = getContext().getFilesDir().getAbsolutePath();
        toolsDir = homeDir + "/tools";
        projectsDir = homeDir + "/projects";
        prefixDir = homeDir + "/usr";
        currentCwd = homeDir;
        new File(toolsDir).mkdirs();
        new File(projectsDir).mkdirs();
        new File(homeDir + "/bin").mkdirs();
        new File(homeDir + "/tmp").mkdirs();

        refreshShell();
    }

    private String prootPath = null;
    private String prootLoaderPath = null;

    private void refreshShell() {
        String nativeLibDir = getNativeLibDir();

        if (nativeLibDir != null) {
            File bundledProot = new File(nativeLibDir, "libproot.so");
            File bundledLoader = new File(nativeLibDir, "libproot-loader.so");
            if (bundledProot.exists()) {
                prootPath = bundledProot.getAbsolutePath();
                if (bundledLoader.exists()) {
                    prootLoaderPath = bundledLoader.getAbsolutePath();
                }
                Log.i(TAG, "Using bundled proot from nativeLib: " + prootPath);
            }
        }

        if (prootPath == null) {
            File prootInPrefix = new File(prefixDir + "/bin/proot");
            if (prootInPrefix.exists()) {
                prootPath = prootInPrefix.getAbsolutePath();
            }
        }

        File bash = new File(prefixDir + "/bin/bash");
        File sh = new File(prefixDir + "/bin/sh");
        if (bash.exists()) {
            try { Os.chmod(bash.getAbsolutePath(), 0755); } catch (Exception e) {}
        }
        if (sh.exists()) {
            try { Os.chmod(sh.getAbsolutePath(), 0755); } catch (Exception e) {}
        }

        shellPath = "/system/bin/sh";
    }

    private String getNativeLibDir() {
        try {
            return getContext().getApplicationInfo().nativeLibraryDir;
        } catch (Exception e) { return null; }
    }

    @PluginMethod
    public void execute(PluginCall call) {
        String command = call.getString("command", "");
        String cwd = call.getString("cwd", currentCwd);
        boolean stream = call.getBoolean("stream", false);
        int timeout = call.getInt("timeout", 300000);
        boolean useProot = call.getBoolean("useProot", false);

        if (command.isEmpty()) {
            call.reject("No command provided");
            return;
        }

        if (cwd == null || cwd.isEmpty()) cwd = homeDir;

        try {
            refreshShell();
            String[] env = buildEnv();
            String shell = shellPath != null ? shellPath : "sh";
            String actualCommand = command;

            if (useProot && prootPath != null) {
                actualCommand = prootPath + " -0 -b /dev -b /proc -b /sys -r " + prefixDir + " /bin/sh -c " + bashEscape(command);
            } else if (prootPath != null && isTermuxCommand(command)) {
                actualCommand = prootPath + " -0 -b /dev -b /proc -b /sys -r " + prefixDir + " /bin/sh -c " + bashEscape(command);
            }

            ProcessBuilder pb = new ProcessBuilder(shell, "-c", actualCommand);
            pb.directory(new File(cwd));
            pb.environment().putAll(toEnvMap(env));
            pb.redirectErrorStream(true);

            Process process;
            try {
                process = pb.start();
            } catch (java.io.IOException ioe) {
                Log.w(TAG, "Shell " + shell + " failed: " + ioe.getMessage());
                pb = new ProcessBuilder("/system/bin/sh", "-c", actualCommand);
                pb.directory(new File(cwd));
                pb.environment().putAll(toEnvMap(env));
                pb.redirectErrorStream(true);
                process = pb.start();
            }
            String processId = String.valueOf(System.currentTimeMillis());
            activeProcesses.put(processId, process);

            if (stream) {
                streamOutput(call, process, processId, cwd);
            } else {
                executeBlocking(call, process, processId, timeout);
            }
        } catch (Exception e) {
            call.reject("Execution failed: " + e.getMessage());
        }
    }

    private String bashEscape(String s) {
        return "'" + s.replace("'", "'\\''") + "'";
    }

    private boolean isTermuxCommand(String cmd) {
        if (prefixDir == null) return false;
        return cmd.contains(prefixDir + "/bin/") || cmd.contains("pkg ") || cmd.contains("apt ") || cmd.contains("dpkg ");
    }

    @PluginMethod
    public void kill(PluginCall call) {
        String processId = call.getString("pid", "");
        Process p = activeProcesses.remove(processId);
        if (p != null) {
            p.destroyForcibly();
            call.resolve(new JSObject().put("killed", true));
        } else {
            call.resolve(new JSObject().put("killed", false));
        }
    }

    @PluginMethod
    public void getCwd(PluginCall call) {
        call.resolve(new JSObject().put("cwd", currentCwd != null ? currentCwd : homeDir));
    }

    @PluginMethod
    public void setCwd(PluginCall call) {
        String cwd = call.getString("cwd", homeDir);
        if (new File(cwd).isDirectory()) {
            currentCwd = cwd;
            call.resolve(new JSObject().put("cwd", currentCwd));
        } else {
            call.reject("Not a directory: " + cwd);
        }
    }

    @PluginMethod
    public void getEnv(PluginCall call) {
        JSObject env = new JSObject();
        env.put("HOME", homeDir + "/home");
        env.put("TOOLS", toolsDir);
        env.put("PROJECTS", projectsDir);
        env.put("CWD", currentCwd);
        env.put("PREFIX", prefixDir);
        env.put("hasProot", prootPath != null);
        env.put("prootPath", prootPath != null ? prootPath : "");
        env.put("nativeLibDir", getNativeLibDir());
        call.resolve(env);
    }

    @PluginMethod
    public void testExec(PluginCall call) {
        String testPath = call.getString("path", prefixDir + "/bin/sh");
        try {
            File f = new File(testPath);
            if (!f.exists()) {
                call.resolve(new JSObject().put("exists", false).put("executable", false));
                return;
            }
            boolean canExec = f.canExecute();
            call.resolve(new JSObject().put("exists", true).put("executable", canExec).put("path", testPath));
        } catch (Exception e) {
            call.resolve(new JSObject().put("exists", false).put("executable", false).put("error", e.getMessage()));
        }
    }

    @PluginMethod
    public void writeFile(PluginCall call) {
        String path = call.getString("path", "");
        String content = call.getString("content", "");

        if (path.isEmpty()) {
            call.reject("No path provided");
            return;
        }

        try {
            File file = new File(path);
            File parent = file.getParentFile();
            if (parent != null && !parent.exists()) parent.mkdirs();
            try (java.io.FileWriter writer = new java.io.FileWriter(file)) {
                writer.write(content);
            }
            call.resolve(new JSObject().put("path", file.getAbsolutePath()).put("size", file.length()));
        } catch (Exception e) {
            call.reject("Write failed: " + e.getMessage());
        }
    }

    @PluginMethod
    public void readFile(PluginCall call) {
        String path = call.getString("path", "");
        if (path.isEmpty()) {
            call.reject("No path provided");
            return;
        }
        try {
            File file = new File(path);
            if (!file.exists()) {
                call.reject("File not found: " + path);
                return;
            }
            StringBuilder sb = new StringBuilder();
            try (BufferedReader reader = new BufferedReader(new InputStreamReader(new java.io.FileInputStream(file), "UTF-8"))) {
                String line;
                while ((line = reader.readLine()) != null) {
                    sb.append(line).append("\n");
                }
            }
            call.resolve(new JSObject().put("content", sb.toString()).put("path", file.getAbsolutePath()));
        } catch (Exception e) {
            call.reject("Read failed: " + e.getMessage());
        }
    }

    @PluginMethod
    public void mkdirs(PluginCall call) {
        String path = call.getString("path", "");
        if (path.isEmpty()) {
            call.reject("No path provided");
            return;
        }
        boolean created = new File(path).mkdirs();
        call.resolve(new JSObject().put("created", created).put("path", new File(path).getAbsolutePath()));
    }

    @PluginMethod
    public void listDir(PluginCall call) {
        String path = call.getString("path", currentCwd != null ? currentCwd : homeDir);
        File dir = new File(path);
        if (!dir.isDirectory()) {
            call.reject("Not a directory: " + path);
            return;
        }
        try {
            com.getcapacitor.JSArray files = new com.getcapacitor.JSArray();
            File[] children = dir.listFiles();
            if (children != null) {
                for (File f : children) {
                    JSObject entry = new JSObject();
                    entry.put("name", f.getName());
                    entry.put("path", f.getAbsolutePath());
                    entry.put("isDirectory", f.isDirectory());
                    entry.put("size", f.length());
                    files.put(entry);
                }
            }
            call.resolve(new JSObject().put("files", files).put("path", dir.getAbsolutePath()));
        } catch (Exception e) {
            call.reject("List failed: " + e.getMessage());
        }
    }

    @PluginMethod
    public void deleteFile(PluginCall call) {
        String path = call.getString("path", "");
        if (path.isEmpty()) {
            call.reject("No path provided");
            return;
        }
        boolean deleted = deleteRecursive(new File(path));
        call.resolve(new JSObject().put("deleted", deleted));
    }

    private String[] buildEnv() {
        String ourBin = prefixDir + "/bin";
        String termuxBin = "/data/data/com.termux/files/usr/bin";
        String termuxPrefix = "/data/data/com.termux/files/usr";
        boolean hasTermux = new File(termuxBin).isDirectory();
        boolean hasOurPrefix = new File(ourBin + "/bash").exists() || new File(ourBin + "/sh").exists();

        String toolsBin = toolsDir + "/bin";
        String toolsUsrBin = toolsDir + "/usr/bin";
        String appBin = homeDir + "/bin";
        String systemPath = System.getenv("PATH");

        StringBuilder pathBuilder = new StringBuilder();
        if (hasOurPrefix) pathBuilder.append(ourBin).append(":");
        pathBuilder.append(appBin).append(":");
        pathBuilder.append(toolsBin).append(":");
        pathBuilder.append(toolsUsrBin).append(":");
        if (hasTermux) pathBuilder.append(termuxBin).append(":");
        pathBuilder.append(systemPath);

        String prefix = hasOurPrefix ? prefixDir : (hasTermux ? termuxPrefix : toolsDir + "/usr");
        String home = hasOurPrefix ? homeDir + "/home" : homeDir;

        java.util.List<String> envList = new java.util.ArrayList<>();
        envList.add("HOME=" + home);
        envList.add("PATH=" + pathBuilder.toString());
        envList.add("PREFIX=" + prefix);
        envList.add("TMPDIR=" + (hasOurPrefix ? prefixDir + "/tmp" : homeDir + "/tmp"));
        envList.add("TERM=xterm-256color");
        envList.add("LANG=en_US.UTF-8");
        envList.add("ANDROID_HOME=" + toolsDir);
        envList.add("ANDROID_SDK_ROOT=" + toolsDir);
        envList.add("JAVA_HOME=" + toolsDir + "/java");
        envList.add("PROJECTS=" + projectsDir);
        if (hasOurPrefix) {
            envList.add("LD_LIBRARY_PATH=" + prefixDir + "/lib:" + toolsDir + "/lib");
            envList.add("BOOTSTRAP=zaichat");
            if (prootLoaderPath != null) {
                envList.add("PROOT_LOADER=" + prootLoaderPath);
            }
        } else {
            envList.add("LD_LIBRARY_PATH=" + toolsDir + "/lib");
        }
        if (hasTermux) {
            envList.add("TERMUX_VERSION=" + getTermuxVersion());
            envList.add("LD_LIBRARY_PATH=" + termuxPrefix + "/lib");
        }

        return envList.toArray(new String[0]);
    }

    private String getTermuxVersion() {
        try {
            File versionFile = new File("/data/data/com.termux/files/usr/share/doc/termux/VERSION");
            if (versionFile.exists()) {
                String version;
                try (java.io.BufferedReader reader = new java.io.BufferedReader(new java.io.FileReader(versionFile))) {
                    version = reader.readLine();
                }
                return version != null ? version : "unknown";
            }
        } catch (Exception e) {
            Log.w(TAG, "Unable to read Termux version", e);
        }
        return "installed";
    }

    private Map<String, String> toEnvMap(String[] envPairs) {
        Map<String, String> map = new HashMap<>();
        for (String pair : envPairs) {
            int idx = pair.indexOf('=');
            if (idx > 0) {
                map.put(pair.substring(0, idx), pair.substring(idx + 1));
            }
        }
        return map;
    }

    private void executeBlocking(PluginCall call, Process process, String processId, int timeout) {
        try {
            BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()));
            StringBuilder output = new StringBuilder();
            String line;
            while ((line = reader.readLine()) != null) {
                output.append(line).append("\n");
            }

            boolean finished = process.waitFor(timeout / 1000, java.util.concurrent.TimeUnit.SECONDS);
            int exitCode;
            if (finished) {
                exitCode = process.exitValue();
            } else {
                process.destroyForcibly();
                exitCode = -1;
            }

            activeProcesses.remove(processId);

            JSObject result = new JSObject();
            result.put("output", output.toString());
            result.put("exitCode", exitCode);
            result.put("pid", processId);
            if (exitCode == 0 && output.toString().contains("cd ")) {
                updateCwdFromOutput(output.toString());
            }
            call.resolve(result);
        } catch (Exception e) {
            activeProcesses.remove(processId);
            call.reject("Execution error: " + e.getMessage());
        }
    }

    private void streamOutput(PluginCall call, Process process, String processId, String cwd) {
        call.setKeepAlive(true);
        String streamId = "shell:" + processId;

        Thread readerThread = new Thread(() -> {
            try {
                BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()));
                char[] buffer = new char[4096];
                int read;
                while ((read = reader.read(buffer)) != -1) {
                    String chunk = new String(buffer, 0, read);
                    JSObject event = new JSObject();
                    event.put("data", chunk);
                    event.put("pid", processId);
                    notifyListeners(streamId, event);
                }

                int exitCode;
                try {
                    exitCode = process.waitFor();
                } catch (InterruptedException e) {
                    process.destroyForcibly();
                    exitCode = -1;
                }

                JSObject doneEvent = new JSObject();
                doneEvent.put("data", "\n[Process exited with code " + exitCode + "]\n");
                doneEvent.put("pid", processId);
                doneEvent.put("exitCode", exitCode);
                doneEvent.put("done", true);
                notifyListeners(streamId, doneEvent);

                activeProcesses.remove(processId);
            } catch (Exception e) {
                Log.e(TAG, "Stream error", e);
                JSObject errEvent = new JSObject();
                errEvent.put("data", "\n[Error: " + e.getMessage() + "]\n");
                errEvent.put("pid", processId);
                errEvent.put("done", true);
                errEvent.put("exitCode", -1);
                notifyListeners(streamId, errEvent);
                activeProcesses.remove(processId);
            }
        });
        readerThread.setDaemon(true);
        readerThread.start();

        JSObject result = new JSObject();
        result.put("pid", processId);
        result.put("streamId", streamId);
        call.resolve(result);
    }

    private void updateCwdFromOutput(String output) {
        for (String line : output.split("\n")) {
            line = line.trim();
            if (line.startsWith("cd ") && !line.contains("&&") && !line.contains(";")) {
                String target = line.substring(3).trim();
                File targetDir = target.startsWith("/") ? new File(target) : new File(currentCwd, target);
                if (targetDir.isDirectory()) {
                    currentCwd = targetDir.getAbsolutePath();
                }
            }
        }
    }

    private boolean deleteRecursive(File file) {
        if (file.isDirectory()) {
            File[] children = file.listFiles();
            if (children != null) {
                for (File child : children) {
                    deleteRecursive(child);
                }
            }
        }
        return file.delete();
    }
}
