package com.foxx.androidcast.network.selftest;

import android.app.Service;
import android.content.Intent;
import android.net.ConnectivityManager;
import android.net.Network;
import android.net.NetworkCapabilities;
import android.net.NetworkRequest;
import android.os.Build;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.IBinder;
import android.os.RemoteCallbackList;
import android.os.RemoteException;

import com.foxx.androidcast.BuildConfig;
import com.foxx.androidcast.INetworkChangeListener;
import com.foxx.androidcast.INetworkSelfTestService;
import com.foxx.androidcast.NetworkConditions;
import com.foxx.androidcast.dev.DevNetworkSelfTestProbe;

import org.json.JSONArray;
import org.json.JSONObject;

import java.io.File;
import java.io.FileOutputStream;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;

/** Standalone network self-test service with AIDL cache and change subscriptions. */
public final class NetworkSelfTestService extends Service {
    public static final String ACTION_START_CAPTURE = "com.foxx.androidcast.NET_SELFTEST_START_CAPTURE";
    public static final String ACTION_STOP_CAPTURE = "com.foxx.androidcast.NET_SELFTEST_STOP_CAPTURE";
    public static final String ACTION_FORCE_REFRESH = "com.foxx.androidcast.NET_SELFTEST_FORCE_REFRESH";

    private final RemoteCallbackList<INetworkChangeListener> listeners = new RemoteCallbackList<>();
    private final AtomicBoolean refreshing = new AtomicBoolean(false);
    private final Object lock = new Object();

    private HandlerThread workerThread;
    private Handler worker;
    private ConnectivityManager connectivityManager;
    private ConnectivityManager.NetworkCallback networkCallback;
    private NetworkConditions cached;
    private int periodicIntervalMinutes;

    private boolean captureActive;
    private long captureSessionStartedUtcSec;
    private JSONArray captureEvents = new JSONArray();

    private final Runnable periodicTask = new Runnable() {
        @Override
        public void run() {
            requestRefreshInternal(false, "periodic");
            if (worker != null) {
                worker.postDelayed(this, periodicIntervalMinutes * 60_000L);
            }
        }
    };

    private final INetworkSelfTestService.Stub binder = new INetworkSelfTestService.Stub() {
        @Override
        public NetworkConditions getCachedConditions() {
            synchronized (lock) {
                if (cached == null) {
                    return null;
                }
                return new NetworkConditions(cached.collectedAtMs, cached.fingerprint, cached.reportText,
                        refreshing.get());
            }
        }

        @Override
        public void requestRefresh() {
            requestRefreshInternal(true, "client_request");
        }

        @Override
        public boolean isRefreshing() {
            return refreshing.get();
        }

        @Override
        public void addListener(INetworkChangeListener listener) {
            if (listener != null) {
                listeners.register(listener);
            }
        }

        @Override
        public void removeListener(INetworkChangeListener listener) {
            if (listener != null) {
                listeners.unregister(listener);
            }
        }

        @Override
        public void startCapture() {
            setCaptureActive(true);
        }

        @Override
        public void stopCapture() {
            setCaptureActive(false);
        }

        @Override
        public void setPeriodicIntervalMinutes(int minutes) {
            if (minutes != 1 && minutes != 5 && minutes != 15) {
                return;
            }
            periodicIntervalMinutes = minutes;
            restartPeriodicTask();
        }

        @Override
        public int getPeriodicIntervalMinutes() {
            return periodicIntervalMinutes;
        }
    };

