package com.reactnativevstarcam;

import android.util.Log;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;

import com.facebook.react.bridge.Arguments;
import com.facebook.react.bridge.Promise;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactMethod;
import com.facebook.react.bridge.WritableMap;
import com.facebook.react.bridge.LifecycleEventListener;
import com.facebook.react.module.annotations.ReactModule;
import com.facebook.react.modules.core.DeviceEventManagerModule;

import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.lang.reflect.Method;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.regex.Pattern;

import org.json.JSONObject;


/**
 * VStarCam React Native Module
 * 
 * Implements P2P camera connectivity using the VStarCam SDK.
 * Uses JNI calls to the native OKSMARTPPCS library via JNIApi class.
 * 
 * JNI API Methods (discovered via reflection):
 *   - create(String did, String serverParam) -> long clientPtr
 *   - connect(long clientPtr, int timeout, String serverParam, int connectType) 
 *   - login(long clientPtr, String username, String password)
 *   - disconnect(long clientPtr)
 *   - destroy(long clientPtr)
 *   - writeCgi(long clientPtr, String cgi, int timeout)
 *   - init(ClientStateListener, ClientCommandListener, ClientReleaseListener)
 */
@ReactModule(name = VStarCamModule.MODULE_NAME)
public class VStarCamModule extends ReactContextBaseJavaModule implements LifecycleEventListener {
    private static final String TAG = "VStarCamModule";
    public final static String MODULE_NAME = "VStarCam";
    
    // Set to true for verbose logging during development
    private static final boolean DEBUG_LOGGING = true;
    
    // Counter for unique internal client IDs
    private static final java.util.concurrent.atomic.AtomicInteger nextClientPtr = new java.util.concurrent.atomic.AtomicInteger(1000);
    
    private static Object stateListenerProxy;
    private static Object commandListenerProxy;
    private static Object releaseListenerProxy;
    
    // Hard reference root to completely prevent Dalvik GC of our JNI proxies
    private static final java.util.ArrayList<Object> jniProxyRoots = new java.util.ArrayList<>();
    
    // Static context reference for reloads
    private static java.lang.ref.WeakReference<ReactApplicationContext> currentContext;
    
    // Static initialization state
    private static boolean isNativeLibraryLoaded = false;
    private static boolean isP2PInitialized = false;
    private static Class<?> jniApiClass = null;
    private static Class<?> stateListenerClass = null;
    private static Class<?> commandListenerClass = null;
    private static Class<?> releaseListenerClass = null;
    private static final Object initLock = new Object();
    
    // Helper for conditional debug logging
    private static void logDebug(String message) {
        if (DEBUG_LOGGING) {
            Log.d(TAG, message);
        }
    }
    
    // P2P Server parameters by DID prefix (from Flutter SDK)
    private static final Map<String, String> SERVICE_PARAM_MAP = new HashMap<String, String>() {{
        put("VSTC", "EBGBEMBMKGJMGAJPEIGIFKEGHBMCHMNFGKEGBFCBBMJELILDCJADCIOLHHLLJBKEAMMBLCDGONMDBJCJJPNFJP");
        put("VSTB", "EBGBEMBMKGJMGAJPEIGIFKEGHBMCHMNFGKEGBFCBBMJELILDCJADCIOLHHLLJBKEAMMBLCDGONMDBJCJJPNFJP");
        put("VSTA", "EFGBFFBJKDJKGGJMEAHKFHEJHNNHHANIHIFBBKDFAMJDLKKODNANCBPJGGLEIELOBGNDKADKPGNJBNCNIF");
        put("VSTE", "EEGDFHBAKKIOGNJHEGHMFEEDGLNOHJMPHAFPBEDLADILKEKPDLBDDNPOHKKCIFKJBNNNKLCPPPNDBFDL");
        put("VSTG", "EEGDFHBLKGJIGEJLEKGOFMEDHAMHHJNAGGFABMCOBGJOLHLJDFAFCPPHGILKIKLMANNHKEDKOINIBNCPJOMK:vstarcam2018");
        put("VSTH", "EEGDFHBLKGJIGEJLEKGOFMEDHAMHHJNAGGFABMCOBGJOLHLJDFAFCPPHGILKIKLMANNHKEDKOINIBNCPJOMK:vstarcam2018");
        put("VSTJ", "EEGDFHBLKGJIGEJNEOHEFBEIGANCHHMBHIFEAHDEAMJCKCKJDJAFDDPPHLKJIHLMBENHKDCHPHNJBODA:vstarcam2019");
        put("VSTK", "EBGDEJBJKGJFGJJBEFHPFCEKHGNMHNNMHMFFBICPAJJNLDLLDHACCNONGLLPJGLKANMJLDDHODMEBOCIJEMA:vstarcam2019");
        put("VSTL", "EEGDFHBLKGJIGEJIEIGN FPEEHGNMHPNBGOFIBECEBLJDLMLGDKAPCNPFGOLLJFLJAOMKLBDFOGMAAFCJJPNFJP:vstarcam2019");
        put("VSTM", "EBGEEOBOKHJNHGJGEAGAEPEPHDMGHINBGIECBBCBBJIKLKLCCDBBCFODHLKLJJKPBOMELECKPNMNAICEJCNNJH:vstarcam2019");
        put("VSTN", "EEGDFHBBKBIFGAIAFGHDFLFJGJNIGEMOHFFPAMDMAAIIKBKNCDBDDMOGHLKCJCKFBFMPLMCBPEMG:vstarcam2019");
        put("VSTP", "EEGDFHBLKGJIGEJLEIGJFLENHLNBHCNMGAFGBNCOAIJMLKKODNALCCPKGBLHJLLHAHMBKNDFOGNGBDCIJFMB:vstarcam2019");
        put("VSGG", "EBGDEJBJKGJEGIJHELGKFIEEHBMPHBNNGEFCBGCAAGJBLOLFDJAMCPODGFLEJIKNAFMBKNDHPLNEAM:vstarcam2021");
        put("VSKK", "EIHGFNBAKMIIGLJHECHIFFECGKNNHONAHAFOBBDOAEJGLLKPDMAMCAPIGGLBIBLCAEMIKEDPOEMMBGCFJHNMJG:veepai2023");
        put("VSKM", "EIHGFNBAKMIIGLJEEAHKFEEJHLNBHCNIGFFDBLCMAKJNLELPDDAGCNOOGILMJFLJAOMKLADEOEMJAPCDJCNPJF:veepai2024");
        put("VSLL", "EIHGFOBBKKIOGNJKENHHFKEEGMNOHLMNHBFKBBDOAHJBLMKIDLAJCAPIGDLBJGLKAOMILNDJOJMGAHCLJHNMJG:veepai2024");
        put("ACAE", "EEGDFHBLKGJIGEJLEKGOFMEDHAMHHJNAGGFABMCOBGJOLHLJDFAFCPPHGILKIKLMANNHKEDKOINIBNCPJOMK:vstarcam2018"); // Try VSTH params
        // Fallback for unknown prefixes
        put("DEFAULT", "EBGBEMBMKGJMGAJPEIGIFKEGHBMCHMNFGKEGBFCBBMJELILDCJADCIOLHHLLJBKEAMMBLCDGONMDBJCJJPNFJP");
    }};
    
