package org.torproject.artitoyvpn.vpn;

import static org.torproject.artitoyvpn.vpn.VpnStatusObservable.Status.ERROR;
import static org.torproject.artitoyvpn.vpn.VpnStatusObservable.Status.STOPPED;

import android.app.Notification;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.net.VpnService;
import android.os.Binder;
import android.os.Build;
import android.os.Handler;
import android.os.IBinder;
import android.os.ParcelFileDescriptor;
import android.system.OsConstants;

import androidx.lifecycle.Observer;

import org.torproject.artitoyvpn.R;
import org.torproject.artitoyvpn.utils.PreferenceHelper;
import org.torproject.artitoyvpn.utils.VpnNotificationManager;
import org.torproject.onionmasq.ISocketProtect;
import org.torproject.onionmasq.OnionMasq;
import org.torproject.onionmasq.errors.OnionmasqException;
import org.torproject.onionmasq.events.BootstrapEvent;
import org.torproject.onionmasq.events.ClosedConnectionEvent;
import org.torproject.onionmasq.events.FailedConnectionEvent;
import org.torproject.onionmasq.events.NewConnectionEvent;
import org.torproject.onionmasq.events.NewDirectoryEvent;
import org.torproject.onionmasq.events.OnionmasqEvent;
import org.torproject.onionmasq.events.RelayDetails;
import org.torproject.onionmasq.logging.LogHelper;
import org.torproject.onionmasq.logging.LogObservable;

import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.Timer;
import java.util.TimerTask;

public class ArtiVpnService extends VpnService {
    static final String TAG = ArtiVpnService.class.getSimpleName();
    public static final String ACTION_START_VPN = TAG + ".start";
    public static final String ACTION_STOP_VPN = TAG + ".stop";
    ParcelFileDescriptor fd;
    Thread thread;
    private VpnNotificationManager notificationManager;
    private final static int ALWAYS_ON_MIN_API_LEVEL = Build.VERSION_CODES.N;
    private Thread onionThread;
    private LogHelper logHelper;
    private Observer<OnionmasqEvent> observer;
    private Handler mainHandler;

    private PreferenceHelper preferenceHelper;

    private Timer timer;

    /** Maximum packet size is constrained by the MTU, which is given as a signed short. */
    private static final int MAX_PACKET_SIZE = Short.MAX_VALUE;

    private final IBinder binder = new ArtiVpnServiceBinder();
    public class ArtiVpnServiceBinder extends Binder implements ISocketProtect {
        @Override
        public boolean protect(int socket) {
            return ArtiVpnService.this.protect(socket);
        }
    }

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

