package com.foxx.androidcast.remoteaccess.control;

import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.os.Handler;
import android.os.IBinder;
import android.os.Looper;
import android.os.RemoteException;
import android.util.Log;

import com.foxx.androidcast.BuildConfig;
import com.foxx.androidcast.IRemoteAccessControlService;
import com.foxx.androidcast.IVpnOperationCallback;
import com.foxx.androidcast.dev.DevAdbConnectInfo;
import com.foxx.androidcast.dev.DevAdbWifiKeeper;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;

/**
 * Main-process client for {@link RemoteAccessControlService} ({@code :ra_control}).
 */
public final class RemoteAccessControlClient {
    private static final String TAG = "RaControlClient";
    private static final long BIND_TIMEOUT_MS = 8_000L;

    private static volatile RemoteAccessControlClient instance;
    private static volatile EndpointSnapshot lastSnapshot = EndpointSnapshot.stopped();

    private final Context appContext;
    private final Handler main = new Handler(Looper.getMainLooper());
    private final Object bindLock = new Object();
    private final RemoteAccessControlHostImpl hostImpl;

    private IRemoteAccessControlService service;
    private boolean binding;
    private CountDownLatch bindLatch;

    public interface OperationListener {
        void onSuccess();

        void onFailure(String reason);
    }

    public static final class EndpointSnapshot {
        public final boolean running;
        public final int port;
        public final String token;
        public final List<String> urls;

        EndpointSnapshot(boolean running, int port, String token, List<String> urls) {
            this.running = running;
            this.port = port;
            this.token = token != null ? token : "";
            this.urls = urls != null ? urls : Collections.emptyList();
        }

        static EndpointSnapshot stopped() {
            return new EndpointSnapshot(false, 0, "", Collections.emptyList());
        }
    }

    private RemoteAccessControlClient(Context context) {
        appContext = context.getApplicationContext();
        hostImpl = new RemoteAccessControlHostImpl(appContext);
    }

    public static RemoteAccessControlClient get(Context context) {
        if (instance == null) {
            synchronized (RemoteAccessControlClient.class) {
                if (instance == null) {
                    instance = new RemoteAccessControlClient(context);
                }
            }
        }
        return instance;
    }

    public static EndpointSnapshot snapshot(Context context) {
        EndpointSnapshot snap = lastSnapshot;
        if (snap != null && snap.running) {
            return snap;
        }
        RemoteAccessControlClient client = instance;
        if (client != null && client.service != null) {
            return client.readSnapshotFromService();
        }
        return EndpointSnapshot.stopped();
    }

    public static DevAdbConnectInfo.RaControlEndpoint snapshotForAdb(Context context) {
        EndpointSnapshot snap = snapshot(context);
        if (!snap.running || snap.port <= 0) {
            return null;
        }
        return new DevAdbConnectInfo.RaControlEndpoint(snap.port, snap.token, snap.urls);
    }

    private final ServiceConnection connection = new ServiceConnection() {
        @Override
        public void onServiceConnected(ComponentName name, IBinder binder) {
            synchronized (bindLock) {
                service = IRemoteAccessControlService.Stub.asInterface(binder);
                binding = false;
                if (bindLatch != null) {
                    bindLatch.countDown();
                }
            }
            registerHostIfNeeded();
        }

        @Override
        public void onServiceDisconnected(ComponentName name) {
            synchronized (bindLock) {
                service = null;
                binding = false;
            }
            updateSnapshot(EndpointSnapshot.stopped());
        }
    };

    public void startSession(OperationListener listener) {
        runWithService(s -> {
            try {
                if (s.isRunning()) {
                    refreshSnapshot();
                    deliver(listener, true, null);
                    return;
                }
                s.start(new IVpnOperationCallback.Stub() {
                    @Override
                    public void onSuccess() {
                        refreshSnapshot();
                        notifyAdbJsonRefresh();
                        deliver(listener, true, null);
                    }

                    @Override
                    public void onFailure(String reason) {
                        updateSnapshot(EndpointSnapshot.stopped());
                        deliver(listener, false, reason);
                    }
                });
            } catch (RemoteException e) {
                deliver(listener, false, e.getMessage());
            }
        });
    }