    // Get service parameter for a DID
    private static String getServiceParam(String did) {
        if (did == null || did.length() < 4) {
            return SERVICE_PARAM_MAP.get("DEFAULT");
        }
        String prefix = did.substring(0, 4).toUpperCase();
        String param = SERVICE_PARAM_MAP.get(prefix);
        if (param == null) {
            Log.w(TAG, "No service param for prefix: " + prefix + ", using DEFAULT");
            param = SERVICE_PARAM_MAP.get("DEFAULT");
        }
        Log.d(TAG, "Using service param for prefix " + prefix + ": " + (param.length() > 20 ? param.substring(0, 20) + "..." : param));
        return param;
    }
    
    // Pattern to detect virtual UIDs (like ACAE0001151GWYQ)
    // Virtual UIDs: start with letters, have 7+ digits, end with letters
    private static final Pattern VIRTUAL_UID_PATTERN = Pattern.compile("^[a-zA-Z]{1,}\\d{7,}.*[a-zA-Z]$");
    
    // Check if a device ID is a virtual UID that needs conversion
    private static boolean isVirtualId(String deviceId) {
        if (deviceId == null || deviceId.length() < 10) return false;
        return VIRTUAL_UID_PATTERN.matcher(deviceId).matches();
    }
    
    // Convert virtual UID to real client ID using VStarCam API
    // Returns: {uid, supplier, cluster} or null on failure
    private String[] convertVirtualUid(String virtualUid) {
        try {
            URL url = new URL("https://vuid.eye4.cn?vuid=" + virtualUid);
            HttpURLConnection conn = (HttpURLConnection) url.openConnection();
            conn.setRequestMethod("GET");
            conn.setConnectTimeout(10000);
            conn.setReadTimeout(10000);
            conn.setRequestProperty("Content-Type", "application/json; charset=utf-8");
            
            int responseCode = conn.getResponseCode();
            if (responseCode == 200) {
                BufferedReader reader = new BufferedReader(new InputStreamReader(conn.getInputStream()));
                StringBuilder response = new StringBuilder();
                String line;
                while ((line = reader.readLine()) != null) {
                    response.append(line);
                }
                reader.close();
                
                JSONObject json = new JSONObject(response.toString());
                String uid = json.optString("uid", null);
                String supplier = json.optString("supplier", "");
                String cluster = json.optString("cluster", "");
                
                if (uid != null && !uid.isEmpty()) {
                    Log.d(TAG, "Converted virtual UID " + virtualUid + " -> real UID: " + uid + " (supplier: " + supplier + ", cluster: " + cluster + ")");
                    return new String[]{uid, supplier, cluster};
                }
            } else {
                Log.e(TAG, "Virtual UID conversion failed, HTTP " + responseCode);
            }
            conn.disconnect();
        } catch (Exception e) {
            Log.e(TAG, "Failed to convert virtual UID: " + e.getMessage(), e);
        }
        return null;
    }
    
    private final ReactApplicationContext reactContext;
    private final ExecutorService executor;
    
    // Client tracking - maps our clientPtr to actual SDK client handle
    // Made static so VStarCamVideoView can access SDK pointers
    private static Map<Integer, ClientInfo> clients = new java.util.concurrent.ConcurrentHashMap<>();
    
