package ai.z.chat;

import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.net.Uri;
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 android.system.Os;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;

@CapacitorPlugin(name = "Bootstrap")
public class BootstrapPlugin extends Plugin {
    private static final String TAG = "BootstrapPlugin";

    private static final String BOOTSTRAP_URL_AARCH64 =
        "https://github.com/termux/termux-packages/releases/download/bootstrap-2026.02.12-r1%2Bapt.android-7/bootstrap-aarch64.zip";
    private static final String BOOTSTRAP_URL_ARM =
        "https://github.com/termux/termux-packages/releases/download/bootstrap-2026.02.12-r1%2Bapt.android-7/bootstrap-arm.zip";
    private static final String BOOTSTRAP_URL_X86_64 =
        "https://github.com/termux/termux-packages/releases/download/bootstrap-2026.02.12-r1%2Bapt.android-7/bootstrap-x86_64.zip";
    private static final String BOOTSTRAP_URL_X86 =
        "https://github.com/termux/termux-packages/releases/download/bootstrap-2026.02.12-r1%2Bapt.android-7/bootstrap-i686.zip";

    private static final String BOOTSTRAP_MIRROR_AARCH64 =
        "https://mirror.termux.dev/termux/termux-packages/bootstrap-2026.02.12-r1%2Bapt.android-7/bootstrap-aarch64.zip";
    private static final String BOOTSTRAP_MIRROR_ARM =
        "https://mirror.termux.dev/termux/termux-packages/bootstrap-2026.02.12-r1%2Bapt.android-7/bootstrap-arm.zip";
    private static final String BOOTSTRAP_MIRROR_X86_64 =
        "https://mirror.termux.dev/termux/termux-packages/bootstrap-2026.02.12-r1%2Bapt.android-7/bootstrap-x86_64.zip";
    private static final String BOOTSTRAP_MIRROR_X86 =
        "https://mirror.termux.dev/termux/termux-packages/bootstrap-2026.02.12-r1%2Bapt.android-7/bootstrap-i686.zip";

    private static final String TERMUX_PREFIX = "/data/data/com.termux/files/usr";
    private static final int BUFFER_SIZE = 8192;

    private String filesDir;
    private String prefixDir;
    private String stagingDir;
    private String homeDir;
    private String binDir;
    private volatile boolean isInstalling = false;

    @Override
    public void load() {
        filesDir = getContext().getFilesDir().getAbsolutePath();
        prefixDir = filesDir + "/usr";
        stagingDir = filesDir + "/usr-staging";
        homeDir = filesDir + "/home";
        binDir = prefixDir + "/bin";
    }

    @PluginMethod
    public void getStatus(PluginCall call) {
        JSObject status = new JSObject();
        boolean installed = new File(binDir + "/bash").exists() || new File(binDir + "/sh").exists();
        status.put("installed", installed);
        status.put("prefixDir", prefixDir);
        status.put("binDir", binDir);
        status.put("homeDir", homeDir);
        status.put("arch", getArch());
        status.put("isInstalling", isInstalling);
        if (installed) {
            status.put("shellPath", new File(binDir + "/bash").exists() ? binDir + "/bash" : binDir + "/sh");
        }
        call.resolve(status);
    }

    @PluginMethod
    public void install(PluginCall call) {
        if (isInstalling) {
            call.reject("Installation already in progress");
            return;
        }

        boolean installed = new File(binDir + "/bash").exists();
        if (installed) {
            call.resolve(new JSObject().put("installed", true).put("message", "Already installed"));
            return;
        }

        call.setKeepAlive(true);
        isInstalling = true;

        new Thread(() -> {
            try {
                doInstall(call);
            } catch (Exception e) {
                Log.e(TAG, "Install failed", e);
                isInstalling = false;
                try {
                    call.reject("Install failed: " + e.getMessage());
                } catch (Exception ignored) {}
            }
        }).start();
    }

    private void doInstall(PluginCall call) throws Exception {
        String arch = getArch();
        String[] urls = getBootstrapUrls(arch);

        sendProgress(call, "Downloading bootstrap for " + arch + "...", 0);

        File zipFile = new File(getContext().getCacheDir(), "bootstrap.zip");
        Exception lastError = null;
        for (String url : urls) {
            try {
                downloadFile(url, zipFile, (downloaded, total) -> {
                    int percent = total > 0 ? (int)(downloaded * 100 / total) : 0;
                    String sizeMB = String.format("%.1f", downloaded / (1024.0 * 1024.0));
                    sendProgress(call, "Downloading... " + sizeMB + " MB (" + percent + "%)", percent / 3);
                });
                lastError = null;
                break;
            } catch (Exception e) {
                lastError = e;
                Log.w(TAG, "Download from " + url + " failed, trying next mirror...");
                sendProgress(call, "Retrying alternate source...", 5);
            }
        }
        if (lastError != null) throw lastError;

        sendProgress(call, "Extracting bootstrap...", 35);

        new File(stagingDir).mkdirs();
        List<String[]> symlinks = extractBootstrap(zipFile, stagingDir, (extracted, total) -> {
            int percent = 35 + (int)(extracted * 30 / Math.max(total, 1));
            sendProgress(call, "Extracting... " + extracted + "/" + total + " files", percent);
        });

        sendProgress(call, "Creating symlinks (" + symlinks.size() + ")...", 68);
        createSymlinks(symlinks, stagingDir);

        sendProgress(call, "Patching paths...", 75);
        patchPaths(stagingDir);

        sendProgress(call, "Setting permissions...", 85);
        chmodRecursive(new File(stagingDir, "bin"));
        chmodRecursive(new File(stagingDir, "libexec"));
        chmodRecursive(new File(stagingDir, "lib"));

        new File(homeDir).mkdirs();
        new File(prefixDir + "/tmp").mkdirs();

        sendProgress(call, "Finalizing...", 92);

        File staging = new File(stagingDir);
        File prefix = new File(prefixDir);
        if (prefix.exists()) {
            deleteRecursive(prefix);
        }
        boolean renamed = staging.renameTo(prefix);
        if (!renamed) {
            throw new RuntimeException("Failed to rename staging to prefix");
        }

        chmodRecursive(new File(prefixDir + "/bin"));
        chmodRecursive(new File(prefixDir + "/libexec"));
        chmodRecursive(new File(prefixDir + "/lib"));

        writeEnvFile();
        writeProfileFile();

        zipFile.delete();

        sendProgress(call, "Termux environment ready!", 100);
        isInstalling = false;

        JSObject result = new JSObject();
        result.put("installed", true);
        result.put("prefixDir", prefixDir);
        result.put("shellPath", binDir + "/bash");
        result.put("message", "Bootstrap installed successfully");
        call.resolve(result);
    }

