package com.foxx.androidcast.remoteaccess;

import android.app.AlarmManager;
import android.app.PendingIntent;
import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.os.Build;
import android.os.IBinder;
import android.os.SystemClock;
import android.util.Log;

import com.foxx.androidcast.AppPreferences;
import com.foxx.androidcast.CastTrayNotifier;
import com.foxx.androidcast.DeviceInfo;
import com.foxx.androidcast.remoteaccess.control.RemoteAccessControlClient;
import com.foxx.androidcast.vpn.VpnServiceClient;

import java.net.Inet4Address;
import java.net.InetAddress;
import java.net.NetworkInterface;
import java.util.Collections;
import java.util.Random;
import java.util.UUID;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

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

/** Jittered heartbeat type:ra polling while remote access mode is active. */
public final class RemoteAccessService extends Service {
    private static final String TAG = "RemoteAccessSvc";

    public static final String ACTION_START = "com.foxx.androidcast.remoteaccess.START";
    public static final String ACTION_STOP = "com.foxx.androidcast.remoteaccess.STOP";
    public static final String ACTION_POLL = "com.foxx.androidcast.remoteaccess.POLL";
    public static final String ACTION_DISABLE_ONCE = "com.foxx.androidcast.remoteaccess.DISABLE_ONCE";
    public static final String EXTRA_MODE = "mode";

    private static final int MIN_POLL_MS = 60_000;
    private static final int MAX_POLL_MS = 420_000;

    private final ExecutorService executor = Executors.newSingleThreadExecutor();
    private final Random random = new Random();
    private String sessionRandom = "";

    private String loadOrCreateSessionRandom() {
        String stored = AppPreferences.getDevRemoteAccessRandom(this);
        if (stored != null && !stored.isEmpty()) {
            sessionRandom = stored;
            return sessionRandom;
        }
        sessionRandom = UUID.randomUUID().toString();
        AppPreferences.setDevRemoteAccessRandom(this, sessionRandom);
        return sessionRandom;
    }

    private void checkSessionExpiry() {
        long exp = AppPreferences.getDevRemoteAccessExpiresAt(this);
        if (exp > 0 && exp < System.currentTimeMillis() / 1000L) {
            Log.i(TAG, "session expired — tearing down tunnel");
            RemoteAccessTunnelHelper.tearDownAll(this);
            AppPreferences.clearDevRemoteAccessSession(this);
        }
    }