    private static class ClientInfo {
        String deviceId;          // Original device ID (can be virtual UID)
        String realClientId;      // Converted client ID (for JNI calls)
        long sdkClientPtr = 0;    // Actual SDK client pointer (long)
        boolean isConnected = false;
        boolean isLoggedIn = false;
        String username;          // Login username for CGI commands
        String password;          // Login password for CGI commands
    }

    /**
     * Get the actual SDK client pointer for a given internal client ID.
     * This is needed by VStarCamVideoView to connect to the player.
     */
    public static long getSdkClientPtr(int clientPtr) {
        ClientInfo info = clients.get(clientPtr);
        if (info != null) {
            return info.sdkClientPtr;
        }
        return 0;
    }

    public VStarCamModule(ReactApplicationContext reactContext) {
        super(reactContext);
        this.reactContext = reactContext;
        this.executor = Executors.newCachedThreadPool();
        
        // Register for lifecycle events to tear down JNI on fast-refresh
        reactContext.addLifecycleEventListener(this);
        
        // Update current context for static proxies
        currentContext = new java.lang.ref.WeakReference<>(reactContext);
        
        synchronized (initLock) {
            // Load the native libraries
            try {
                if (!isNativeLibraryLoaded) {
                    System.loadLibrary("vp_log");
                    System.loadLibrary("OKSMARTPPCS");
                    isNativeLibraryLoaded = true;
                    Log.d(TAG, "Native libraries loaded successfully");
                }
                
                // Load JNI API class
                if (jniApiClass == null) {
                    jniApiClass = Class.forName("com.vstarcam.JNIApi");
                    stateListenerClass = Class.forName("com.vstarcam.app_p2p_api.ClientStateListener");
                    commandListenerClass = Class.forName("com.vstarcam.app_p2p_api.ClientCommandListener");
                    releaseListenerClass = Class.forName("com.vstarcam.app_p2p_api.ClientReleaseListener");
                    Log.d(TAG, "JNI classes and interfaces loaded");
                }
                
                // Initialize P2P system
                initializeP2P();
            } catch (Exception e) {
                Log.e(TAG, "Native initialization failed", e);
            }
        }
    }
    
    @Override
    public void onHostResume() {
    }

    @Override
    public void onHostPause() {
    }

    @Override
    public void onHostDestroy() {
        Log.d(TAG, "onHostDestroy: Tearing down all P2P connections before JNI environment goes out of scope.");
        executor.execute(() -> {
            try {
                if (jniApiClass != null) {
                    Method disconnectMethod = jniApiClass.getMethod("disconnect", long.class);
                    Method destroyMethod = jniApiClass.getMethod("destroy", long.class);
                    
                    for (Map.Entry<Integer, ClientInfo> entry : clients.entrySet()) {
                        long sdkPtr = entry.getValue().sdkClientPtr;
                        if (sdkPtr != 0) {
                            Log.d(TAG, "Teardown SDK client: " + sdkPtr);
                            try { disconnectMethod.invoke(null, sdkPtr); } catch (Exception e) {}
                            try { destroyMethod.invoke(null, sdkPtr); } catch (Exception e) {}
                        }
                    }
                    clients.clear();
                }
            } catch (Exception e) {
                Log.e(TAG, "Failed to teardown P2P in onHostDestroy", e);
            }
        });
    }
    
    private void initializeP2P() {
        if (!VStarCamModule.isNativeLibraryLoaded || VStarCamModule.jniApiClass == null) {
            Log.e(TAG, "Cannot initialize P2P: native library or JNIApi not available");
            return;
        }

        if (VStarCamModule.isP2PInitialized) {
            Log.d(TAG, "P2P system already initialized. Skipping JNIApi.init() to prevent C++ SDK crashes during hot reload.");
            return;
        }
        
        try {
            // Create CONCRETE listener implementations (NOT dynamic proxies).
            // Dynamic Proxies ($Proxy0 classes) crash under CheckJNI when invoked
            // from the C++ SDK's native pthreads via CallVoidMethod. Concrete
            // anonymous classes have real vtables that work correctly.
            
            com.vstarcam.app_p2p_api.ClientStateListener stateListener = 
                new com.vstarcam.app_p2p_api.ClientStateListener() {
                    @Override
                    public void stateListener(long clientPtr, int state) {
                        try {
                            Log.d(TAG, "State callback: clientPtr=" + clientPtr + ", state=" + state);
                            VStarCamModule.handleStateChange(clientPtr, state);
                        } catch (Exception e) {
                            Log.e(TAG, "Error in stateListener callback", e);
                        }
                    }
                };
            
            com.vstarcam.app_p2p_api.ClientCommandListener commandListener = 
                new com.vstarcam.app_p2p_api.ClientCommandListener() {
                    @Override
                    public void commandListener(long clientPtr, byte[] data, int length) {
                        try {
                            Log.d(TAG, "Command callback: clientPtr=" + clientPtr + ", dataLen=" + (data != null ? data.length : 0));
                            VStarCamModule.handleCommandReceive(clientPtr, 0, data);
                        } catch (Exception e) {
                            Log.e(TAG, "Error in commandListener callback", e);
                        }
                    }
                };
            
            com.vstarcam.app_p2p_api.ClientReleaseListener releaseListener = 
                new com.vstarcam.app_p2p_api.ClientReleaseListener() {
                    @Override
                    public void releaseListener(long clientPtr) {
                        try {
                            Log.d(TAG, "Release callback: clientPtr=" + clientPtr);
                        } catch (Exception e) {
                            Log.e(TAG, "Error in releaseListener callback", e);
                        }
                    }
                };
            
            // Keep strong references to prevent GC
            stateListenerProxy = stateListener;
            commandListenerProxy = commandListener;
            releaseListenerProxy = releaseListener;
            jniProxyRoots.add(stateListener);
            jniProxyRoots.add(commandListener);
            jniProxyRoots.add(releaseListener);
            
            // Call JNIApi.init(stateListener, commandListener, releaseListener)
            com.vstarcam.JNIApi.init(stateListener, commandListener, releaseListener);
            
            VStarCamModule.isP2PInitialized = true;
            Log.d(TAG, "P2P system initialized successfully with concrete listeners!");
        } catch (Exception e) {
            Log.e(TAG, "Failed to initialize P2P: " + e.getMessage(), e);
        }
    }
    