    private void downloadFile(String urlStr, File outputFile, ProgressCallback callback) throws Exception {
        URL url = new URL(urlStr);
        HttpURLConnection conn = (HttpURLConnection) url.openConnection();
        conn.setConnectTimeout(30000);
        conn.setReadTimeout(60000);
        conn.setRequestProperty("Accept-Language", "en-US,en");
        conn.connect();

        int total = conn.getContentLength();
        try (BufferedInputStream in = new BufferedInputStream(conn.getInputStream());
             FileOutputStream out = new FileOutputStream(outputFile)) {
            byte[] buffer = new byte[BUFFER_SIZE];
            long downloaded = 0;
            int read;
            long lastNotify = 0;

            while ((read = in.read(buffer)) != -1) {
                out.write(buffer, 0, read);
                downloaded += read;
                long now = System.currentTimeMillis();
                if (callback != null && now - lastNotify > 500) {
                    callback.onProgress(downloaded, total);
                    lastNotify = now;
                }
            }
            out.flush();
        } finally {
            conn.disconnect();
        }
    }

    private List<String[]> extractBootstrap(File zipFile, String destDir, ExtractCallback callback) throws Exception {
        List<String[]> symlinks = new ArrayList<>();
        ZipInputStream zis;
        ZipEntry entry;
        int extracted = 0;
        int total = 0;

        java.util.zip.ZipFile zf = new java.util.zip.ZipFile(zipFile);
        total = zf.size();
        zf.close();

        zis = new ZipInputStream(new FileInputStream(zipFile));
        while ((entry = zis.getNextEntry()) != null) {
            String name = entry.getName();
            if (name.equals("SYMLINKS.txt")) {
                BufferedReader reader = new BufferedReader(new InputStreamReader(zis));
                String line;
                while ((line = reader.readLine()) != null) {
                    String[] parts = line.split("\u2190");
                    if (parts.length == 2) {
                        String target = parts[0].replace(TERMUX_PREFIX, destDir);
                        String linkPath = destDir + "/" + parts[1];
                        symlinks.add(new String[]{target, linkPath});
                    }
                }
            } else {
                File outFile = new File(destDir, name);
                if (entry.isDirectory()) {
                    outFile.mkdirs();
                } else {
                    File parent = outFile.getParentFile();
                    if (parent != null) parent.mkdirs();
                    try (FileOutputStream fos = new FileOutputStream(outFile)) {
                        byte[] buf = new byte[BUFFER_SIZE];
                        int len;
                        while ((len = zis.read(buf)) > 0) {
                            fos.write(buf, 0, len);
                        }
                    }
                }
            }
            extracted++;
            if (callback != null && extracted % 50 == 0) {
                callback.onProgress(extracted, total);
            }
            zis.closeEntry();
        }
        zis.close();
        return symlinks;
    }

    private void createSymlinks(List<String[]> symlinks, String stagingDir) {
        for (String[] link : symlinks) {
            try {
                String target = link[0];
                String linkPath = link[1];
                File linkFile = new File(linkPath);
                File parent = linkFile.getParentFile();
                if (parent != null && !parent.exists()) parent.mkdirs();
                if (linkFile.exists()) linkFile.delete();
                android.system.Os.symlink(target, linkPath);
            } catch (Exception e) {
                Log.w(TAG, "Symlink failed: " + link[1] + " -> " + link[0] + ": " + e.getMessage());
            }
        }
    }

