package org.aethelos.command;

import android.content.Intent;
import android.net.VpnService;
import android.os.ParcelFileDescriptor;
import java.io.FileDescriptor;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.OutputStream;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.HttpURLConnection;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.URL;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.util.Collections;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;

/**
 * Aethelos Guard — on-device packet interceptor.
 *
 * Opens a system VPN (user must confirm the Android dialog). Reads IPv4
 * headers from the TUN, posts {src,dst,sport,dport,proto,bytes} to the local
 * Guard engine, and drops destinations the engine has kicked. UDP that is
 * not kicked is forwarded with {@link #protect(java.net.DatagramSocket)}.
 *
 * This is not a remote VPN. There is no outside concentrator. Payloads are
 * never written to the engine — headers only. Kick list is pulled from the
 * engine on a timer.
 *
 * Manifest (merge into the package that already launches the command):
 *
 * <pre>
 * {@code
 * <uses-permission android:name="android.permission.INTERNET" />
 * <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
 * <service
 *     android:name=".VpnCaptureService"
 *     android:exported="false"
 *     android:permission="android.permission.BIND_VPN_SERVICE">
 *   <intent-filter>
 *     <action android:name="android.net.VpnService" />
 *   </intent-filter>
 * </service>
 * }
 * </pre>
 *
 * Prepare from an Activity with {@link VpnService#prepare(android.content.Context)}
 * then {@code startService(new Intent(this, VpnCaptureService.class))}.
 */
public class VpnCaptureService extends VpnService implements Runnable {

  public static final String ENGINE = "http://127.0.0.1:8787";
  private static final int IPV4 = 4;
  private static final int PROTO_TCP = 6;
  private static final int PROTO_UDP = 17;
  private static final int MTU = 1500;

  private ParcelFileDescriptor tun;
  private Thread loop;
  private volatile boolean running;
  private final Set<String> kicked = Collections.newSetFromMap(new ConcurrentHashMap<String, Boolean>());

  @Override
  public int onStartCommand(Intent intent, int flags, int startId) {
    if (running) return START_STICKY;
    Builder b = new Builder();
    b.setSession("Aethelos Guard");
    b.setMtu(MTU);
    b.addAddress("10.8.0.2", 32);
    b.addRoute("0.0.0.0", 0);
    b.addDnsServer("1.1.1.1");
    try {
      tun = b.establish();
    } catch (Exception e) {
      stopSelf();
      return START_NOT_STICKY;
    }
    if (tun == null) {
      stopSelf();
      return START_NOT_STICKY;
    }
    running = true;
    loop = new Thread(this, "aethelos-tun");
    loop.start();
    new Thread(this::refreshKicks, "aethelos-kicks").start();
    return START_STICKY;
  }

  @Override
  public void onDestroy() {
    running = false;
    try {
      if (tun != null) tun.close();
    } catch (Exception ignored) {
    }
    super.onDestroy();
  }

  @Override
  public void run() {
    FileDescriptor fd = tun.getFileDescriptor();
    FileInputStream in = new FileInputStream(fd);
    FileOutputStream out = new FileOutputStream(fd);
    byte[] raw = new byte[MTU];
    while (running) {
      try {
        int n = in.read(raw);
        if (n <= 0) continue;
        handle(ByteBuffer.wrap(raw, 0, n), out);
      } catch (Exception e) {
        if (!running) break;
      }
    }
  }