    private static void handleStateChange(long clientPtr, int state) {
        Log.d(TAG, "State change: clientPtr=" + clientPtr + ", state=" + state);
        
        final ReactApplicationContext ctx = currentContext != null ? currentContext.get() : null;
        if (ctx == null || !ctx.hasActiveReactInstance()) {
            Log.w(TAG, "State change ignored: no active React instance");
            return;
        }

        ctx.runOnUiQueueThread(new Runnable() {
            @Override
            public void run() {
                // Find our client by SDK pointer
                for (Map.Entry<Integer, ClientInfo> entry : clients.entrySet()) {
                    if (entry.getValue().sdkClientPtr == clientPtr) {
                        int ourClientPtr = entry.getKey();
                        
                        WritableMap params = Arguments.createMap();
                        params.putInt("clientId", ourClientPtr);
                        params.putInt("state", state);
                        sendEvent("onConnectionStateChanged", params);
                        
                        // Update connected state
                        entry.getValue().isConnected = (state == 3); // ONLINE
                        break;
                    }
                }
            }
        });
    }
    
    private static void handleCommandReceive(long clientPtr, int command, byte[] data) {
        Log.d(TAG, "Command receive: clientPtr=" + clientPtr + ", command=" + command);
        
        final ReactApplicationContext ctx = currentContext != null ? currentContext.get() : null;
        if (ctx == null || !ctx.hasActiveReactInstance()) {
            Log.w(TAG, "Command receive ignored: no active React instance");
            return;
        }

        ctx.runOnUiQueueThread(new Runnable() {
            @Override
            public void run() {
                // Find our client by SDK pointer
                for (Map.Entry<Integer, ClientInfo> entry : clients.entrySet()) {
                    if (entry.getValue().sdkClientPtr == clientPtr) {
                        int ourClientPtr = entry.getKey();
                        
                        WritableMap params = Arguments.createMap();
                        params.putInt("clientId", ourClientPtr);
                        params.putInt("command", command);
                        params.putString("data", data != null ? new String(data) : "");
                        sendEvent("onCommandReceived", params);
                        break;
                    }
                }
            }
        });
    }

    @Override
    @NonNull
    public String getName() {
        return MODULE_NAME;
    }

    // Required by NativeEventEmitter
    @ReactMethod
    public void addListener(String eventName) {
        // Keep: Required for RN built in Event Emitter Calls
        Log.d(TAG, "addListener called for: " + eventName);
    }

    // Required by NativeEventEmitter
    @ReactMethod
    public void removeListeners(Integer count) {
        // Keep: Required for RN built in Event Emitter Calls
        Log.d(TAG, "removeListeners called with count: " + count);
    }

    @Override
    public void invalidate() {
        Log.d(TAG, "invalidate: Cleaning up " + clients.size() + " clients");
        for (Integer clientId : clients.keySet()) {
            try {
                ClientInfo info = clients.get(clientId);
                if (info != null && info.sdkClientPtr != 0 && jniApiClass != null) {
                    Method disconnectMethod = jniApiClass.getMethod("disconnect", long.class);
                    disconnectMethod.invoke(null, info.sdkClientPtr);
                    
                    Method destroyMethod = jniApiClass.getMethod("destroy", long.class);
                    destroyMethod.invoke(null, info.sdkClientPtr);
                    Log.d(TAG, "Cleanup: disconnected and destroyed client " + clientId);
                }
            } catch (Exception e) {
                Log.e(TAG, "Failed to cleanup client " + clientId, e);
            }
        }
        clients.clear();
        super.invalidate();
    }

