package com.dragontcp.client;

import android.app.Notification;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.net.VpnService;
import android.os.Build;
import android.os.IBinder;
import android.os.ParcelFileDescriptor;

import java.io.BufferedReader;
import java.io.File;
import java.io.InputStreamReader;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;

import tech.xvanturing.freeproxy.data.model.DnsMode;
import tech.xvanturing.freeproxy.data.model.ProxyProfile;
import tech.xvanturing.freeproxy.data.model.ProxyType;
import tech.xvanturing.freeproxy.vpn.TunnelEngine;
import tech.xvanturing.freeproxy.vpn.proxy.SocketProtector;

public class DragonService extends VpnService {
    public static final String ACTION_CONNECT = "com.dragontcp.client.CONNECT";
    public static final String ACTION_STOP = "com.dragontcp.client.STOP";
    public static final String ACTION_STATE = "com.dragontcp.client.STATE";
    public static final String EXTRA_ACTIVE = "active";
    public static final String EXTRA_STATUS = "status";

    public static final String EXTRA_SERVER = "server";
    public static final String EXTRA_PORT = "port";
    public static final String EXTRA_TOKEN = "token";
    /** Wire format: "auto", "b" or "x". */
    public static final String EXTRA_WIRE = "wire";
    public static final String EXTRA_CHUNK_MAX = "chunkMax";
    public static final String EXTRA_CHUNK_MIN = "chunkMin";
    /** Maximum download records per request. A transport setting, not a thread count. */
    public static final String EXTRA_BATCH_MAX = "batchMax";
    /** Minimum download records per request; equal to the maximum pins the depth. */
    public static final String EXTRA_BATCH_MIN = "batchMin";
    public static final String EXTRA_RECONNECT = "reconnect";
    public static final String EXTRA_TIMEOUT = "timeout";

    private static final int BATCH_LIMIT = 256;

    private static final int NOTIFICATION_ID = 53;
    private static final String CHANNEL_ID = "dragontcp-lite";
    private static final int LOCAL_PROXY_PORT = 8080;
    private static final int TUN_MTU = 1400;

    private static volatile boolean appActive;
    private static volatile String appStatus = "DISCONNECTED";

    public static boolean isActive() { return appActive; }
    public static String currentStatus() { return appStatus; }

    private final Object stateLock = new Object();
    private volatile Process coreProcess;
    private volatile TunnelEngine tunnelEngine;
    private volatile ParcelFileDescriptor tunFd;
    private volatile boolean connected;
    private volatile boolean stopping;
    private volatile boolean cancelRequested;