    private void patchPaths(String dir) {
        try {
            String ourPrefix = filesDir + "/usr";
            String sedExpr = "s|/data/data/com.termux/files/usr|" + ourPrefix + "|g;" +
                "s|/data/data/com.termux/files/home|" + homeDir + "|g;" +
                "s|/data/data/com.termux|" + filesDir + "|g";

            ProcessBuilder findPb = new ProcessBuilder("find", dir, "-type", "f",
                "-not", "-name", "*.so", "-not", "-name", "*.elf",
                "-not", "-name", "*.pyc", "-not", "-name", "*.a", "-size", "-512k");
            findPb.redirectErrorStream(true);
            Process p = findPb.start();
            BufferedReader reader = new BufferedReader(new InputStreamReader(p.getInputStream()));
            List<String> files = new ArrayList<>();
            String line;
            while ((line = reader.readLine()) != null) {
                files.add(line);
            }
            p.waitFor();

            for (String filePath : files) {
                try {
                    ProcessBuilder sedPb = new ProcessBuilder("sed", "-i", sedExpr, filePath);
                    sedPb.start().waitFor();
                } catch (Exception e) {
                    Log.w(TAG, "Patch failed for " + filePath + ": " + e.getMessage());
                }
            }

            patchShebangs(dir + "/bin");
            if (new File(dir + "/libexec").exists()) {
                patchShebangs(dir + "/libexec");
            }

            ProcessBuilder ldSo = new ProcessBuilder("sed", "-i",
                "s|/data/data/com.termux/files/usr|" + ourPrefix + "|g",
                dir + "/etc/ld.so.conf");
            ldSo.start().waitFor();
        } catch (Exception e) {
            Log.w(TAG, "Path patching error: " + e.getMessage());
        }
    }

    private void patchShebangs(String dirPath) {
        File dir = new File(dirPath);
        if (!dir.exists() || !dir.isDirectory()) return;
        File[] files = dir.listFiles();
        if (files == null) return;
        for (File f : files) {
            if (f.isDirectory()) continue;
            try {
                byte[] header = new byte[256];
                java.io.FileInputStream fis = new java.io.FileInputStream(f);
                int read = fis.read(header);
                fis.close();
                if (read < 2 || header[0] != '#' || header[1] != '!') continue;

                String shebangLine = new String(header, 0, Math.min(read, 256)).split("\n")[0];
                String newShebang = shebangLine.replaceAll("/data/data/com\\.termux/files/usr/bin/(sh|bash)", "/system/bin/sh")
                    .replaceAll("/data/user/0/ai\\.z\\.chat/files/usr/bin/(sh|bash)", "/system/bin/sh");
                if (!newShebang.equals(shebangLine)) {
                    ProcessBuilder sedPb = new ProcessBuilder("sed", "-i", "1s|" + shebangLine + "|" + newShebang + "|", f.getAbsolutePath());
                    sedPb.start().waitFor();
                    Log.i(TAG, "Patched shebang: " + f.getName() + " → " + newShebang);
                }
            } catch (Exception e) {
                Log.w(TAG, "Shebang patch failed for " + f.getName() + ": " + e.getMessage());
            }
        }
    }

    private void chmodRecursive(File dir) {
        if (!dir.exists() || !dir.isDirectory()) return;
        File[] children = dir.listFiles();
        if (children == null) return;
        for (File f : children) {
            try {
                if (f.isDirectory()) {
                    Os.chmod(f.getAbsolutePath(), 0755);
                    chmodRecursive(f);
                } else {
                    Os.chmod(f.getAbsolutePath(), 0755);
                }
            } catch (Exception e) {
                Log.w(TAG, "chmod failed: " + f.getAbsolutePath() + ": " + e.getMessage());
            }
        }
    }

    @PluginMethod
    public void fixPermissions(PluginCall call) {
        try {
            chmodRecursive(new File(prefixDir + "/bin"));
            chmodRecursive(new File(prefixDir + "/libexec"));
            chmodRecursive(new File(prefixDir + "/lib"));
            File etcDir = new File(prefixDir + "/etc");
            if (etcDir.exists()) chmodRecursive(etcDir);
            patchShebangs(prefixDir + "/bin");
            call.resolve(new JSObject().put("fixed", true));
        } catch (Exception e) {
            call.reject("Fix permissions failed: " + e.getMessage());
        }
    }

    @PluginMethod
    public void isTermuxInstalled(PluginCall call) {
        try {
            getContext().getPackageManager().getPackageInfo("com.termux", 0);
            call.resolve(new JSObject().put("installed", true).put("prefix", "/data/data/com.termux/files/usr"));
        } catch (PackageManager.NameNotFoundException e) {
            call.resolve(new JSObject().put("installed", false).put("prefix", ""));
        } catch (Exception e) {
            call.resolve(new JSObject().put("installed", false).put("error", e.getMessage()));
        }
    }