    @Override
    public void onCreate() {
        super.onCreate();
        periodicIntervalMinutes = BuildConfig.DEBUG ? 1 : 5;
        workerThread = new HandlerThread("NetSelfTestWorker");
        workerThread.start();
        worker = new Handler(workerThread.getLooper());
        connectivityManager = getSystemService(ConnectivityManager.class);
        registerNetworkCallback();
        worker.post(periodicTask);
        requestRefreshInternal(true, "service_start");
    }

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        if (intent != null) {
            String action = intent.getAction();
            if (ACTION_START_CAPTURE.equals(action)) {
                setCaptureActive(true);
                return START_STICKY;
            } else if (ACTION_STOP_CAPTURE.equals(action)) {
                setCaptureActive(false);
                return START_STICKY;
            } else if (ACTION_FORCE_REFRESH.equals(action)) {
                requestRefreshInternal(true, "intent_force");
                return START_STICKY;
            }
        }
        return START_STICKY;
    }

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

    @Override
    public void onDestroy() {
        if (worker != null) {
            worker.removeCallbacksAndMessages(null);
        }
        unregisterNetworkCallback();
        listeners.kill();
        if (workerThread != null) {
            workerThread.quitSafely();
        }
        super.onDestroy();
    }

    private void requestRefreshInternal(boolean forceNotifyIfChanged, String reason) {
        if (worker == null || !refreshing.compareAndSet(false, true)) {
            return;
        }
        worker.post(() -> {
            try {
                long now = System.currentTimeMillis();
                String report = DevNetworkSelfTestProbe.run(getApplicationContext(), new AtomicBoolean(false));
                String fingerprint = sha256(report);
                NetworkConditions next = new NetworkConditions(now, fingerprint, report, false);
                NetworkConditions prev;
                synchronized (lock) {
                    prev = cached;
                    cached = next;
                }
                boolean changed = prev == null || !fingerprint.equals(prev.fingerprint);
                if (captureActive && changed) {
                    appendCaptureEvent(reason, next);
                }
                if (changed || forceNotifyIfChanged) {
                    notifyListeners(next, changed);
                }
            } finally {
                refreshing.set(false);
            }
        });
    }

    private void notifyListeners(NetworkConditions current, boolean changed) {
        // Requirement: no push notifications when conditions have not changed.
        if (!changed) {
            return;
        }
        int n = listeners.beginBroadcast();
        try {
            for (int i = 0; i < n; i++) {
                INetworkChangeListener cb = listeners.getBroadcastItem(i);
                if (cb == null) {
                    continue;
                }
                try {
                    cb.onNetworkConditionChange(current);
                } catch (RemoteException ignored) {
                }
            }
        } finally {
            listeners.finishBroadcast();
        }
    }

    private void registerNetworkCallback() {
        if (connectivityManager == null || networkCallback != null) {
            return;
        }
        networkCallback = new ConnectivityManager.NetworkCallback() {
            @Override
            public void onAvailable(Network network) {
                onNetworkChange();
            }

            @Override
            public void onLost(Network network) {
                onNetworkChange();
            }

            @Override
            public void onCapabilitiesChanged(Network network, NetworkCapabilities networkCapabilities) {
                onNetworkChange();
            }
        };
        try {
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
                connectivityManager.registerDefaultNetworkCallback(networkCallback);
            } else {
                NetworkRequest req = new NetworkRequest.Builder().build();
                connectivityManager.registerNetworkCallback(req, networkCallback);
            }
        } catch (Exception ignored) {
        }
    }

    private void unregisterNetworkCallback() {
        if (connectivityManager == null || networkCallback == null) {
            return;
        }
        try {
            connectivityManager.unregisterNetworkCallback(networkCallback);
        } catch (Exception ignored) {
        }
        networkCallback = null;
    }

    private void onNetworkChange() {
        if (!captureActive) {
            return;
        }
        requestRefreshInternal(true, "network_changed");
    }

    private void restartPeriodicTask() {
        if (worker == null) {
            return;
        }
        worker.removeCallbacks(periodicTask);
        worker.post(periodicTask);
    }

    private void setCaptureActive(boolean active) {
        synchronized (lock) {
            if (captureActive == active) {
                return;
            }
            captureActive = active;
            if (active) {
                captureSessionStartedUtcSec = System.currentTimeMillis() / 1000L;
                captureEvents = new JSONArray();
                rotateCaptureFiles();
                requestRefreshInternal(true, "capture_start");
            } else {
                flushCaptureJournal();
            }
        }
    }

    private void appendCaptureEvent(String reason, NetworkConditions conditions) {
        synchronized (lock) {
            if (!captureActive) {
                return;
            }
            try {
                JSONObject event = new JSONObject();
                event.put("ts_ms", System.currentTimeMillis());
                event.put("reason", reason);
                event.put("collected_at_ms", conditions.collectedAtMs);
                event.put("fingerprint", conditions.fingerprint);
                event.put("report", conditions.reportText);
                captureEvents.put(event);
            } catch (Exception ignored) {
            }
            flushCaptureJournal();
        }
    }

    private void flushCaptureJournal() {
        synchronized (lock) {
            if (captureSessionStartedUtcSec <= 0) {
                return;
            }
            File out = new File(getFilesDir(), "net_conn_" + captureSessionStartedUtcSec + ".json");
            try {
                JSONObject root = new JSONObject();
                root.put("session_start_utc", captureSessionStartedUtcSec);
                root.put("capture_active", captureActive);
                root.put("events", captureEvents);
                try (FileOutputStream fos = new FileOutputStream(out, false)) {
                    fos.write(root.toString(2).getBytes(StandardCharsets.UTF_8));
                }
            } catch (Exception ignored) {
            }
        }
    }

    private void rotateCaptureFiles() {
        File dir = getFilesDir();
        File[] files = dir.listFiles((d, name) -> name.startsWith("net_conn_") && name.endsWith(".json"));
        if (files == null || files.length <= 20) {
            return;
        }
        List<File> all = new ArrayList<>(Arrays.asList(files));
        all.sort(Comparator.comparingLong(File::lastModified).reversed());
        for (int i = 20; i < all.size(); i++) {
            //noinspection ResultOfMethodCallIgnored
            all.get(i).delete();
        }
    }

    private static String sha256(String text) {
        try {
            MessageDigest md = MessageDigest.getInstance("SHA-256");
            byte[] digest = md.digest((text != null ? text : "").getBytes(StandardCharsets.UTF_8));
            StringBuilder sb = new StringBuilder(digest.length * 2);
            for (byte b : digest) {
                sb.append(String.format("%02x", b));
            }
            return sb.toString();
        } catch (Exception e) {
            return Long.toHexString(System.currentTimeMillis());
        }
    }
}