  private void handle(ByteBuffer pkt, FileOutputStream tunOut) {
    if (pkt.remaining() < 20) return;
    int b0 = pkt.get(0) & 0xff;
    int version = b0 >>> 4;
    if (version != IPV4) return;
    int ihl = (b0 & 0x0f) * 4;
    if (ihl < 20 || pkt.remaining() < ihl + 4) return;
    int proto = pkt.get(9) & 0xff;
    int total = ((pkt.get(2) & 0xff) << 8) | (pkt.get(3) & 0xff);
    byte[] srcB = new byte[4];
    byte[] dstB = new byte[4];
    pkt.position(12);
    pkt.get(srcB);
    pkt.get(dstB);
    String src;
    String dst;
    try {
      src = InetAddress.getByAddress(srcB).getHostAddress();
      dst = InetAddress.getByAddress(dstB).getHostAddress();
    } catch (Exception e) {
      return;
    }
    int sport = 0;
    int dport = 0;
    String name = proto == PROTO_TCP ? "tcp" : proto == PROTO_UDP ? "udp" : "ip";
    if ((proto == PROTO_TCP || proto == PROTO_UDP) && pkt.remaining() >= ihl + 4) {
      pkt.position(ihl);
      sport = pkt.getShort() & 0xffff;
      dport = pkt.getShort() & 0xffff;
    }
    report(src, dst, sport, dport, name, total);
    if (kicked.contains(dst) || kicked.contains(dst + ":" + dport)) {
      return;
    }
    if (proto == PROTO_UDP) {
      forwardUdp(pkt, ihl, dst, dport, total);
    }
    // TCP pass-through needs a userspace stack (SYN-ACK injection). Kick
    // still drops. Termux tcpdump path covers full TCP header watch.
  }

  private void forwardUdp(ByteBuffer pkt, int ihl, String dst, int dport, int total) {
    int payloadOff = ihl + 8;
    if (total < payloadOff) return;
    int len = total - payloadOff;
    byte[] payload = new byte[len];
    pkt.position(payloadOff);
    pkt.get(payload);
    try {
      DatagramSocket sock = new DatagramSocket();
      protect(sock);
      sock.send(new DatagramPacket(payload, payload.length, new InetSocketAddress(dst, dport)));
      sock.close();
    } catch (Exception ignored) {
    }
  }

  private void report(String src, String dst, int sport, int dport, String proto, int bytes) {
    if ("127.0.0.1".equals(dst) || "10.8.0.2".equals(dst)) return;
    final String body =
        "{\"src\":\""
            + src
            + "\",\"dst\":\""
            + dst
            + "\",\"sport\":"
            + sport
            + ",\"dport\":"
            + dport
            + ",\"proto\":\""
            + proto
            + "\",\"bytes\":"
            + bytes
            + ",\"device_id\":\"phone-a\",\"app\":\"vpn\"}";
    new Thread(
            () -> {
              HttpURLConnection c = null;
              try {
                URL u = new URL(ENGINE + "/api/packets");
                c = (HttpURLConnection) u.openConnection();
                c.setConnectTimeout(800);
                c.setReadTimeout(800);
                c.setRequestMethod("POST");
                c.setDoOutput(true);
                c.setRequestProperty("Content-Type", "application/json");
                byte[] raw = body.getBytes(StandardCharsets.UTF_8);
                c.setFixedLengthStreamingMode(raw.length);
                OutputStream os = c.getOutputStream();
                os.write(raw);
                os.close();
                c.getResponseCode();
              } catch (Exception ignored) {
              } finally {
                if (c != null) c.disconnect();
              }
            })
        .start();
  }

  private void refreshKicks() {
    while (running) {
      HttpURLConnection c = null;
      try {
        URL u = new URL(ENGINE + "/api/firewall/rules");
        c = (HttpURLConnection) u.openConnection();
        c.setConnectTimeout(1200);
        c.setReadTimeout(1200);
        byte[] buf = new byte[8192];
        int n = c.getInputStream().read(buf);
        if (n > 0) {
          String json = new String(buf, 0, n, StandardCharsets.UTF_8);
          kicked.clear();
          int idx = 0;
          while (true) {
            int ipAt = json.indexOf("\"remote_ip\":\"", idx);
            if (ipAt < 0) break;
            int start = ipAt + 13;
            int end = json.indexOf('"', start);
            if (end < 0) break;
            String ip = json.substring(start, end);
            kicked.add(ip);
            idx = end + 1;
          }
        }
      } catch (Exception ignored) {
      } finally {
        if (c != null) c.disconnect();
      }
      try {
        Thread.sleep(4000);
      } catch (InterruptedException e) {
        return;
      }
    }
  }
}