    private static void sendEvent(String eventName, @Nullable WritableMap params) {
        final ReactApplicationContext ctx = currentContext != null ? currentContext.get() : null;
        if (ctx != null && ctx.hasActiveReactInstance()) {
            ctx.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class)
                .emit(eventName, params);
        }
    }

    /**
     * Test the bridge connection
     */
    @ReactMethod
    public void testBridge(Promise promise) {
        Log.d(TAG, "testBridge called");
        WritableMap result = Arguments.createMap();
        result.putBoolean("success", true);
        result.putString("message", "Bridge is working");
        result.putDouble("timestamp", (double) System.currentTimeMillis());
        promise.resolve(result);
    }

    /**
     * Create a P2P client for a device
     * JNIApi.create(String did, String serverParam) -> long
     */
    @ReactMethod
    public void clientCreate(String deviceId, Promise promise) {
        executor.execute(() -> {
            try {
                Log.d(TAG, "clientCreate called with deviceId: " + deviceId);
                
                if (jniApiClass == null) {
                    promise.reject("CREATE_ERROR", "JNIApi not available");
                    return;
                }
                
                // Check if this is a virtual UID that needs conversion
                String realClientId = deviceId;
                if (isVirtualId(deviceId)) {
                    Log.d(TAG, "Device ID " + deviceId + " looks like a virtual UID, converting...");
                    String[] conversionResult = convertVirtualUid(deviceId);
                    if (conversionResult != null && conversionResult[0] != null) {
                        realClientId = conversionResult[0];
                        Log.d(TAG, "Using converted client ID: " + realClientId);
                    } else {
                        Log.w(TAG, "Virtual UID conversion failed, trying with original ID");
                    }
                }
                
                // Call JNIApi.create(did, serverParam) with real client ID
                String serviceParam = getServiceParam(realClientId);  // Use real client ID prefix
                Method createMethod = jniApiClass.getMethod("create", String.class, String.class);
                Object result = createMethod.invoke(null, realClientId, serviceParam);
                long sdkClientPtr = (Long) result;
                
                Log.d(TAG, "JNIApi.create result: " + sdkClientPtr);
                
                // Note: pointer can be negative (unsigned long cast to signed) - only 0 is invalid
                if (sdkClientPtr == 0) {
                    promise.reject("CREATE_ERROR", "Failed to create client, result: " + sdkClientPtr);
                    return;
                }
                
                // Generate our own client ID and store mapping
                int ourClientPtr = nextClientPtr.getAndIncrement();
                
                ClientInfo clientInfo = new ClientInfo();
                clientInfo.deviceId = deviceId;
                clientInfo.realClientId = realClientId;  // Store the converted ID
                clientInfo.sdkClientPtr = sdkClientPtr;
                clients.put(ourClientPtr, clientInfo);
                
                Log.d(TAG, "Created client: ourPtr=" + ourClientPtr + ", sdkPtr=" + sdkClientPtr + ", realId=" + realClientId);
                promise.resolve(ourClientPtr);
            } catch (Exception e) {
                Log.e(TAG, "clientCreate failed", e);
                promise.reject("CREATE_ERROR", e.getMessage());
            }
        });
    }

    /**
     * Connect to camera via P2P
     * JNIApi.connect(long clientPtr, int timeout, String serverParam, int connectType)
     */
    @ReactMethod
    public void clientConnect(int clientPtr, boolean lanScan, String serverParam, int connectType, Promise promise) {
        executor.execute(() -> {
            try {
                Log.d(TAG, "clientConnect called for client: " + clientPtr);
                
                ClientInfo clientInfo = clients.get(clientPtr);
                if (clientInfo == null) {
                    promise.reject("CONNECT_ERROR", "Client not found");
                    return;
                }
                
                if (jniApiClass == null) {
                    promise.reject("CONNECT_ERROR", "JNIApi not available");
                    return;
                }
                
                // Emit connecting state
                WritableMap params = Arguments.createMap();
                params.putInt("clientId", clientPtr);
                params.putInt("state", 1); // CONNECTING
                sendEvent("onConnectionStateChanged", params);
                
                // Use realClientId for service param lookup (it has the correct prefix like VSTN)
                String realId = clientInfo.realClientId != null ? clientInfo.realClientId : clientInfo.deviceId;
                String param = (serverParam != null && !serverParam.isEmpty()) 
                    ? serverParam 
                    : getServiceParam(realId);
                
                // Default connectType to 126 (from V1 app) if not specified
                // connectType 126 = P2P/Relay combination that works reliably
                int actualConnectType = (connectType <= 0) ? 126 : connectType;
                
                // Call JNIApi.connect(sdkClientPtr, timeout, serverParam, connectType)
                // Signature: connect([long, int, class java.lang.String, int])
                Method connectMethod = jniApiClass.getMethod("connect", 
                    long.class, int.class, String.class, int.class);
                
                Log.d(TAG, "Calling JNIApi.connect(" + clientInfo.sdkClientPtr + ", 15, " + param + ", " + actualConnectType + ")");
                
                try {
                    connectMethod.invoke(null, clientInfo.sdkClientPtr, 15, param, actualConnectType);
                    Log.d(TAG, "JNIApi.connect returned normally");
                } catch (java.lang.reflect.InvocationTargetException ite) {
                    Log.e(TAG, "JNIApi.connect threw exception: " + ite.getCause(), ite.getCause());
                    throw ite;
                } catch (Throwable t) {
                    Log.e(TAG, "JNIApi.connect crashed: " + t.getMessage(), t);
                    throw t;
                }
                
                Log.d(TAG, "JNIApi.connect called - waiting for state callback");
                
                // The actual state will come via the ClientStateListener callback
                // For now, return immediately - the JS side will get updates via events
                promise.resolve(1); // CONNECTING
            } catch (Exception e) {
                Log.e(TAG, "clientConnect failed", e);
                
                WritableMap params = Arguments.createMap();
                params.putInt("clientId", clientPtr);
                params.putInt("state", 4); // CONNECT_FAILED
                params.putString("error", e.getMessage());
                sendEvent("onConnectionStateChanged", params);
                
                promise.reject("CONNECT_ERROR", e.getMessage());
            }
        });
    }

    /**
     * Get the current connection state of a client
     */
    @ReactMethod
    public void clientGetState(int clientPtr, Promise promise) {
        ClientInfo clientInfo = clients.get(clientPtr);
        if (clientInfo == null) {
            promise.resolve(5); // DISCONNECTED
            return;
        }
        
        WritableMap result = Arguments.createMap();
        result.putInt("state", clientInfo.isConnected ? 3 : 1); // 3=ONLINE, 1=CONNECTING (approx)
        result.putBoolean("isConnected", clientInfo.isConnected);
        result.putBoolean("isLoggedIn", clientInfo.isLoggedIn);
        promise.resolve(result);
    }

    /**
     * Login to camera
     * JNIApi.login(long clientPtr, String username, String password)
     */
    @ReactMethod
    public void clientLogin(int clientPtr, String username, String password, Promise promise) {
        executor.execute(() -> {
            try {
                Log.d(TAG, "clientLogin called with user: " + username);
                
                ClientInfo clientInfo = clients.get(clientPtr);
                if (clientInfo == null) {
                    promise.reject("LOGIN_ERROR", "Client not found");
                    return;
                }
                
                if (jniApiClass == null) {
                    promise.reject("LOGIN_ERROR", "JNIApi not available");
                    return;
                }
                
                // Call JNIApi.login(sdkClientPtr, username, password)
                Method loginMethod = jniApiClass.getMethod("login", 
                    long.class, String.class, String.class);
                
                Log.d(TAG, "Calling JNIApi.login(" + clientInfo.sdkClientPtr + ", " + username + ", ***)");
                
                boolean loginSuccess = false;
                try {
                    loginMethod.invoke(null, clientInfo.sdkClientPtr, username, password);
                    Log.d(TAG, "JNIApi.login returned normally");
                    loginSuccess = true;
                } catch (java.lang.reflect.InvocationTargetException ite) {
                    Throwable cause = ite.getCause();
                    Log.e(TAG, "JNIApi.login threw exception: " + cause, cause);
                    // Don't rethrow - return false instead to prevent crash
                    promise.resolve(false);
                    return;
                } catch (Throwable t) {
                    Log.e(TAG, "JNIApi.login crashed: " + t.getMessage(), t);
                    // Don't rethrow - return false instead to prevent crash
                    promise.resolve(false);
                    return;
                }
                
                if (loginSuccess) {
                    clientInfo.isLoggedIn = true;
                    clientInfo.username = username;
                    clientInfo.password = password;
                }
                
                Log.d(TAG, "JNIApi.login completed with result: " + loginSuccess);
                promise.resolve(loginSuccess);
            } catch (Exception e) {
                Log.e(TAG, "clientLogin failed", e);
                promise.reject("LOGIN_ERROR", e.getMessage());
            }
        });
    }

    /**
     * Send CGI command to camera
     * JNIApi.writeCgi(long clientPtr, String cgi, int timeout)
     */
    @ReactMethod
    public void clientWriteCgi(int clientPtr, String cgi, int timeout, Promise promise) {
        executor.execute(() -> {
            try {
                Log.d(TAG, "clientWriteCgi called with cgi: " + cgi);
                
                ClientInfo clientInfo = clients.get(clientPtr);
                if (clientInfo == null) {
                    promise.reject("CGI_ERROR", "Client not found");
                    return;
                }
                
                if (jniApiClass == null) {
                    promise.reject("CGI_ERROR", "JNIApi not available");
                    return;
                }
                
                // Call JNIApi.writeCgi(sdkClientPtr, cgi, timeout)
                Method writeCgiMethod = jniApiClass.getMethod("writeCgi", 
                    long.class, String.class, int.class);
                
                Log.d(TAG, "Calling JNIApi.writeCgi(" + clientInfo.sdkClientPtr + ", " + cgi + ", " + timeout + ")");
                writeCgiMethod.invoke(null, clientInfo.sdkClientPtr, cgi, timeout);
                
                Log.d(TAG, "JNIApi.writeCgi called successfully");
                promise.resolve(true);
            } catch (Exception e) {
                Log.e(TAG, "clientWriteCgi failed", e);
                promise.reject("CGI_ERROR", e.getMessage());
            }
        });
    }

    /**
     * Scan WiFi networks available to the camera
     */
    @ReactMethod
    public void scanWifi(int clientPtr, Promise promise) {
        // Use writeCgi to send wifi_scan command
        clientWriteCgi(clientPtr, "wifi_scan.cgi?user=admin&pwd=888888", 10, promise);
    }

    /**
     * Configure WiFi on camera
     */
    @ReactMethod
    public void configureWifi(int clientPtr, String ssid, String password, int authType, Promise promise) {
        String wifiCgi = String.format(
            "set_wifi.cgi?ssid=%s&key=%s&authtype=%d&enc=0&mode=0&wifienable=1",
            ssid, password, authType
        );
        clientWriteCgi(clientPtr, wifiCgi, 10, promise);
    }

    /**
     * Disconnect from camera
     * JNIApi.disconnect(long clientPtr)
     */
    @ReactMethod
    public void clientDisconnect(int clientPtr, Promise promise) {
        executor.execute(() -> {
            try {
                Log.d(TAG, "clientDisconnect called for: " + clientPtr);
                
                ClientInfo clientInfo = clients.get(clientPtr);
                if (clientInfo != null && jniApiClass != null) {
                    Method disconnectMethod = jniApiClass.getMethod("disconnect", long.class);
                    disconnectMethod.invoke(null, clientInfo.sdkClientPtr);
                    
                    clientInfo.isConnected = false;
                    clientInfo.isLoggedIn = false;
                    Log.d(TAG, "JNIApi.disconnect called successfully");
                }
                
                WritableMap params = Arguments.createMap();
                params.putInt("clientId", clientPtr);
                params.putInt("state", 5); // DISCONNECTED
                sendEvent("onConnectionStateChanged", params);
                
                promise.resolve(true);
            } catch (Exception e) {
                Log.e(TAG, "clientDisconnect failed", e);
                promise.reject("DISCONNECT_ERROR", e.getMessage());
            }
        });
    }

    /**
     * Destroy client and cleanup resources
     * JNIApi.destroy(long clientPtr)
     */
    @ReactMethod
    public void clientDestroy(int clientPtr, Promise promise) {
        executor.execute(() -> {
            try {
                Log.d(TAG, "clientDestroy called for: " + clientPtr);
                
                ClientInfo clientInfo = clients.get(clientPtr);
                if (clientInfo != null && jniApiClass != null) {
                    Method destroyMethod = jniApiClass.getMethod("destroy", long.class);
                    destroyMethod.invoke(null, clientInfo.sdkClientPtr);
                    Log.d(TAG, "JNIApi.destroy called successfully");
                }
                
                clients.remove(clientPtr);
                promise.resolve(true);
            } catch (Exception e) {
                Log.e(TAG, "clientDestroy failed", e);
                promise.reject("DESTROY_ERROR", e.getMessage());
            }
        });
    }

    /**
     * Get device information
     */
    @ReactMethod
    public void clientGetDeviceInfo(int clientPtr, Promise promise) {
        Log.d(TAG, "clientGetDeviceInfo called for ptr: " + clientPtr);
        executor.execute(() -> {
            try {
                ClientInfo clientInfo = clients.get(clientPtr);
                if (clientInfo == null) {
                    Log.w(TAG, "clientGetDeviceInfo: client not found for ptr " + clientPtr);
                    promise.reject("E_CLIENT_NOT_FOUND", "Client not found");
                    return;
                }

                Log.d(TAG, "Fetching device info for device: " + clientInfo.deviceId);
                
                WritableMap result = Arguments.createMap();
                result.putString("serialNumber", clientInfo.deviceId != null ? clientInfo.deviceId : "Unknown");
                result.putString("model", "VStarCam");
                result.putString("firmware", "Unknown");
                result.putString("manufacturer", "VStarCam");
                result.putBoolean("responsive", clientInfo.isConnected);
                
                WritableMap features = Arguments.createMap();
                features.putBoolean("ptz", true);
                features.putBoolean("audio", true);
                features.putBoolean("twoWayAudio", true);
                features.putBoolean("nightVision", true);
                features.putBoolean("motionDetection", true);
                features.putBoolean("wifi", true);
                result.putMap("features", features);

                Log.d(TAG, "Resolving device info for " + clientInfo.deviceId);
                promise.resolve(result);
            } catch (Throwable t) {
                Log.e(TAG, "clientGetDeviceInfo crashed: " + t.getMessage(), t);
                promise.reject("E_GET_INFO_FAILED", t.getMessage());
            }
        });
    }

    /**
     * Start video stream by sending livestream.cgi command
     * Resolution: 1=high, 2=general, 4=low, 100=superHD
     */
    @ReactMethod
    public void startVideoStream(int clientPtr, int streamType, Promise promise) {
        boolean success = sendCgiInternal(clientPtr, 
            String.format("livestream.cgi?streamid=10&substream=%d", streamType), 5);
        
        if (success) {
            WritableMap response = Arguments.createMap();
            response.putBoolean("success", true);
            response.putString("message", "Video stream started");
            response.putInt("resolution", streamType);
            promise.resolve(response);
        } else {
            promise.reject("E_VIDEO_START_FAILED", "Failed to send livestream.cgi");
        }
    }

    /**
     * Helper for internal components to send CGI commands without a Promise
     */
    public static boolean sendCgiInternal(int clientPtr, String cgi, int timeout) {
        try {
            ClientInfo clientInfo = clients.get(clientPtr);
            if (clientInfo == null || clientInfo.sdkClientPtr == 0) {
                Log.e(TAG, "sendCgiInternal: Client not connected for ptr " + clientPtr);
                return false;
            }

            // Append login info if missing
            String finalCgi = cgi;
            if (!cgi.contains("loginuse=")) {
                String sep = cgi.contains("?") ? "&" : "?";
                finalCgi = cgi + sep + "loginuse=" + (clientInfo.username != null ? clientInfo.username : "admin") + 
                                "&loginpas=" + (clientInfo.password != null ? clientInfo.password : "");
            }

            Log.d(TAG, "sendCgiInternal: " + finalCgi);
            
            // Invoke JNIApi.writeCgi
            // Since we need to access jniApiClass which is not static, we use reflection on the instance if available
            // but for simplicity, we can just grab the class by name again or make the class ref static.
            Class<?> jniClass = Class.forName("com.vstarcam.JNIApi");
            Method writeCgiMethod = jniClass.getMethod("writeCgi", long.class, String.class, int.class);
            Object result = writeCgiMethod.invoke(null, clientInfo.sdkClientPtr, finalCgi, timeout);
            
            int res = (Integer) result;
            return res == 0; // 0 usually means success in VStarCam JNI
        } catch (Exception e) {
            Log.e(TAG, "sendCgiInternal failed", e);
            return false;
        }
    }

    /**
     * Stop video stream
     */
    @ReactMethod
    public void stopVideoStream(int clientPtr, Promise promise) {
        Log.d(TAG, "stopVideoStream called");
        try {
            ClientInfo clientInfo = clients.get(clientPtr);
            if (clientInfo == null || clientInfo.sdkClientPtr == 0) {
                promise.reject("E_NOT_CONNECTED", "Client not connected");
                return;
            }

            // Send livestream.cgi command to stop streaming
            // streamid=16 stops the stream
            String cgi = String.format(
                "livestream.cgi?streamid=16&substream=0&loginuse=%s&loginpas=%s",
                clientInfo.username != null ? clientInfo.username : "admin",
                clientInfo.password != null ? clientInfo.password : ""
            );
            
            Log.d(TAG, "Sending livestream stop command: " + cgi);
            
            Method writeCgiMethod = jniApiClass.getMethod("writeCgi", long.class, String.class, int.class);
            Object result = writeCgiMethod.invoke(null, clientInfo.sdkClientPtr, cgi, 5);
            
            Log.d(TAG, "livestream stop command sent, result: " + result);
            
            WritableMap response = Arguments.createMap();
            response.putBoolean("success", true);
            response.putString("message", "Video stream stopped");
            promise.resolve(response);
            
        } catch (Exception e) {
            Log.e(TAG, "stopVideoStream error", e);
            promise.reject("E_VIDEO_STOP_FAILED", e.getMessage(), e);
        }
    }

    /**
     * Check connection mode
     * JNIApi.checkMode(long clientPtr)
     */
    @ReactMethod
    public void clientCheckMode(int clientPtr, Promise promise) {
        Log.d(TAG, "clientCheckMode called for: " + clientPtr);
        try {
            ClientInfo clientInfo = clients.get(clientPtr);
            
            int mode = 0;
            // Note: sdkClientPtr can be negative (unsigned to signed cast) - only check != 0
            if (clientInfo != null && jniApiClass != null && clientInfo.sdkClientPtr != 0) {
                try {
                    Method checkModeMethod = jniApiClass.getMethod("checkMode", long.class);
                    Object result = checkModeMethod.invoke(null, clientInfo.sdkClientPtr);
                    mode = (Integer) result;
                    Log.d(TAG, "JNIApi.checkMode result: " + mode + " (0=none, 1=P2P, 2=relay, 3=sock)");
                } catch (Exception e) {
                    Log.e(TAG, "checkMode JNI call failed", e);
                }
            } else {
                Log.w(TAG, "checkMode skipped: clientInfo=" + clientInfo + 
                    ", jniApiClass=" + (jniApiClass != null) + 
                    ", sdkPtr=" + (clientInfo != null ? clientInfo.sdkClientPtr : "null"));
            }
            
            // Mode > 0 means connected (1=P2P, 2=Relay, 3=Socket)
            boolean connected = mode > 0;
            if (connected && clientInfo != null) {
                clientInfo.isConnected = true;
            }
            
            WritableMap result = Arguments.createMap();
            result.putBoolean("success", connected);
            result.putInt("mode", mode);
            result.putDouble("sessionHandle", clientInfo != null ? clientInfo.sdkClientPtr : -1);
            Log.d(TAG, "clientCheckMode returning: success=" + connected + ", mode=" + mode);
            promise.resolve(result);
        } catch (Exception e) {
            Log.e(TAG, "clientCheckMode failed", e);
            promise.reject("MODE_ERROR", e.getMessage());
        }
    }
    
    /**
     * Check if native library is loaded
     */
    @ReactMethod
    public void isNativeLibraryLoaded(Promise promise) {
        promise.resolve(isNativeLibraryLoaded);
    }
    
    /**
     * Get SDK version info
     */
    @ReactMethod
    public void getSdkVersion(Promise promise) {
        WritableMap result = Arguments.createMap();
        result.putString("version", "1.0.47");
        result.putBoolean("nativeLoaded", isNativeLibraryLoaded);
        result.putString("nativeLib", "OKSMARTPPCS");
        result.putBoolean("p2pInitialized", isP2PInitialized);
        result.putBoolean("jniClassFound", jniApiClass != null);
        if (jniApiClass != null) {
            result.putString("jniClassName", jniApiClass.getName());
        }
        promise.resolve(result);
    }
}