    @Override
    public void onCreate() {
        super.onCreate();
        notificationManager = new VpnNotificationManager(this);
        logHelper = new LogHelper();
        OnionMasq.bindVPNService(ArtiVpnService.class);
        mainHandler = new Handler(getMainLooper());
        timer = new Timer();
        preferenceHelper = new PreferenceHelper(this);
    }

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        Notification notification = notificationManager.buildForegroundServiceNotification();
        startForeground(VpnNotificationManager.ARTI_NOTIFICATION_ID, notification);
        String action = intent != null ? intent.getAction() : "";
        if (action.equals(ACTION_START_VPN) ||
                action.equals("android.net.VpnService") && Build.VERSION.SDK_INT >= ALWAYS_ON_MIN_API_LEVEL) {
                //only always-on feature triggers this
            thread = new Thread(this::establishVpn);
            thread.start();
        } else if (action.equals(ACTION_STOP_VPN)) {
            stop();
        }
        return START_STICKY;
    }

    @Override
    public void onRevoke() {
        super.onRevoke();
        stop();
    }

    @Override
    public void onDestroy() {
        super.onDestroy();
        notificationManager.cancelNotifications();
        if (VpnStatusObservable.getStatus().getValue() != ERROR) {
            VpnStatusObservable.update(STOPPED);
        }
    }

    private void stop() {
        OnionMasq.stop();
        logHelper.stopLog();
        mainHandler.post(() -> OnionMasq.getEventObservable().removeObserver(observer));
        timer.cancel();
        VpnStatusObservable.resetData();
        closeFd();
        VpnStatusObservable.update(VpnStatusObservable.Status.STOPPING);
        stopForeground(true);
        OnionMasq.unbindVPNService();
        stopSelf();
    }

    private void closeFd() {
        try {
            if (fd != null)
                fd.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    private Builder prepareVpnProfile() {
        Builder builder = new Builder();
        applyAppFilter(builder);
        builder.setSession("Arti dummy session");
        builder.addRoute("0.0.0.0", 0);
        builder.addRoute("::",0);
        builder.addAddress("169.254.42.1", 16);
        builder.addAddress("fc00::", 7);
        builder.addDnsServer("169.254.42.53");
        builder.addDnsServer("fe80::53");

        builder.allowFamily(OsConstants.AF_INET);
        builder.allowFamily(OsConstants.AF_INET6);
        builder.setMtu(1500);

        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
            builder.setMetered(false);
        }
        return builder;
    }

    private void establishVpn() {
        try {
            Builder builder = prepareVpnProfile();
            ParcelFileDescriptor fd = builder.establish();

            logHelper.readLog();
            onionThread = new Thread(() -> {
                try {
                    OnionMasq.start(fd.detachFd(), preferenceHelper.getBridgeLines());
                } catch (OnionmasqException e) {
                    // FIXME(eta): We probably want to actually show the error to the user here!
                    e.printStackTrace();
                    VpnStatusObservable.update(ERROR);
                    stopSelf();
                }
            });
            observer = (Observer<OnionmasqEvent>) onionmasqEvent -> {
                if (onionmasqEvent instanceof BootstrapEvent) {
                    BootstrapEvent event = (BootstrapEvent) onionmasqEvent;
                    if (event.isReadyForTraffic) {
                        VpnStatusObservable.update(VpnStatusObservable.Status.RUNNING);
                    }
                    LogObservable.getInstance().addLog(getString(R.string.bootstrap_at, event.bootstrapStatus));
                }
                else if (onionmasqEvent instanceof NewConnectionEvent) {
                    VpnStatusObservable.handleConnectionEvent(onionmasqEvent);
                    NewConnectionEvent event = (NewConnectionEvent) onionmasqEvent;
                    LogObservable.getInstance().addLog(getString(R.string.new_connection, event.proxySrc, event.proxyDst, event.torDst, event.appId));
                    int i = 0;
                    for (RelayDetails relay : event.circuit) {
                        String identity = "unknown";
                        String address = "unknown";
                        String country = "??";
                        if (relay.ed_identity != null) {
                            identity = relay.ed_identity;
                        }
                        if (relay.rsa_identity != null) {
                            identity = relay.rsa_identity;
                        }
                        if (relay.country_code != null) {
                            country = relay.country_code;
                        }
                        if (relay.addresses.size() > 0) {
                            address = relay.addresses.get(0);
                        }
                        LogObservable.getInstance().addLog(getString(R.string.new_connection_hop, event.proxySrc, event.proxyDst, i, address, country, identity));
                        i++;
                    }
                }
                else if (onionmasqEvent instanceof FailedConnectionEvent) {
                    VpnStatusObservable.handleConnectionEvent(onionmasqEvent);
                    FailedConnectionEvent event = (FailedConnectionEvent) onionmasqEvent;
                    LogObservable.getInstance().addLog(getString(R.string.failed_connection, event.proxySrc, event.proxyDst, event.torDst, event.error, event.appId));
                }
                else if (onionmasqEvent instanceof ClosedConnectionEvent) {
                    VpnStatusObservable.handleConnectionEvent(onionmasqEvent);
                    ClosedConnectionEvent event = (ClosedConnectionEvent) onionmasqEvent;
                    if (event.error != null) {
                        LogObservable.getInstance().addLog(getString(R.string.failed_connection_closed, event.proxySrc, event.proxyDst, event.error));
                    }
                    else {
                        LogObservable.getInstance().addLog(getString(R.string.closed_connection, event.proxySrc, event.proxyDst));
                    }
                }
                else if (onionmasqEvent instanceof NewDirectoryEvent) {
                    LogObservable.getInstance().addLog("New directory received.");

                    NewDirectoryEvent event = ((NewDirectoryEvent) onionmasqEvent);
                    List<Map.Entry<String, Long>> relaysByCountry = new ArrayList(event.relaysByCountry.entrySet());
                    relaysByCountry.sort((x, y) -> x.getValue().compareTo(y.getValue()));
                    for (Map.Entry<String, Long> entry : relaysByCountry) {
                        LogObservable.getInstance().addLog(getString(R.string.relay_country_count, entry.getKey(), entry.getValue()));
                    }
                }
            };
            mainHandler.post(() -> OnionMasq.getEventObservable().observeForever(observer));
            onionThread.start();
            timer.schedule(new TimerTask() {
                @Override
                public void run() {
                    VpnStatusObservable.updateDataUsage(OnionMasq.getBytesReceived(), OnionMasq.getBytesSent());
                }
            }, 0, 1000);
        } catch (Exception e) {
            // Catch any exception
            OnionMasq.stop();
            e.printStackTrace();
            timer.cancel();
            VpnStatusObservable.resetData();
            VpnStatusObservable.update(ERROR);
            stopSelf();
        }
    }

    /**
     * Adds selected app into 'allowed apps' for current vpn connection. Only selected apps will use VPN.
     * @param builder VPN Builder
     */
    private void applyAppFilter(Builder builder){
        Set<String> selectedApps = preferenceHelper.getSelectedApps();

        if (selectedApps == null){
            //No selection done, so we allow all apps.
            return;
        }

        PackageManager packageManager = getPackageManager();
        for (String appPackage: selectedApps){
            try {
                packageManager.getPackageInfo(appPackage, 0);
                builder.addAllowedApplication(appPackage);
            } catch (PackageManager.NameNotFoundException e) {
                // The app is selected but isn't installed anymore.
            }
        }
    }
}