    public void stopSession() {
        runWithService(s -> {
            try {
                s.stop(new IVpnOperationCallback.Stub() {
                    @Override
                    public void onSuccess() {
                        updateSnapshot(EndpointSnapshot.stopped());
                        notifyAdbJsonRefresh();
                    }

                    @Override
                    public void onFailure(String reason) {
                        updateSnapshot(EndpointSnapshot.stopped());
                        notifyAdbJsonRefresh();
                    }
                });
            } catch (RemoteException e) {
                Log.w(TAG, "stop failed: " + e.getMessage());
                updateSnapshot(EndpointSnapshot.stopped());
            }
        });
    }

    public void stopSessionSync() {
        if (!ensureBoundSync()) {
            updateSnapshot(EndpointSnapshot.stopped());
            return;
        }
        try {
            service.stop(null);
        } catch (RemoteException ignored) {
        }
        updateSnapshot(EndpointSnapshot.stopped());
    }

    private void refreshSnapshot() {
        if (!ensureBoundSync()) {
            updateSnapshot(EndpointSnapshot.stopped());
            return;
        }
        updateSnapshot(readSnapshotFromService());
    }

    private EndpointSnapshot readSnapshotFromService() {
        IRemoteAccessControlService s = service;
        if (s == null) {
            return EndpointSnapshot.stopped();
        }
        try {
            if (!s.isRunning()) {
                return EndpointSnapshot.stopped();
            }
            int port = s.getListenPort();
            String token = s.getAuthToken();
            List<String> urls = buildUrls(port);
            return new EndpointSnapshot(true, port, token, urls);
        } catch (RemoteException e) {
            return EndpointSnapshot.stopped();
        }
    }

    private static List<String> buildUrls(int port) {
        if (port <= 0) {
            return Collections.emptyList();
        }
        List<String> out = new ArrayList<>();
        out.add("127.0.0.1:" + port);
        return out;
    }

    private static void updateSnapshot(EndpointSnapshot snap) {
        lastSnapshot = snap != null ? snap : EndpointSnapshot.stopped();
    }

    private void notifyAdbJsonRefresh() {
        if (BuildConfig.DEBUG) {
            DevAdbWifiKeeper.refreshNow(appContext, "ra-control-endpoint");
        }
    }

    private void registerHostIfNeeded() {
        IRemoteAccessControlService s = service;
        if (s == null) {
            return;
        }
        try {
            s.registerHost(hostImpl);
        } catch (RemoteException e) {
            Log.w(TAG, "registerHost failed: " + e.getMessage());
        }
    }

    private void deliver(OperationListener listener, boolean ok, String reason) {
        if (listener == null) {
            return;
        }
        main.post(() -> {
            if (ok) {
                listener.onSuccess();
            } else {
                listener.onFailure(reason != null ? reason : "error");
            }
        });
    }

    private interface ServiceTask {
        void run(IRemoteAccessControlService s) throws RemoteException;
    }

    private void runWithService(ServiceTask task) {
        IRemoteAccessControlService s = service;
        if (s != null) {
            try {
                task.run(s);
            } catch (RemoteException e) {
                Log.w(TAG, "remote call failed: " + e.getMessage());
            }
            return;
        }
        ensureBoundAsync();
        new Thread(() -> {
            if (!ensureBoundSync()) {
                return;
            }
            try {
                task.run(service);
            } catch (RemoteException e) {
                Log.w(TAG, "remote call failed: " + e.getMessage());
            }
        }, "RaControlClientBind").start();
    }

    private void ensureBoundAsync() {
        synchronized (bindLock) {
            if (service != null || binding) {
                return;
            }
            binding = true;
            Intent i = new Intent(appContext, RemoteAccessControlService.class);
            appContext.startService(i);
            appContext.bindService(i, connection, Context.BIND_AUTO_CREATE);
        }
    }

    private boolean ensureBoundSync() {
        if (service != null) {
            registerHostIfNeeded();
            return true;
        }
        synchronized (bindLock) {
            if (service != null) {
                return true;
            }
            bindLatch = new CountDownLatch(1);
            ensureBoundAsync();
            try {
                if (!bindLatch.await(BIND_TIMEOUT_MS, TimeUnit.MILLISECONDS)) {
                    Log.w(TAG, "RA control bind timeout");
                    return false;
                }
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
                return false;
            } finally {
                bindLatch = null;
            }
        }
        registerHostIfNeeded();
        return service != null;
    }
}