    public static void start(Context context, RemoteAccessMode mode) {
        Intent i = new Intent(context, RemoteAccessService.class);
        i.setAction(ACTION_START);
        i.putExtra(EXTRA_MODE, mode.name());
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            context.startForegroundService(i);
        } else {
            context.startService(i);
        }
    }

    public static void stop(Context context) {
        Intent i = new Intent(context, RemoteAccessService.class);
        i.setAction(ACTION_STOP);
        context.startService(i);
    }

    public static void sendDisableOnce(Context context) {
        Intent i = new Intent(context, RemoteAccessService.class);
        i.setAction(ACTION_DISABLE_ONCE);
        context.startService(i);
    }

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        String action = intent != null ? intent.getAction() : ACTION_POLL;
        if (ACTION_STOP.equals(action)) {
            cancelPoll(this);
            RemoteAccessTunnelHelper.tearDownAll(this);
            stopForeground(STOP_FOREGROUND_REMOVE);
            stopSelf();
            CastTrayNotifier.refresh(this);
            return START_NOT_STICKY;
        }
        if (ACTION_DISABLE_ONCE.equals(action)) {
            executor.execute(() -> postDisableHeartbeat());
            return START_NOT_STICKY;
        }
        RemoteAccessMode mode = AppPreferences.getDevRemoteAccessMode(this);
        if (intent != null && intent.hasExtra(EXTRA_MODE)) {
            mode = RemoteAccessMode.fromStored(intent.getStringExtra(EXTRA_MODE));
        }
        if (!mode.isActive()) {
            stop(this);
            return START_NOT_STICKY;
        }
        int fgsType = Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q
                ? android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_CONNECTED_DEVICE
                : 0;
        CastTrayNotifier.startForeground(this, fgsType);
        CastTrayNotifier.ensureTrayService(this);
        loadOrCreateSessionRandom();
        checkSessionExpiry();
        final RemoteAccessMode pollMode = mode;
        if (ACTION_START.equals(action) || ACTION_POLL.equals(action)) {
            executor.execute(() -> runPoll(pollMode));
        }
        scheduleNextPoll();
        return START_STICKY;
    }

    private void runPoll(RemoteAccessMode mode) {
        checkSessionExpiry();
        try {
            JSONObject hb = new JSONObject();
            hb.put("type", "ra");
            hb.put("status", "enable");
            hb.put("device_id", DeviceInfo.getDeviceUuid(this));
            hb.put("random", loadOrCreateSessionRandom());
            hb.put("app_version", com.foxx.androidcast.BuildConfig.VERSION_NAME);
            hb.put("device", buildDeviceMeta(this));
            hb.put("tunnel_mode", mode.toApiValue());
            hb.put("vpn_route_scope", RemoteAccessVpnRouting.routeScopeLabel(
                    AppPreferences.getDevRemoteAccessVpnRouteScope(this)));
            hb.put("vpn_app_scope", RemoteAccessVpnRouting.appScopeLabel(
                    AppPreferences.getDevRemoteAccessVpnAppScope(this)));
            JSONArray caps = new JSONArray();
            if (mode == RemoteAccessMode.WIREGUARD) {
                caps.put("wireguard");
            } else if (mode == RemoteAccessMode.RSSH) {
                caps.put("rssh");
                caps.put("ssh_reverse");
            }
            caps.put("files_app_home");
            hb.put("capabilities", caps);

            JSONObject resp = RemoteAccessHeartbeatClient.postRa(this, hb);
            if (resp == null) {
                RemoteAccessStatusStore.record(
                        this, mode, RemoteAccessStatusStore.STATE_FAILURE,
                        "RemoteAccessHeartbeatClient.postRa",
                        RemoteAccessStatusStore.INITIATOR_DEVICE,
                        "no response from BE", "");
                return;
            }
            String raAction = resp.optString("action", "wait");
            if ("connect".equals(raAction)) {
                handleConnect(mode, resp);
            } else if ("disabled".equals(raAction) || "deny".equals(raAction)) {
                RemoteAccessTunnelHelper.tearDownAll(this);
                RemoteAccessStatusStore.record(
                        this, mode, RemoteAccessStatusStore.STATE_OFF,
                        "RemoteAccessService.runPoll",
                        RemoteAccessStatusStore.INITIATOR_BE,
                        raAction, resp.optString("session_id", ""));
            } else {
                String waitReason = resp.optString("reason", "");
                RemoteAccessStatusStore.record(
                        this, mode, RemoteAccessStatusStore.STATE_WAIT,
                        "RemoteAccessService.runPoll",
                        RemoteAccessStatusStore.INITIATOR_BE,
                        "", waitReason, resp.optString("session_id", ""));
            }
        } catch (Exception e) {
            Log.w(TAG, "poll failed: " + e.getMessage());
            RemoteAccessStatusStore.record(
                    this, mode, RemoteAccessStatusStore.STATE_FAILURE,
                    "RemoteAccessService.runPoll",
                    RemoteAccessStatusStore.INITIATOR_DEVICE,
                    RemoteAccessFailureReason.fromPollException(this, e), "");
        }
    }

    private void postDisableHeartbeat() {
        try {
            RemoteAccessTunnelHelper.tearDownAll(this);
            AppPreferences.clearDevRemoteAccessSession(this);
            JSONObject hb = new JSONObject();
            hb.put("type", "ra");
            hb.put("status", "disable");
            hb.put("device_id", DeviceInfo.getDeviceUuid(this));
            hb.put("random", loadOrCreateSessionRandom());
            RemoteAccessHeartbeatClient.postRa(this, hb);
        } catch (Exception e) {
            Log.w(TAG, "disable notify failed: " + e.getMessage());
        }
    }

    private void handleConnect(RemoteAccessMode mode, JSONObject resp) {
        String tunnel = resp.optString("tunnel", "");
        String sessionId = resp.optString("session_id", "");
        long expiresAt = resp.optLong("expires_at", 0L);
        if (!sessionId.isEmpty()) {
            AppPreferences.setDevRemoteAccessSession(this, sessionId, expiresAt);
        }
        JSONObject creds = resp.optJSONObject("credentials");
        if (creds == null) {
            RemoteAccessStatusStore.record(
                    this, mode, RemoteAccessStatusStore.STATE_FAILURE,
                    "RemoteAccessService.handleConnect",
                    RemoteAccessStatusStore.INITIATOR_BE,
                    "connect without credentials", sessionId);
            return;
        }
        if ("wireguard".equals(tunnel)) {
            String wgConfig = WireGuardConfigBuilder.fromConnectCredentials(this, creds);
            if (wgConfig.isEmpty()) {
                RemoteAccessStatusStore.record(
                        this, mode, RemoteAccessStatusStore.STATE_FAILURE,
                        "VpnServiceClient.applyWireGuardConfig",
                        RemoteAccessStatusStore.INITIATOR_BE,
                        "empty wireguard config", sessionId);
                return;
            }
            RemoteAccessStatusStore.record(
                    this, mode, RemoteAccessStatusStore.STATE_CONNECTING,
                    "VpnServiceClient.applyWireGuardConfig",
                    RemoteAccessStatusStore.INITIATOR_BE,
                    "", sessionId);
            final String sid = sessionId;
            VpnServiceClient.get(this).applyWireGuardConfig(wgConfig, new VpnServiceClient.OperationListener() {
                @Override
                public void onSuccess() {
                    updateNotificationConnected(sid, tunnel);
                    startRemoteControlPlane(sid);
                    RemoteAccessStatusStore.record(
                            RemoteAccessService.this, mode, RemoteAccessStatusStore.STATE_SUCCESS,
                            "VpnServiceClient.applyWireGuardConfig",
                            RemoteAccessStatusStore.INITIATOR_BE,
                            "", sid);
                }

                @Override
                public void onFailure(String reason) {
                    Log.w(TAG, "wireguard connect failed: " + reason);
                    RemoteAccessStatusStore.record(
                            RemoteAccessService.this, mode, RemoteAccessStatusStore.STATE_FAILURE,
                            "VpnServiceClient.applyWireGuardConfig",
                            RemoteAccessStatusStore.INITIATOR_BE,
                            reason, sid);
                }
            });
        } else if ("ssh_reverse".equals(tunnel)) {
            RemoteAccessStatusStore.record(
                    this, mode, RemoteAccessStatusStore.STATE_CONNECTING,
                    "ReverseSshTunnelBridge.applyConfig",
                    RemoteAccessStatusStore.INITIATOR_BE,
                    "", sessionId);
            if (ReverseSshTunnelBridge.applyConfig(this, creds)) {
                updateNotificationConnected(sessionId, tunnel);
                startRemoteControlPlane(sessionId);
                RemoteAccessStatusStore.record(
                        this, mode, RemoteAccessStatusStore.STATE_SUCCESS,
                        "ReverseSshTunnelBridge.applyConfig",
                        RemoteAccessStatusStore.INITIATOR_BE,
                        "", sessionId);
            } else {
                RemoteAccessStatusStore.record(
                        this, mode, RemoteAccessStatusStore.STATE_FAILURE,
                        "ReverseSshTunnelBridge.applyConfig",
                        RemoteAccessStatusStore.INITIATOR_BE,
                        "RSSH connect failed or timed out", sessionId);
            }
        } else {
            Log.w(TAG, "unsupported tunnel in connect: " + tunnel);
            RemoteAccessStatusStore.record(
                    this, mode, RemoteAccessStatusStore.STATE_FAILURE,
                    "RemoteAccessService.handleConnect",
                    RemoteAccessStatusStore.INITIATOR_BE,
                    "unsupported tunnel: " + tunnel, sessionId);
        }
    }

    private void startRemoteControlPlane(String sessionId) {
        RemoteAccessControlClient.get(this).startSession(new RemoteAccessControlClient.OperationListener() {
            @Override
            public void onSuccess() {
                RemoteAccessControlClient.EndpointSnapshot snap =
                        RemoteAccessControlClient.snapshot(RemoteAccessService.this);
                Log.i(TAG, "RA control HTTP on :" + snap.port + " session=" + sessionId);
            }

            @Override
            public void onFailure(String reason) {
                Log.w(TAG, "RA control HTTP failed: " + reason);
            }
        });
    }

    private void updateNotificationConnected(String sessionId, String tunnel) {
        CastTrayNotifier.refresh(this);
    }

    private void scheduleNextPoll() {
        int delay = MIN_POLL_MS + random.nextInt(MAX_POLL_MS - MIN_POLL_MS + 1);
        AlarmManager am = (AlarmManager) getSystemService(Context.ALARM_SERVICE);
        if (am == null) {
            return;
        }
        PendingIntent pi = PendingIntent.getService(this, 0,
                new Intent(this, RemoteAccessService.class).setAction(ACTION_POLL),
                PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE);
        long trigger = SystemClock.elapsedRealtime() + delay;
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
            am.setAndAllowWhileIdle(AlarmManager.ELAPSED_REALTIME_WAKEUP, trigger, pi);
        } else {
            am.set(AlarmManager.ELAPSED_REALTIME_WAKEUP, trigger, pi);
        }
    }

    private static void cancelPoll(Context context) {
        AlarmManager am = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
        if (am == null) {
            return;
        }
        PendingIntent pi = PendingIntent.getService(context, 0,
                new Intent(context, RemoteAccessService.class).setAction(ACTION_POLL),
                PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE);
        am.cancel(pi);
    }

    private static JSONObject buildDeviceMeta(Context context) throws Exception {
        JSONObject device = new JSONObject();
        device.put("name", AppPreferences.getUsername(context));
        device.put("manufacturer", Build.MANUFACTURER);
        device.put("brand", Build.BRAND);
        device.put("model", Build.MODEL);
        device.put("device", Build.DEVICE);
        device.put("product", Build.PRODUCT);
        device.put("sdk_int", Build.VERSION.SDK_INT);
        device.put("release", Build.VERSION.RELEASE);
        JSONArray abis = new JSONArray();
        for (String abi : Build.SUPPORTED_ABIS) {
            abis.put(abi);
        }
        device.put("abis", abis);
        String lanIp = localLanIpV4();
        if (!lanIp.isEmpty()) {
            device.put("lan_ip", lanIp);
        }
        return device;
    }

    private static String localLanIpV4() {
        try {
            for (NetworkInterface intf : Collections.list(NetworkInterface.getNetworkInterfaces())) {
                if (intf == null || !intf.isUp() || intf.isLoopback()) {
                    continue;
                }
                for (InetAddress addr : Collections.list(intf.getInetAddresses())) {
                    if (addr == null || addr.isLoopbackAddress() || !(addr instanceof Inet4Address)) {
                        continue;
                    }
                    String host = addr.getHostAddress();
                    if (host != null && !host.isEmpty()) {
                        return host;
                    }
                }
            }
        } catch (Exception ignored) {
        }
        return "";
    }

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

    @Override
    public void onDestroy() {
        executor.shutdownNow();
        super.onDestroy();
    }
}