    @Override
    public void onCreate() {
        super.onCreate();
        createNotificationChannel();
    }

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        String action = intent != null ? intent.getAction() : null;
        if (ACTION_STOP.equals(action)) {
            cancelRequested = true;
            publishState("STOPPING", true);
            new Thread(() -> stopEverything("Stopped"), "dragontcp-stop").start();
            return START_NOT_STICKY;
        }
        if (ACTION_CONNECT.equals(action)) {
            synchronized (stateLock) {
                // UI disabling is backed by a service-side guard too. A duplicate
                // CONNECT can no longer tear down and restart a healthy tunnel.
                if (appActive || connected || coreProcess != null || tunnelEngine != null) {
                    publishState(connected ? "CONNECTED" : "CONNECTING", true);
                    return START_STICKY;
                }
                cancelRequested = false;
                publishState("CONNECTING", true);
            }
            startForeground(NOTIFICATION_ID, buildNotification("Starting..."));
            Intent copy = new Intent(intent);
            new Thread(() -> startEverything(copy), "dragontcp-start").start();
            return START_STICKY;
        }
        return START_NOT_STICKY;
    }

    private void startEverything(Intent intent) {
        synchronized (stateLock) {
            stopping = false;
        }

        String server = intent.getStringExtra(EXTRA_SERVER);
        int port = intent.getIntExtra(EXTRA_PORT, 53);
        String token = intent.getStringExtra(EXTRA_TOKEN);
        String wire = intent.getStringExtra(EXTRA_WIRE);
        int chunkMax = intent.getIntExtra(EXTRA_CHUNK_MAX, 1024 * 1024);
        int chunkMin = intent.getIntExtra(EXTRA_CHUNK_MIN, 32);
        int batchMax = intent.getIntExtra(EXTRA_BATCH_MAX, 1);
        int batchMin = intent.getIntExtra(EXTRA_BATCH_MIN, 1);
        int reconnect = intent.getIntExtra(EXTRA_RECONNECT, 1);
        int timeout = intent.getIntExtra(EXTRA_TIMEOUT, 2);

        if (server == null || server.trim().isEmpty()) {
            failStart("Server is required");
            return;
        }
        server = server.trim();
        if (token == null) token = "";
        if (wire == null || !(wire.equals("b") || wire.equals("x"))) wire = "auto";
        chunkMax = Math.max(32, Math.min(1024 * 1024, chunkMax));
        chunkMin = Math.max(32, Math.min(chunkMax, chunkMin));
        batchMax = Math.max(1, Math.min(BATCH_LIMIT, batchMax));
        batchMin = Math.max(1, Math.min(batchMax, batchMin));
        reconnect = Math.max(0, reconnect);
        timeout = Math.max(1, timeout);

        try {
            AppLog.append("Starting DragonTCP → " + server + ":" + port);
            AppLog.append(describeBatch(batchMin, batchMax));
            AppLog.append(describeWire(wire));
            Process process = startDragonCore(server, port, token, chunkMax, chunkMin, batchMax, batchMin, wire, reconnect, timeout);
            synchronized (stateLock) { coreProcess = process; }

            startCoreLogReader(process);
            waitForLocalProxy(process);
            if (cancelRequested) throw new InterruptedException("Stop requested");
            AppLog.append("Local DragonTCP proxy ready on 127.0.0.1:8080");

            ParcelFileDescriptor pfd = establishVpn();
            if (pfd == null) throw new IllegalStateException("Android refused to establish the VPN interface");

            SocketProtector protector = new SocketProtector() {
                @Override public boolean protect(Socket socket) {
                    return DragonService.this.protect(socket);
                }
                @Override public boolean protect(DatagramSocket socket) {
                    return DragonService.this.protect(socket);
                }
            };

            ProxyProfile profile = new ProxyProfile(ProxyType.HTTP, DnsMode.PROXY, false);
            InetSocketAddress localProxy = new InetSocketAddress(
                    InetAddress.getByAddress(new byte[]{127, 0, 0, 1}),
                    LOCAL_PROXY_PORT
            );
            TunnelEngine engine = new TunnelEngine(
                    pfd,
                    profile,
                    localProxy,
                    TUN_MTU,
                    protector,
                    null,
                    null
            );

            synchronized (stateLock) {
                if (cancelRequested) {
                    try { pfd.close(); } catch (Throwable ignored) {}
                    throw new InterruptedException("Stop requested");
                }
                tunFd = pfd;
                tunnelEngine = engine;
                connected = true;
            }
            engine.start();
            publishState("CONNECTED", true);
            updateNotification("Connected");
            AppLog.append("VPN connected");
            AppLog.append("DNS: forced through 1.1.1.1 over DragonTCP");
            AppLog.append("IPv6: captured and blocked to prevent bypass");
        } catch (InterruptedException stopped) {
            Thread.currentThread().interrupt();
            stopEverything(null);
        } catch (Throwable t) {
            failStart(t.getMessage() != null ? t.getMessage() : t.toString());
        }
    }

    private Process startDragonCore(
            String server,
            int port,
            String token,
            int chunkMax,
            int chunkMin,
            int batchMax,
            int batchMin,
            String wire,
            int reconnect,
            int timeout
    ) throws Exception {
        File executable = new File(getApplicationInfo().nativeLibraryDir, "libdragontcp_client.so");
        if (!executable.exists()) throw new IllegalStateException("Embedded DragonTCP core is missing");

        List<String> cmd = new ArrayList<>();
        cmd.add(executable.getAbsolutePath());
        cmd.add("--listen-host"); cmd.add("127.0.0.1");
        cmd.add("--listen-port"); cmd.add(Integer.toString(LOCAL_PROXY_PORT));
        cmd.add("--server-host"); cmd.add(server);
        cmd.add("--server-port"); cmd.add(Integer.toString(port));
        if (!token.isEmpty()) { cmd.add("--token"); cmd.add(token); }
        cmd.add("--transport"); cmd.add("chunk");
        cmd.add("--chunk-start"); cmd.add(Integer.toString(chunkMax));
        cmd.add("--chunk-min"); cmd.add(Integer.toString(chunkMin));
        cmd.add("--chunk-max"); cmd.add(Integer.toString(chunkMax));
        cmd.add("--chunk-pollers"); cmd.add("1");
        cmd.add("--wire"); cmd.add(wire);
        cmd.add("--chunk-concurrency"); cmd.add(Integer.toString(batchMax));
        cmd.add("--chunk-concurrency-min"); cmd.add(Integer.toString(batchMin));
        cmd.add("--chunk-reconnect-every"); cmd.add(Integer.toString(reconnect));
        cmd.add("--chunk-timeout"); cmd.add(timeout + "s");
        cmd.add("--chunk-grow-after"); cmd.add("16");
        cmd.add("--chunk-adapt-log=true");

        ProcessBuilder pb = new ProcessBuilder(cmd);
        pb.redirectErrorStream(true);
        return pb.start();
    }

    /** Human-readable summary of the download batch configuration, for the log screen. */
    private static String describeBatch(int min, int max) {
        if (min == max) {
            return max == 1
                    ? "Download batch: fixed at 1 record per request"
                    : "Download batch: pinned at " + max + " records per request (never adapts)";
        }
        return "Download batch: adaptive " + min + "-" + max + " records per request";
    }


    /** Human-readable summary of the wire selection, for the log screen. */
    private static String describeWire(String wire) {
        if ("b".equals(wire)) return "Wire: B (manual)";
        if ("x".equals(wire)) return "Wire: X (manual)";
        return "Wire: auto (probing)";
    }

    private void startCoreLogReader(Process process) {
        Thread reader = new Thread(() -> {
            try (BufferedReader br = new BufferedReader(new InputStreamReader(process.getInputStream()))) {
                String line;
                while ((line = br.readLine()) != null) {
                    // Keep the UI useful: adaptation changes and real errors only.
                    String lower = line.toLowerCase();
                    if (line.startsWith("adaptive ") || line.startsWith("path probe:") || line.startsWith("wire") || lower.contains("error") || lower.contains("failed")) {
                        AppLog.append(line);
                    }
                }
            } catch (Throwable ignored) {
            }
        }, "dragontcp-core-log");
        reader.setDaemon(true);
        reader.start();

        Thread watcher = new Thread(() -> {
            try {
                int code = process.waitFor();
                boolean shouldStop;
                synchronized (stateLock) {
                    shouldStop = !stopping && coreProcess == process && connected;
                }
                if (shouldStop) {
                    AppLog.append("DragonTCP core exited: " + code);
                    stopEverything("Core stopped");
                }
            } catch (InterruptedException ignored) {
                Thread.currentThread().interrupt();
            }
        }, "dragontcp-core-watch");
        watcher.setDaemon(true);
        watcher.start();
    }

    private void waitForLocalProxy(Process process) throws Exception {
        long deadline = System.currentTimeMillis() + 10_000;
        Throwable last = null;
        while (System.currentTimeMillis() < deadline) {
            if (!process.isAlive()) throw new IllegalStateException("DragonTCP core exited before proxy startup");
            try (Socket socket = new Socket()) {
                socket.connect(new InetSocketAddress("127.0.0.1", LOCAL_PROXY_PORT), 150);
                return;
            } catch (Throwable t) {
                last = t;
                Thread.sleep(100);
            }
        }
        throw new IllegalStateException("Local proxy did not start" + (last != null ? ": " + last.getMessage() : ""));
    }

    private ParcelFileDescriptor establishVpn() throws Exception {
        Builder builder = new Builder()
                .setSession("DragonTCP Lite")
                .setMtu(TUN_MTU)
                .addAddress("10.77.0.2", 32)
                .addRoute("0.0.0.0", 0)
                .addDnsServer("1.1.1.1")
                // The embedded userspace adapter is intentionally IPv4-only.
                // Capturing ::/0 blocks IPv6 instead of leaking it outside the VPN.
                .addAddress("fd77:6472:6167:6f6e::2", 128)
                .addRoute("::", 0);

        try {
            builder.addDisallowedApplication(getPackageName());
        } catch (PackageManager.NameNotFoundException ignored) {
        }

        if (Build.VERSION.SDK_INT >= 29) {
            builder.setBlocking(true);
            builder.setMetered(false);
        }
        return builder.establish();
    }

    private void failStart(String message) {
        AppLog.append("CONNECT failed: " + message);
        stopEverything("Failed");
    }

    private void stopEverything(String logMessage) {
        synchronized (stateLock) {
            stopEverythingLocked(logMessage);
        }
    }

    private void cleanupComponentsLocked() {
        connected = false;

        TunnelEngine engine = tunnelEngine;
        tunnelEngine = null;
        if (engine != null) {
            try { engine.stop(); } catch (Throwable ignored) {}
        }

        ParcelFileDescriptor fd = tunFd;
        tunFd = null;
        if (fd != null) {
            try { fd.close(); } catch (Throwable ignored) {}
        }

        Process process = coreProcess;
        coreProcess = null;
        if (process != null) {
            try {
                process.destroy();
                if (!process.waitFor(1200, TimeUnit.MILLISECONDS)) {
                    process.destroyForcibly();
                    process.waitFor(800, TimeUnit.MILLISECONDS);
                }
            } catch (Throwable ignored) {
                try { process.destroyForcibly(); } catch (Throwable ignored2) {}
            }
        }
    }

    private void stopEverythingLocked(String logMessage) {
        if (stopping) return;
        stopping = true;
        cleanupComponentsLocked();
        if (logMessage != null) AppLog.append(logMessage);
        publishState("DISCONNECTED", false);
        try { stopForeground(true); } catch (Throwable ignored) {}
        stopSelf();
        stopping = false;
    }

    @Override
    public void onRevoke() {
        stopEverything("VPN permission revoked");
        super.onRevoke();
    }

    @Override
    public void onDestroy() {
        stopEverything(null);
        super.onDestroy();
    }

    @Override
    public IBinder onBind(Intent intent) {
        return super.onBind(intent);
    }

    private void publishState(String status, boolean active) {
        appStatus = status == null ? (active ? "CONNECTED" : "DISCONNECTED") : status;
        appActive = active;
        Intent state = new Intent(ACTION_STATE);
        state.setPackage(getPackageName());
        state.putExtra(EXTRA_ACTIVE, active);
        state.putExtra(EXTRA_STATUS, appStatus);
        try { sendBroadcast(state); } catch (Throwable ignored) {}
    }

    private void createNotificationChannel() {
        if (Build.VERSION.SDK_INT >= 26) {
            NotificationManager nm = getSystemService(NotificationManager.class);
            NotificationChannel channel = new NotificationChannel(
                    CHANNEL_ID,
                    "DragonTCP VPN",
                    NotificationManager.IMPORTANCE_LOW
            );
            nm.createNotificationChannel(channel);
        }
    }

    private Notification buildNotification(String status) {
        Intent open = new Intent(this, MainActivity.class);
        PendingIntent contentIntent = PendingIntent.getActivity(
                this, 0, open,
                PendingIntent.FLAG_UPDATE_CURRENT | (Build.VERSION.SDK_INT >= 23 ? PendingIntent.FLAG_IMMUTABLE : 0)
        );

        Intent stopIntent = new Intent(this, DragonService.class).setAction(ACTION_STOP);
        PendingIntent stopPending = PendingIntent.getService(
                this, 1, stopIntent,
                PendingIntent.FLAG_UPDATE_CURRENT | (Build.VERSION.SDK_INT >= 23 ? PendingIntent.FLAG_IMMUTABLE : 0)
        );

        Notification.Builder b = Build.VERSION.SDK_INT >= 26
                ? new Notification.Builder(this, CHANNEL_ID)
                : new Notification.Builder(this);
        return b.setContentTitle("DragonTCP Lite")
                .setContentText(status)
                .setSmallIcon(android.R.drawable.stat_sys_upload_done)
                .setContentIntent(contentIntent)
                .setOngoing(true)
                .addAction(new Notification.Action.Builder(
                        android.R.drawable.ic_menu_close_clear_cancel,
                        "STOP",
                        stopPending
                ).build())
                .build();
    }

    private void updateNotification(String status) {
        NotificationManager nm = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
        nm.notify(NOTIFICATION_ID, buildNotification(status));
    }
}