    @PluginMethod
    public void openTermuxPage(PluginCall call) {
        try {
            Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse("https://f-droid.org/en/packages/com.termux/"));
            getContext().startActivity(intent);
            call.resolve(new JSObject().put("opened", true));
        } catch (Exception e) {
            call.reject("Failed to open: " + e.getMessage());
        }
    }

    @PluginMethod
    public void runInTermux(PluginCall call) {
        String command = call.getString("command", "");
        if (command.isEmpty()) {
            call.reject("No command");
            return;
        }
        try {
            String[] cmd = new String[]{
                "/system/bin/am", "broadcast",
                "--user", "0",
                "-n", "com.termux/com.termux.app.TermuxOpenReceiver",
                "-a", "com.termux.RUN_COMMAND",
                "--es", "com.termux.RUN_COMMAND_PATH", "/data/data/com.termux/files/usr/bin/bash",
                "--es", "com.termux.RUN_COMMAND_ARGUMENTS", "-c " + command,
                "--ez", "com.termux.RUN_COMMAND_BACKGROUND", "false",
                "--es", "com.termux.RUN_COMMAND_WORKDIR", "/data/data/com.termux/files/home"
            };
            ProcessBuilder pb = new ProcessBuilder(cmd);
            pb.redirectErrorStream(true);
            Process p = pb.start();
            BufferedReader reader = new BufferedReader(new InputStreamReader(p.getInputStream()));
            StringBuilder output = new StringBuilder();
            String line;
            while ((line = reader.readLine()) != null) output.append(line).append("\n");
            p.waitFor(10, java.util.concurrent.TimeUnit.SECONDS);
            call.resolve(new JSObject().put("sent", true).put("output", output.toString()));
        } catch (Exception e) {
            call.reject("Termux command failed: " + e.getMessage());
        }
    }

    @PluginMethod
    public void downloadTermuxApk(PluginCall call) {
        call.setKeepAlive(true);
        new Thread(() -> {
            try {
                String packagesContent = downloadToString("https://f-droid.org/repo/index-v1.jar");
                String apkUrl = null;
                String[] lines = packagesContent.split("\n");
                for (String l : lines) {
                    if (l.contains("com.termux") && l.endsWith(".apk")) {
                        apkUrl = l.trim();
                        break;
                    }
                }

                if (apkUrl == null) {
                    apkUrl = "https://f-droid.org/repo/com.termux_1020.apk";
                }

                File apkFile = new File(getContext().getCacheDir(), "termux.apk");
                downloadFile(apkUrl, apkFile, null);

                call.resolve(new JSObject().put("path", apkFile.getAbsolutePath())
                    .put("size", apkFile.length()));
            } catch (Exception e) {
                try { call.reject("Download failed: " + e.getMessage()); } catch (Exception ignored) {}
            }
         }).start();
    }

    @PluginMethod
    public void extractAsset(PluginCall call) {
        String src = call.getString("src", "");
        String dest = call.getString("dest", "");
        if (src.isEmpty() || dest.isEmpty()) {
            call.reject("src and dest required");
            return;
        }
        try {
            java.io.InputStream is = getContext().getAssets().open(src);
            new File(dest).getParentFile().mkdirs();
            java.io.FileOutputStream fos = new java.io.FileOutputStream(dest);
            byte[] buf = new byte[8192];
            int r;
            long total = 0;
            while ((r = is.read(buf)) > 0) {
                fos.write(buf, 0, r);
                total += r;
            }
            fos.close();
            is.close();
            call.resolve(new JSObject().put("path", dest).put("size", total));
        } catch (Exception e) {
            call.reject("Extract failed: " + e.getMessage());
        }
    }

    @PluginMethod
    public void downloadFile(PluginCall call) {
        String url = call.getString("url", "");
        String dest = call.getString("dest", "");
        if (url.isEmpty() || dest.isEmpty()) {
            call.reject("url and dest required");
            return;
        }
        call.setKeepAlive(true);
        new Thread(() -> {
            try {
                new File(dest).getParentFile().mkdirs();
                downloadFile(url, new File(dest), null);
                call.resolve(new JSObject().put("path", dest));
            } catch (Exception e) {
                try { call.reject("Download failed: " + e.getMessage()); } catch (Exception ignored) {}
            }
        }).start();
    }

    private void writeEnvFile() {
        try {
            File envFile = new File(prefixDir + "/etc/termux.env");
            envFile.getParentFile().mkdirs();
            java.io.FileWriter writer = new java.io.FileWriter(envFile);
            writer.write("HOME=" + homeDir + "\n");
            writer.write("PREFIX=" + prefixDir + "\n");
            writer.write("PATH=" + binDir + "\n");
            writer.write("TMPDIR=" + prefixDir + "/tmp\n");
            writer.write("TERM=xterm-256color\n");
            writer.write("LANG=en_US.UTF-8\n");
            writer.write("BOOTSTRAP=zaichat\n");
            writer.close();
        } catch (Exception e) {
            Log.w(TAG, "Failed to write env file: " + e.getMessage());
        }
    }

    private void writeProfileFile() {
        try {
            File profileDir = new File(prefixDir + "/etc/profile.d");
            profileDir.mkdirs();
            File profile = new File(profileDir, "zai-chat.sh");
            java.io.FileWriter writer = new java.io.FileWriter(profile);
            writer.write("export HOME=" + homeDir + "\n");
            writer.write("export PREFIX=" + prefixDir + "\n");
            writer.write("export PATH=" + binDir + "\n");
            writer.write("export TMPDIR=" + prefixDir + "/tmp\n");
            writer.write("export TERM=xterm-256color\n");
            writer.write("export LANG=en_US.UTF-8\n");
            writer.write("export ANDROID_HOME=" + filesDir + "/tools\n");
            writer.write("export PROJECTS=" + filesDir + "/projects\n");
            writer.write("export PS1='\\$ '\n");
            writer.close();
            profile.setExecutable(true, false);
        } catch (Exception e) {
            Log.w(TAG, "Failed to write profile: " + e.getMessage());
        }
    }

    private void sendProgress(PluginCall call, String message, int percent) {
        JSObject event = new JSObject();
        event.put("message", message);
        event.put("percent", percent);
        notifyListeners("bootstrap-progress", event);
    }

    @PluginMethod
    public void installHermes(PluginCall call) {
        call.setKeepAlive(true);
        new Thread(() -> {
            try {
                String prefix = call.getString("prefix", prefixDir);
                String home = call.getString("home", homeDir + "/home");
                String hermesDir = home + "/.hermes";
                String venvDir = home + "/hermes-venv";

                new File(hermesDir).mkdirs();

                String hermesLink = venvDir + "/bin/hermes";
                if (new File(hermesLink).exists()) {
                    call.resolve(new JSObject().put("installed", true).put("path", hermesLink).put("venv", venvDir));
                    return;
                }

                JSObject setup = setupVirtualEnvInternal(prefix, home, venvDir);
                String pipBin = setup.getString("pip");
                if (pipBin == null || !new File(pipBin).exists()) {
                    call.reject("Failed to prepare internal virtual environment");
                    return;
                }

                String pipOutput = runBootstrapCommand(prefix, home,
                    "\"" + pipBin + "\" install --upgrade pip setuptools wheel && " +
                    "\"" + pipBin + "\" install --prefer-binary hermes-agent");

                if (!new File(hermesLink).exists()) {
                    call.reject("hermes-agent installation failed: " + pipOutput.substring(0, Math.min(800, pipOutput.length())));
                    return;
                }

                Log.i(TAG, "Hermes agent installed at " + venvDir + "/bin/hermes");
                call.resolve(new JSObject()
                    .put("installed", true)
                    .put("path", venvDir + "/bin/hermes")
                    .put("venv", venvDir)
                    .put("dir", hermesDir));
            } catch (Exception e) {
                Log.e(TAG, "installHermes failed", e);
                try { call.reject("installHermes failed: " + e.getMessage()); } catch (Exception ignored) {}
            }
        }).start();
    }

    @PluginMethod
    public void hermesExec(PluginCall call) {
        call.setKeepAlive(true);
        new Thread(() -> {
            try {
                String command = call.getString("command", "");
                String home = call.getString("home", homeDir + "/home");
                String venvDir = call.getString("venv", home + "/hermes-venv");

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

                String hermesBin = venvDir + "/bin/hermes";
                if (!new File(hermesBin).exists()) {
                    call.reject("Hermes not installed");
                    return;
                }

                String fullCmd = hermesBin + " " + command;
                ProcessBuilder pb = new ProcessBuilder("/system/bin/sh", "-c", fullCmd);
                pb.environment().put("HOME", home);
                pb.environment().put("PATH", venvDir + "/bin:" + prefixDir + "/bin:/system/bin");
                pb.environment().put("ANDROID_API_LEVEL", String.valueOf(android.os.Build.VERSION.SDK_INT));
                pb.environment().put("TERMUX_HOME", home);
                pb.redirectErrorStream(true);
                Process p = pb.start();
                String output = drainProcess(p);
                int exitCode = p.waitFor();

                call.resolve(new JSObject()
                    .put("output", output)
                    .put("exitCode", exitCode)
                    .put("ok", exitCode == 0));
            } catch (Exception e) {
                try { call.reject("hermesExec failed: " + e.getMessage()); } catch (Exception ignored) {}
            }
        }).start();
    }

    @PluginMethod
    public void setupVirtualEnv(PluginCall call) {
        call.setKeepAlive(true);
        new Thread(() -> {
            try {
                String prefix = call.getString("prefix", prefixDir);
                String home = call.getString("home", homeDir + "/home");
                String venvDir = call.getString("venv", filesDir + "/venv/default");

                call.resolve(setupVirtualEnvInternal(prefix, home, venvDir));
            } catch (Exception e) {
                try { call.reject("setupVirtualEnv failed: " + e.getMessage()); } catch (Exception ignored) {}
            }
        }).start();
    }

    @PluginMethod
    public void venvPipInstall(PluginCall call) {
        call.setKeepAlive(true);
        new Thread(() -> {
            try {
                String prefix = call.getString("prefix", prefixDir);
                String home = call.getString("home", homeDir + "/home");
                String venvDir = call.getString("venv", filesDir + "/venv/default");
                String packages = call.getString("packages", "");
                if (packages.trim().isEmpty()) {
                    call.reject("packages required");
                    return;
                }

                String pip = venvDir + "/bin/pip";
                if (!new File(pip).exists()) {
                    call.reject("venv pip not found. Run setupVirtualEnv first.");
                    return;
                }

                String output = runBootstrapCommand(prefix, home,
                    "\"" + pip + "\" install --prefer-binary " + packages);
                call.resolve(new JSObject().put("ok", true).put("output", output));
            } catch (Exception e) {
                try { call.reject("venvPipInstall failed: " + e.getMessage()); } catch (Exception ignored) {}
            }
        }).start();
    }

    private JSObject setupVirtualEnvInternal(String prefix, String home, String venvDir) throws Exception {
        new File(venvDir).getParentFile().mkdirs();
        String pkgBin = prefix + "/bin/pkg";
        String aptBin = prefix + "/bin/apt";
        String python3 = prefix + "/bin/python3";

        if (!new File(pkgBin).exists() && !new File(aptBin).exists()) {
            throw new RuntimeException("Bootstrap tools missing. Install internal dev environment first.");
        }

        if (!new File(python3).exists()) {
            String installer = new File(pkgBin).exists() ? pkgBin : aptBin;
            runBootstrapCommand(prefix, home,
                "sh \"" + installer + "\" install -y python clang rust make pkg-config libffi openssl");
        }

        if (!new File(python3).exists()) {
            throw new RuntimeException("python3 unavailable after install");
        }

        String venvPython = venvDir + "/bin/python";
        String venvPip = venvDir + "/bin/pip";
        if (!new File(venvPip).exists()) {
            runBootstrapCommand(prefix, home,
                "\"" + python3 + "\" -m venv \"" + venvDir + "\"");
        }
        if (!new File(venvPip).exists()) {
            throw new RuntimeException("Failed to create Python venv");
        }

        runBootstrapCommand(prefix, home,
            "\"" + venvPip + "\" install --upgrade pip setuptools wheel");

        return new JSObject()
            .put("ok", true)
            .put("venv", venvDir)
            .put("python", venvPython)
            .put("pip", venvPip);
    }

    private String runBootstrapCommand(String prefix, String home, String command) throws Exception {
        ProcessBuilder pb = new ProcessBuilder("/system/bin/sh", "-c",
            "export PREFIX=\"" + prefix + "\" HOME=\"" + home + "\" " +
            "LD_LIBRARY_PATH=\"" + prefix + "/lib\" PATH=\"" + prefix + "/bin:/system/bin:$PATH\" " +
            "ANDROID_API_LEVEL=\"" + android.os.Build.VERSION.SDK_INT + "\" && " + command);
        pb.redirectErrorStream(true);
        Process p = pb.start();
        String out = drainProcess(p);
        int code = p.waitFor();
        if (code != 0) throw new RuntimeException("cmd failed(" + code + "): " + out);
        return out;
    }

    private String drainProcess(Process p) throws Exception {
        java.io.InputStream is = p.getInputStream();
        java.io.ByteArrayOutputStream baos = new java.io.ByteArrayOutputStream();
        byte[] buf = new byte[8192];
        int r;
        while ((r = is.read(buf)) > 0) baos.write(buf, 0, r);
        return baos.toString("UTF-8");
    }

    private String getArch() {
        String abi = android.os.Build.SUPPORTED_ABIS[0];
        switch (abi) {
            case "arm64-v8a": return "aarch64";
            case "armeabi-v7a": return "arm";
            case "x86_64": return "x86_64";
            case "x86": return "i686";
            default: return "aarch64";
        }
    }

    private String[] getBootstrapUrls(String arch) {
        switch (arch) {
            case "aarch64": return new String[]{BOOTSTRAP_URL_AARCH64, BOOTSTRAP_MIRROR_AARCH64};
            case "arm": return new String[]{BOOTSTRAP_URL_ARM, BOOTSTRAP_MIRROR_ARM};
            case "x86_64": return new String[]{BOOTSTRAP_URL_X86_64, BOOTSTRAP_MIRROR_X86_64};
            case "i686": return new String[]{BOOTSTRAP_URL_X86, BOOTSTRAP_MIRROR_X86};
            default: return new String[]{BOOTSTRAP_URL_AARCH64, BOOTSTRAP_MIRROR_AARCH64};
        }
    }

    private String getMirrorUrl(String primaryUrl) {
        return primaryUrl.replace("https://github.com/termux/", "https://mirror.termux.dev/termux/");
    }

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

    @PluginMethod
    public void installProot(PluginCall call) {
        call.setKeepAlive(true);
        new Thread(() -> {
            try {
                String arch = getArch();
                String prootBinPath = binDir + "/proot";
                File prootBin = new File(prootBinPath);

                if (prootBin.exists()) {
                    try { Os.chmod(prootBinPath, 0755); } catch (Exception e) {}
                    call.resolve(new JSObject().put("installed", true).put("path", prootBinPath));
                    return;
                }

                String nativeLibDir = null;
                try {
                    nativeLibDir = getContext().getApplicationInfo().nativeLibraryDir;
                    File nativeProot = new File(nativeLibDir, "libproot.so");
                    if (nativeProot.exists()) {
                        call.resolve(new JSObject().put("installed", true).put("path", nativeProot.getAbsolutePath()).put("bundled", true));
                        return;
                    }
                } catch (Exception e) {}

                String packagesUrl = "https://packages.termux.dev/apt/termux-main/dists/stable/main/binary-" + arch + "/Packages";
                Log.i(TAG, "Fetching package index: " + packagesUrl);

                String packagesContent = downloadToString(packagesUrl);
                String prootDebUrl = null;
                String prootDebName = null;

                String[] blocks = packagesContent.split("\n\n");
                for (String block : blocks) {
                    if (block.contains("Package: proot")) {
                        for (String line : block.split("\n")) {
                            if (line.startsWith("Filename:")) {
                                prootDebName = line.substring("Filename:".length()).trim();
                            }
                        }
                        if (prootDebName != null) break;
                    }
                }

                if (prootDebName == null) {
                    call.reject("proot package not found in repo");
                    return;
                }

                prootDebUrl = "https://packages.termux.dev/apt/termux-main/" + prootDebName;
                Log.i(TAG, "Downloading proot: " + prootDebUrl);

                File debFile = new File(getContext().getCacheDir(), "proot.deb");
                downloadFile(prootDebUrl, debFile, null);

                Log.i(TAG, "Extracting proot binary from .deb...");
                byte[] prootData = extractBinaryFromDeb(debFile, "proot");
                if (prootData == null) {
                    call.reject("Failed to extract proot binary from .deb");
                    debFile.delete();
                    return;
                }

                new File(binDir).mkdirs();
                FileOutputStream fos = new FileOutputStream(prootBin);
                fos.write(prootData);
                fos.close();

                Os.chmod(prootBinPath, 0755);
                debFile.delete();

                Log.i(TAG, "proot installed: " + prootBinPath + " (" + prootData.length + " bytes)");
                call.resolve(new JSObject().put("installed", true).put("path", prootBinPath).put("size", prootData.length));
            } catch (Exception e) {
                Log.e(TAG, "installProot failed", e);
                try { call.reject("installProot failed: " + e.getMessage()); } catch (Exception ignored) {}
            }
        }).start();
    }

    @PluginMethod
    public void installAapt2(PluginCall call) {
        call.setKeepAlive(true);
        new Thread(() -> {
            try {
                String arch = getArch();
                String toolsDir = call.getString("toolsDir", "");
                if (toolsDir.isEmpty()) {
                    call.reject("toolsDir required");
                    return;
                }

                File aapt2File = new File(toolsDir, "bin/aapt2");
                if (aapt2File.exists()) {
                    call.resolve(new JSObject().put("installed", true).put("path", aapt2File.getAbsolutePath()));
                    return;
                }

                String packagesUrl = "https://packages.termux.dev/apt/termux-main/dists/stable/main/binary-" + arch + "/Packages";
                String packagesContent = downloadToString(packagesUrl);

                String[] neededPkgs = {"aapt2", "aapt"};
                java.util.Map<String, String> debUrls = new java.util.LinkedHashMap<>();
                String[] blocks = packagesContent.split("\n\n");
                for (String block : blocks) {
                    for (String pkgName : neededPkgs) {
                        if (block.contains("Package: " + pkgName + "\n") && !debUrls.containsKey(pkgName)) {
                            String filename = null;
                            for (String line : block.split("\n")) {
                                if (line.startsWith("Filename:")) {
                                    filename = line.substring("Filename:".length()).trim();
                                }
                            }
                            if (filename != null) {
                                debUrls.put(pkgName, "https://packages.termux.dev/apt/termux-main/" + filename);
                            }
                            break;
                        }
                    }
                    if (debUrls.size() >= neededPkgs.length) break;
                }

                if (debUrls.isEmpty()) {
                    call.reject("aapt2 package not found in repo");
                    return;
                }

                new File(toolsDir + "/bin").mkdirs();
                new File(toolsDir + "/lib").mkdirs();
                new File(toolsDir + "/share/aapt2").mkdirs();

                for (java.util.Map.Entry<String, String> entry : debUrls.entrySet()) {
                    String pkgName = entry.getKey();
                    String debUrl = entry.getValue();
                    Log.i(TAG, "Downloading " + pkgName + ": " + debUrl);

                    File debFile = new File(getContext().getCacheDir(), pkgName + ".deb");
                    downloadFile(debUrl, debFile, null);

                    Log.i(TAG, "Extracting " + pkgName + " from .deb...");
                    extractDebToDir(debFile, new File(toolsDir));
                    debFile.delete();
                }

                if (aapt2File.exists()) {
                    try { Os.chmod(aapt2File.getAbsolutePath(), 0755); } catch (Exception e) {}
                    File aaptBin = new File(toolsDir, "bin/aapt");
                    if (aaptBin.exists()) {
                        try { Os.chmod(aaptBin.getAbsolutePath(), 0755); } catch (Exception e) {}
                    }
                    call.resolve(new JSObject().put("installed", true).put("path", aapt2File.getAbsolutePath()));
                } else {
                    call.reject("aapt2 binary not found after extraction");
                }
            } catch (Exception e) {
                Log.e(TAG, "installAapt2 failed", e);
                try { call.reject("installAapt2 failed: " + e.getMessage()); } catch (Exception ignored) {}
            }
        }).start();
    }

    private void extractDebToDir(File debFile, File destDir) throws Exception {
        FileInputStream fis = new FileInputStream(debFile);
        byte[] magic = new byte[8];
        if (fis.read(magic) != 8 || !new String(magic).equals("!<arch>\n")) {
            fis.close();
            throw new RuntimeException("Not a valid .deb (AR) file");
        }

        while (true) {
            byte[] header = new byte[60];
            int read = fis.read(header);
            if (read < 60) break;

            String name = new String(header, 0, 16).trim();
            String sizeStr = new String(header, 48, 10).trim();
            long entrySize = Long.parseLong(sizeStr);

            if (name.startsWith("data.tar")) {
                java.io.ByteArrayOutputStream baos = new java.io.ByteArrayOutputStream();
                byte[] buf = new byte[8192];
                long remaining = entrySize;
                while (remaining > 0) {
                    int toRead = (int) Math.min(buf.length, remaining);
                    int r = fis.read(buf, 0, toRead);
                    if (r <= 0) break;
                    baos.write(buf, 0, r);
                    remaining -= r;
                }
                fis.close();

                byte[] tarData = baos.toByteArray();
                if (name.contains(".xz")) {
                    tarData = decompressXz(tarData);
                } else if (name.contains(".gz")) {
                    tarData = decompressGz(tarData);
                } else if (name.contains(".zst")) {
                    throw new RuntimeException("ZSTD compression not supported");
                }
                extractTarToDir(tarData, destDir);
                return;
            } else {
                long skip = (entrySize + 1) & ~1L;
                fis.skip(skip);
            }
        }
        fis.close();
    }

    private void extractTarToDir(byte[] tarData, File destDir) throws Exception {
        String prefix = "data/data/com.termux/files/usr/";
        int offset = 0;
        while (offset + 512 <= tarData.length) {
            String headerName = new String(tarData, offset, 100).trim().replace("\0", "");
            String sizeStr = new String(tarData, offset + 124, 12).trim().replace("\0", "");
            long fileSize = 0;
            try { fileSize = Long.parseLong(sizeStr, 8); } catch (Exception e) { break; }

            String type = new String(tarData, offset + 156, 1);
            String relPath = headerName;
            if (relPath.startsWith(prefix)) {
                relPath = relPath.substring(prefix.length());
            } else if (relPath.startsWith("./")) {
                relPath = relPath.substring(2);
                if (relPath.startsWith(prefix)) relPath = relPath.substring(prefix.length());
            }

            if (!relPath.isEmpty() && fileSize > 0 && (type.equals("0") || type.isEmpty())) {
                File outFile = new File(destDir, relPath);
                outFile.getParentFile().mkdirs();
                FileOutputStream fos = new FileOutputStream(outFile);
                fos.write(tarData, offset + 512, (int) Math.min(fileSize, tarData.length - offset - 512));
                fos.close();

                if (relPath.startsWith("bin/") || relPath.contains("/bin/")) {
                    try { Os.chmod(outFile.getAbsolutePath(), 0755); } catch (Exception e) {}
                }
            }

            long blocks = (fileSize + 511) / 512;
            offset += 512 + (int)(blocks * 512);
        }
    }

    private String downloadToString(String urlStr) throws Exception {
        URL url = new URL(urlStr);
        HttpURLConnection conn = (HttpURLConnection) url.openConnection();
        conn.setConnectTimeout(15000);
        conn.setReadTimeout(30000);
        BufferedReader reader = new BufferedReader(new InputStreamReader(conn.getInputStream()));
        StringBuilder sb = new StringBuilder();
        String line;
        while ((line = reader.readLine()) != null) {
            sb.append(line).append("\n");
        }
        reader.close();
        conn.disconnect();
        return sb.toString();
    }

    private byte[] extractBinaryFromDeb(File debFile, String binaryName) throws Exception {
        FileInputStream fis = new FileInputStream(debFile);
        byte[] magic = new byte[8];
        if (fis.read(magic) != 8 || !new String(magic).equals("!<arch>\n")) {
            fis.close();
            throw new RuntimeException("Not a valid .deb (AR) file");
        }

        while (true) {
            byte[] header = new byte[60];
            int read = fis.read(header);
            if (read < 60) break;

            String name = new String(header, 0, 16).trim();
            String sizeStr = new String(header, 48, 10).trim();
            long entrySize = Long.parseLong(sizeStr);

            if (name.startsWith("data.tar")) {
                java.io.ByteArrayOutputStream baos = new java.io.ByteArrayOutputStream();
                byte[] buf = new byte[8192];
                long remaining = entrySize;
                while (remaining > 0) {
                    int toRead = (int) Math.min(buf.length, remaining);
                    int r = fis.read(buf, 0, toRead);
                    if (r <= 0) break;
                    baos.write(buf, 0, r);
                    remaining -= r;
                }
                fis.close();

                byte[] tarData = baos.toByteArray();
                if (name.contains(".xz")) {
                    tarData = decompressXz(tarData);
                } else if (name.contains(".gz")) {
                    tarData = decompressGz(tarData);
                } else if (name.contains(".zst")) {
                    throw new RuntimeException("ZSTD compression not supported");
                }
                return findBinaryInTar(tarData, binaryName);
            } else {
                long skip = (entrySize + 1) & ~1L;
                fis.skip(skip);
            }
        }
        fis.close();
        return null;
    }

    private byte[] decompressGz(byte[] data) throws Exception {
        java.util.zip.GZIPInputStream gzis = new java.util.zip.GZIPInputStream(
            new java.io.ByteArrayInputStream(data));
        java.io.ByteArrayOutputStream baos = new java.io.ByteArrayOutputStream();
        byte[] buf = new byte[8192];
        int r;
        while ((r = gzis.read(buf)) > 0) baos.write(buf, 0, r);
        gzis.close();
        baos.close();
        return baos.toByteArray();
    }

    private byte[] decompressXz(byte[] data) throws Exception {
        ProcessBuilder pb = new ProcessBuilder("xz", "-d", "--stdout", "-");
        pb.redirectErrorStream(true);
        Process p = pb.start();
        OutputStream pos = p.getOutputStream();
        pos.write(data);
        pos.close();
        java.io.ByteArrayOutputStream baos = new java.io.ByteArrayOutputStream();
        byte[] buf = new byte[8192];
        java.io.InputStream pis = p.getInputStream();
        int r;
        while ((r = pis.read(buf)) > 0) baos.write(buf, 0, r);
        pis.close();
        p.waitFor();
        return baos.toByteArray();
    }

    private byte[] findBinaryInTar(byte[] tarData, String binaryName) throws Exception {
        int offset = 0;
        while (offset + 512 <= tarData.length) {
            String headerName = new String(tarData, offset, 100).trim().replace("\0", "");
            String sizeStr = new String(tarData, offset + 124, 12).trim().replace("\0", "");
            long fileSize = 0;
            try { fileSize = Long.parseLong(sizeStr, 8); } catch (Exception e) { break; }

            if (headerName.endsWith("/bin/" + binaryName) || headerName.equals(binaryName) ||
                headerName.endsWith("/" + binaryName)) {
                byte[] result = new byte[(int) fileSize];
                System.arraycopy(tarData, offset + 512, result, 0, (int) Math.min(fileSize, tarData.length - offset - 512));
                return result;
            }

            long blocks = (fileSize + 511) / 512;
            offset += 512 + (int)(blocks * 512);
        }
        return null;
    }

    interface ProgressCallback {
        void onProgress(long downloaded, long total);
    }

    interface ExtractCallback {
        void onProgress(int extracted, int total);
    }
}
