package pendo.io.reactnative;

import android.app.Activity;
import android.content.Context;
import android.util.Log;
import android.view.View;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;

import com.facebook.react.bridge.Promise;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactMethod;
import com.facebook.react.bridge.ReadableArray;
import com.facebook.react.bridge.ReadableMap;
import com.facebook.react.bridge.WritableMap;
import com.facebook.react.modules.core.DeviceEventManagerModule;
import com.facebook.react.uimanager.UIManagerModule;
import com.facebook.react.uimanager.util.ReactFindViewUtil;


import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;

import sdk.pendo.io.Pendo;
import sdk.pendo.io.PendoPhasesCallbackInterface;
import sdk.pendo.io.sdk.react.IReactNativeBridge;
import sdk.pendo.io.sdk.react.IReactNativeEventsImp;

public class ReactNativePendoModule extends ReactContextBaseJavaModule implements IReactNativeBridge {

    private static final String TAG = "ReactNativePendoModule";
    private static final String NAME = "ReactNativePendo";
    private static final String VISITOR_ID = "visitorId";
    private static final String ACCOUNT_ID = "accountId";
    private static final String VISITOR_DATA = "visitorData";
    private static final String ACCOUNT_DATA = "accountData";
    private static final String OPTIONS = "options";
    private static final String ENVIRONMENT_NAME = "environmentName";
    private static final String DEBUG_MODE = "debugMode";
    private static final String REACT_NATIVE_VERSION = "reactNativeVersion";

    private static final String PLUGIN_VERSION = "pluginVersion";
    private static final String USE_CLICKABLE_ELEMENTS_FROM_JS = "useClickableElementsFromJS";
    private static final String ERROR_MESSAGE = "errorMessage";
    private static final String SPACING = " - ";
    private static final String ON_SCREEN_CONTENT_CHANGED = "onScreenContentChange";
    private static final String ON_MODAL_STATE_VISIBLE = "onModalStateVisible";
    private static final String ON_MODAL_STATE_HIDDEN = "onModalStateHidden";
    private static final String ON_INIT_COMPLETE = "onInitComplete";
    private static final String ON_INIT_FAILED = "onInitFailed";

    private static final int LATCH_TIMEOUT_SECONDS = 2;
    private static final int REACT_NATIVE_NAVIGATION = 1;
    private static final int REACT_NAVIGATION = 2;
    private static final int EXPO_ROUTER = 5;

    private boolean isDebugModeEnabled = false;
    private Boolean isFabricEnabled = null; // Cache for Fabric architecture detection

    // Cached UIManager references for view resolution
    private UIManagerModule cachedPaperUIManager = null;
    private boolean paperUIManagerInitialized = false;

    // Cached Fabric reflection artifacts
    private Method cachedGetUIManagerMethod = null;
    private Method cachedResolveViewMethod = null;
    private int cachedFabricType = -1;
    private Object cachedFabricUIManager = null;
    private boolean fabricReflectionInitialized = false;

    // Event callback for IReactNativeBridge
    @Nullable
    private IReactNativeEventsImp rnEventsImp = null;

    private enum LogLevel {
        DEBUG;
    }

    public ReactNativePendoModule(ReactApplicationContext reactContext) {
        super(reactContext);
    }

    @NonNull
    @Override
    public String getName() {
        return NAME;
    }

    public List<Integer> toIntList(@Nullable ReadableArray readableArray) {
        List<Integer> result = new ArrayList<>();
        if (readableArray == null || readableArray.size() == 0) {
            return result;
        }
        try {
            List<Object> list = readableArray.toArrayList();
            for (int index = 0; index < list.size(); index++) {
                Object value = list.get(index);
                if (value instanceof Integer) {
                    result.add((Integer) value);
                } else if (value instanceof Double) {
                    result.add(((Double) value).intValue());
                } else if (value instanceof Float) {
                    result.add(((Float) value).intValue());
                }
            }
        } catch (Exception e) {
            sendFailureInfo(createErrorMessageMap("toIntList", e), false);
        }
        return result;
    }


    /**
     * toMap converts a {@link ReadableMap} into a HashMap<String, String>.
     *
     * @param readableMap The ReadableMap to be converted.
     * @return A HashMap containing the data that was in the ReadableMap.
     */
    private Map<String, Object> toMap(@Nullable ReadableMap readableMap) {
        Map<String, Object> map = new HashMap<>();
        if (readableMap == null) {
            return map;
        }
        try {
            map = readableMap.toHashMap();
        } catch (Exception e) {
            sendFailureInfo(createErrorMessageMap("toMap", e), false);
        }
        return map;
    }

    private static boolean isNullOrWhiteSpace(String value) {
        return value == null || value.trim().isEmpty();
    }

    private static Map<String, Object> createErrorMessageMap(String errorSendFromMethodNamed, Exception e) {
        Map<String, Object> errorMap = new HashMap<>();
        if (e != null) {
            errorMap.put(ERROR_MESSAGE, errorSendFromMethodNamed + SPACING + e.getMessage());
        }
        return errorMap;
    }

    /**
     * Send indication to the native SDK when something went wrong in the RN plugin
     *
     * @param userInfo            a map to pass in the relevant data (i.e error message).
     * @param shouldSendErrorToBE a boolean indicating whether the error should be sent to the backend.
     */
    private void sendFailureInfo(Map<String, Object> userInfo, boolean shouldSendErrorToBE) {
        if (rnEventsImp != null) {
            rnEventsImp.sendFailureInfo(userInfo, shouldSendErrorToBE);
        } else {
            printDebugLog(TAG, "sendFailureInfo: callback not registered, failure info dropped: " + userInfo, LogLevel.DEBUG);
        }
    }

    /*
        Errors sent from JS part of the plugin
     */
    @ReactMethod
    public void sendFailureInfo(ReadableMap userInfo, boolean shouldSendErrorToBE) {
        sendFailureInfo(toMap(userInfo), shouldSendErrorToBE);
    }

    @ReactMethod
    public void setup(@NonNull String appKey, int navigationLibrary, @Nullable ReadableMap options) {
        Context appContext = getReactApplicationContext().getCurrentActivity();
        //Fallback
        if(appContext == null) {
            appContext = getReactApplicationContext();
        }
        if (appContext != null) {
            Map<String, Object> additionalOptions = new HashMap();
            additionalOptions.put(Pendo.PendoOptions.FRAMEWORK, Pendo.PendoOptions.Framework.REACT_NATIVE);
            additionalOptions.put(Pendo.PendoOptions.REACT_NATIVE_BRIDGE, this);
            switch (navigationLibrary) {
                case REACT_NATIVE_NAVIGATION:
                    additionalOptions.put(Pendo.PendoOptions.FRAMEWORK_TYPE, Pendo.PendoOptions.FrameworkType.REACT_NATIVE_NAVIGATION);
                    break;
                case REACT_NAVIGATION:
                case EXPO_ROUTER:
                    additionalOptions.put(Pendo.PendoOptions.FRAMEWORK_TYPE, Pendo.PendoOptions.FrameworkType.REACT_NAVIGATION);
                    break;
                default:
                    additionalOptions.put(Pendo.PendoOptions.FRAMEWORK_TYPE, Pendo.PendoOptions.FrameworkType.TRACK);
            }
            Pendo.PendoOptions.Builder pendoOptionsBuilder = new Pendo.PendoOptions.Builder();
            String pluginVersion = "";
            String reactNativeVersion = "";

            if (options != null) {
                Map<String, Object> optionsMap = toMap(options);
                if (optionsMap.containsKey(ENVIRONMENT_NAME)) {
                    pendoOptionsBuilder.setEnvironmentName((String) optionsMap.get(ENVIRONMENT_NAME));
                }
                if (optionsMap.containsKey(USE_CLICKABLE_ELEMENTS_FROM_JS)) {
                    pendoOptionsBuilder.setUseClickableElementsFromJS((Boolean) optionsMap.get(USE_CLICKABLE_ELEMENTS_FROM_JS));
                }
                if (optionsMap.containsKey(REACT_NATIVE_VERSION)) {
                    reactNativeVersion = (String) optionsMap.get(REACT_NATIVE_VERSION);
                    additionalOptions.put(Pendo.PendoOptions.FRAMEWORK_VERSION, reactNativeVersion);
                }
                if(optionsMap.containsKey(PLUGIN_VERSION)) {
                  pluginVersion = (String) optionsMap.get(PLUGIN_VERSION);
                  additionalOptions.put(PLUGIN_VERSION, pluginVersion);
                }
            }
            pendoOptionsBuilder.setAdditionalOptions(additionalOptions);
            // Detect architecture and log the result
            boolean fabricEnabled = detectFabricArchitecture();
            printDebugLog(TAG, "Pendo Plugin: " + pluginVersion + ", React Native: " + reactNativeVersion + ", New Architecture: " + fabricEnabled, LogLevel.DEBUG);
            Pendo.setup(appContext, appKey, pendoOptionsBuilder.build(), new PendoPhasesCallbackInterface() {
                @Override
                public void onInitComplete() {
                     printDebugLog(TAG, "onInitComplete", LogLevel.DEBUG);
                    sendEvent(ON_INIT_COMPLETE, null);
                }

                @Override
                public void onInitFailed() {
                    printDebugLog(TAG, "onInitFailed", LogLevel.DEBUG);
                    sendEvent(ON_INIT_FAILED, null);
                }
            });
        } else {
            printDebugLog(TAG, "Cannot call setup() due to application context being null", LogLevel.DEBUG);
        }
    }

    /**
     * Ends current session, resets all managers and listeners, and does not start a new session
     * until switchVisitor is called
     */
    @ReactMethod
    public void endSession() {
        Pendo.endSession();
    }

    /**
     * Ends current session, resets all managers and listeners, and starts a new session
     * with the new parameters.
     *
     * @param visitorId
     * @param accountId
     * @param visitorData
     * @param accountData
     */
    @ReactMethod
    public void startSession(String visitorId, String accountId, @Nullable ReadableMap visitorData, @Nullable ReadableMap accountData) {
        Pendo.startSession(visitorId, accountId, toMap(visitorData), toMap(accountData));
    }

    @ReactMethod
    public void setVisitorData(@Nullable ReadableMap visitorData) {
        Pendo.setVisitorData(toMap(visitorData));
    }

    @ReactMethod
    public void setAccountData(@Nullable ReadableMap accountData) {
        Pendo.setAccountData(toMap(accountData));
    }

    /**
     * Track an event that has happened in your application.
     *
     * @param event      - the name of the event.
     * @param properties - properties connected to the event.
     */
    @ReactMethod
    public void track(@NonNull String event, @Nullable ReadableMap properties) {
        if (isNullOrWhiteSpace(event)) {
            throw new IllegalArgumentException("Event name is required");
        }
        Pendo.track(event, toMap(properties));
    }

    /**
     * Pause showing guides during this session
     */
    @ReactMethod
    public void pauseGuides(boolean dismissGuides) {
        Pendo.pauseGuides(dismissGuides);
    }

    /**
     * Resume showing guides during this session
     */
    @ReactMethod
    public void resumeGuides() {
        Pendo.resumeGuides();
    }

    /**
     * Dismiss all visible guides
     */
    @ReactMethod
    public void dismissVisibleGuides() {
        Pendo.dismissVisibleGuides();
    }

    /**
     * Get visitor Id
     */
    @ReactMethod
    public void getVisitorId(Promise promise) {
        try {
            promise.resolve(Pendo.getVisitorId());
        } catch (Exception e) {
            promise.reject(e);
        }
    }

    /**
     * Get account Id
     */
    @ReactMethod
    public void getAccountId(Promise promise) {
        try {
            promise.resolve(Pendo.getAccountId());
        } catch (Exception e) {
            promise.reject(e);
        }
    }

    /**
     * Get device Id
     */
    @ReactMethod
    public void getDeviceId(Promise promise) {
        try {
            promise.resolve(Pendo.getDeviceId());
        } catch (Exception e) {
            promise.reject(e);
        }
    }

    /**
     * Call this to let the native side know a screen has changed in the JS.
     *
     * @param screenName - the name of the new screen.
     * @param rootTags   - the list of root react tags
     * @param info       - information about the screen including type of structure, clickable elements etc...
     */
    @ReactMethod
    public void screenChanged(String screenName, ReadableArray rootTags, ReadableArray clickableElements, ReadableMap info) {
        try {
            if (screenName != null && rootTags.size() > 0 && rnEventsImp != null) {
                printDebugLog(TAG, "screenChanged, screenName: " + screenName + " rootTag: " + rootTags.toString() +
                        " clickableElements: " + clickableElements.toString() + " info: " + info, LogLevel.DEBUG);
                        rnEventsImp.newScreenIdentified(screenName, toIntList(rootTags), toMap(info), clickableElements.toArrayList());
            } else {
                printDebugLog(TAG, "screenChanged, either screenName is null or rootTags is empty or rnEventsImp is null", LogLevel.DEBUG);
            }
        } catch (Exception e) {
            sendFailureInfo(createErrorMessageMap("screenChanged", e), false);
        }
    }

    /**
     * Enable/disable debug logs
     */
    @ReactMethod
    public void setDebugMode(boolean isDebugEnabled) {
        isDebugModeEnabled = isDebugEnabled;
        Pendo.setDebugMode(isDebugEnabled);
    }

    /**
     * Finding view with provided nativeID and ivokes native sendClickAnalytic API
     */
    @ReactMethod
    public void sendClickAnalytic(String nativeID) {
        Activity activity = getCurrentActivity();
        if (activity != null) {
            activity.runOnUiThread(new Runnable() {
                @Override
                public void run() {
                    View rootView = activity.getWindow().getDecorView().getRootView();
                    View requiredView = ReactFindViewUtil.findView(rootView, nativeID);
                    Pendo.sendClickAnalytic(requiredView);
                }
            });
        } else {
            printDebugLog(TAG, "Cannot call sendClickAnalytic() due to activity being null", LogLevel.DEBUG);
        }
    }

    /**
     * This method wraps usage of the Log class depend on the isDebugModeEnabled flag
     *
     * @param tag      to define for log message
     * @param message  to print
     * @param logLevel to define for log message
     */
    private void printDebugLog(String tag, String message, LogLevel logLevel) {
        if (!isDebugModeEnabled) {
            return;
        }
        switch (logLevel) {
            case DEBUG:
                Log.d(tag, message);
                break;
            default:
                Log.d(tag, message);
        }
    }

    /**
     * Detects if Fabric (new architecture) is enabled at runtime.
     * Uses reflection to check for Fabric classes without compile-time dependencies.
     * Caches the result to avoid repeated expensive reflection operations.
     *
     * @return true if Fabric is available and enabled, false for Paper (old architecture)
     */
    private boolean detectFabricArchitecture() {
        // Return cached result if already detected
        if (isFabricEnabled != null) {
            return isFabricEnabled;
        }

        ReactApplicationContext reactContext = getReactApplicationContext();
        if (reactContext == null) {
            printDebugLog(TAG, "Cannot detect architecture: ReactApplicationContext is null", LogLevel.DEBUG);
            return false;
        }

        boolean fabricDetected = false;
        String uiManagerClassName = "Unknown";

        try {
            // Try to load Fabric-specific classes (available in RN 0.68+)
            Class<?> uiManagerHelperClass = Class.forName("com.facebook.react.uimanager.UIManagerHelper");
            Class<?> uiManagerTypeClass = Class.forName("com.facebook.react.uimanager.common.UIManagerType");

            // Get the FABRIC constant (value 2)
            int fabricType = uiManagerTypeClass.getField("FABRIC").getInt(null);

            // Try to get Fabric UIManager (method signature uses ReactContext, not ReactApplicationContext)
            java.lang.reflect.Method getUIManagerMethod = uiManagerHelperClass.getMethod(
                "getUIManager",
                ReactContext.class,
                int.class
            );
            Object fabricUIManager = getUIManagerMethod.invoke(null, reactContext, fabricType);

            // If we got a Fabric UIManager instance, Fabric is available
            fabricDetected = (fabricUIManager != null);

            if (fabricUIManager != null) {
                uiManagerClassName = fabricUIManager.getClass().getSimpleName();
            }

            printDebugLog(TAG, "Architecture detected: " + (fabricDetected ? "Fabric (New)" : "Paper (Old)") +
                         " - UIManager class: " + uiManagerClassName,
                         LogLevel.DEBUG);

        } catch (ClassNotFoundException e) {
            // Fabric classes don't exist - this is expected on RN < 0.68
            printDebugLog(TAG, "Architecture detected: Paper (Old) - Fabric classes not found (RN < 0.68)",
                         LogLevel.DEBUG);
            fabricDetected = false;
        } catch (Exception e) {
            printDebugLog(TAG, "Architecture detection failed, defaulting to Paper: " + e.getMessage(),
                         LogLevel.DEBUG);
            fabricDetected = false;
        }

        // Cache the result
        isFabricEnabled = fabricDetected;
        return fabricDetected;
    }

    /**
     * Public getter for architecture status.
     * @return true if Fabric (new architecture), false if Paper (old architecture)
     */
    public boolean isFabricEnabled() {
        if (isFabricEnabled == null) {
            detectFabricArchitecture();
        }
        return isFabricEnabled != null ? isFabricEnabled : false;
    }

    /**
     * Stub method that implemented for iOS
     */
    @ReactMethod
    public void shouldScanForDynamicElements(Boolean scanDynamicElements) {
    }

    /**
     * Trigger a screenContent scan to manually detect changes on screen
     */
    @ReactMethod
    public void screenContentChanged() {
        this.sendEvent(ON_SCREEN_CONTENT_CHANGED, null);
    }

    /**
     * Trigger a modalStateChange scan to automatically detect changes on screen when modal is shown or hidden
     */
    @ReactMethod
    public void modalStateChanged(boolean isVisible) {
        if (isVisible) {
            this.sendEvent(ON_MODAL_STATE_VISIBLE, null);
        } else {
            this.sendEvent(ON_MODAL_STATE_HIDDEN, null);
        }
    }

    private void sendEvent(String eventName, @Nullable WritableMap params) {
        ReactApplicationContext reactContext = this.getReactApplicationContext();
        try {
            reactContext.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class).emit(eventName, params);
        } catch (Exception e) {
            sendFailureInfo(createErrorMessageMap("sendEvent", e), false);
        }
    }

    // Required for rn built in EventEmitter Calls.
    @ReactMethod
    public void addListener(String eventName) {
    }

    @ReactMethod
    public void removeListeners(Integer count) {
    }

    /**
     * Check if any of the views with the provided react tags are visible.
     * This is a blocking synchronous method that mirrors the iOS implementation.
     *
     * @param reactTags - array of react tag numbers to check for visibility
     * @return true if at least one view is visible, false otherwise
     */
    @ReactMethod(isBlockingSynchronousMethod = true)
    public Boolean viewsVisible(ReadableArray reactTags) {
        List<Integer> tags = toIntList(reactTags);
        if (tags.isEmpty()) {
            return false;
        }

        return verifyViews(tags);
    }

    /**
     * Helper method to verify if any of the views are visible.
     * Runs on UI thread and checks visibility conditions for each view.
     * Supports both old architecture (Paper) and new architecture (Fabric).
     *
     * @param reactTags - list of react tag integers to check
     * @return true if at least one view passes all visibility checks, false otherwise
     */
    private Boolean verifyViews(List<Integer> reactTags) {
        final AtomicBoolean isVisible = new AtomicBoolean(false);
        final CountDownLatch latch = new CountDownLatch(1);

        Activity activity = getCurrentActivity();
        if (activity == null) {
            printDebugLog(TAG, "viewsVisible: activity is null", LogLevel.DEBUG);
            return false;
        }

        activity.runOnUiThread(new Runnable() {
            @Override
            public void run() {
                try {
                    ReactApplicationContext reactContext = getReactApplicationContext();
                    if (reactContext == null) {
                        printDebugLog(TAG, "viewsVisible: ReactApplicationContext is null", LogLevel.DEBUG);
                        latch.countDown();
                        isVisible.set(false);
                        return;
                     }
                    // Determine architecture once before the loop
                    boolean useFabric = isFabricEnabled();
                    printDebugLog(TAG, "viewsVisible: Using " + (useFabric ? "Fabric (New)" : "Paper (Old)") + " architecture", LogLevel.DEBUG);

                    // Check each react tag for visibility
                    for (Integer reactTag : reactTags) {
                        View view = null;

                        if (useFabric) {
                            // Use Fabric (new architecture) via cached reflection
                            view = tryResolveFabricView(reactContext, reactTag);
                        } else {
                            // Use Paper (old architecture) with cached UIManager
                            view = tryResolvePaperView(reactContext, reactTag);
                        }

                        // Check all visibility conditions (matching iOS implementation):
                        // - View exists (not null)
                        // - View is attached to window
                        // - View alpha > 0 (not transparent)
                        // - View is visible (not GONE or INVISIBLE)
                        // - View has non-zero dimensions

                        // Check each condition individually and log why it failed
                        if (view == null) {
                            printDebugLog(TAG, "viewsVisible: View is null for tag " + reactTag, LogLevel.DEBUG);
                            continue;
                        }

                        if (!view.isAttachedToWindow()) {
                            printDebugLog(TAG, "viewsVisible: View not attached to window for tag " + reactTag, LogLevel.DEBUG);
                            continue;
                        }

                        if (view.getAlpha() <= 0) {
                            printDebugLog(TAG, "viewsVisible: View alpha is " + view.getAlpha() + " for tag " + reactTag, LogLevel.DEBUG);
                            continue;
                        }

                        if (view.getVisibility() != View.VISIBLE) {
                            String visibilityStr = view.getVisibility() == View.GONE ? "GONE" :
                                                  view.getVisibility() == View.INVISIBLE ? "INVISIBLE" : "UNKNOWN";
                            printDebugLog(TAG, "viewsVisible: View visibility is " + visibilityStr + " for tag " + reactTag, LogLevel.DEBUG);
                            continue;
                        }

                        if (view.getWidth() <= 0 || view.getHeight() <= 0) {
                            printDebugLog(TAG, "viewsVisible: View dimensions are " + view.getWidth() + "x" + view.getHeight() + " for tag " + reactTag, LogLevel.DEBUG);
                            continue;
                        }

                        // All checks passed - view is visible
                        isVisible.set(true);
                        printDebugLog(TAG, "viewsVisible: Found visible view with tag " + reactTag, LogLevel.DEBUG);
                        break; // Found at least one visible view, can return true
                    }
                } catch (Exception e) {
                    printDebugLog(TAG, "viewsVisible: Error in verifyViews: " + e.getMessage(), LogLevel.DEBUG);
                    // On error, assume views are visible to avoid false negatives
                    isVisible.set(true);
                } finally {
                    latch.countDown();
                }
            }
        });

        try {
            // Wait for UI thread with timeout
            if (!latch.await(LATCH_TIMEOUT_SECONDS, TimeUnit.SECONDS)) {
                printDebugLog(TAG, "viewsVisible: Timeout waiting for UI thread", LogLevel.DEBUG);
                // If we don't get a response in time, assume views are visible to avoid false negatives
                isVisible.set(true);
            }
        } catch (InterruptedException e) {
            printDebugLog(TAG, "viewsVisible: Interrupted while waiting for UI thread", LogLevel.DEBUG);
            Thread.currentThread().interrupt(); // Restore interrupt status
            // If we are interrupted, assume views are visible to avoid false negatives
            isVisible.set(true);
        }

        return isVisible.get();
    }

    /**
     * Try to resolve a view using Paper UIManager with caching.
     * Caches the UIManagerModule instance to avoid repeated getNativeModule calls.
     *
     * @param reactContext - the React application context
     * @param reactTag - the React tag to resolve
     * @return the resolved View, or null if not found
     */
    private View tryResolvePaperView(ReactApplicationContext reactContext, int reactTag) {
        try {
            // Initialize cached Paper UIManager on first use
            if (!paperUIManagerInitialized) {
                cachedPaperUIManager = reactContext.getNativeModule(UIManagerModule.class);
                paperUIManagerInitialized = true;
                printDebugLog(TAG, "viewsVisible: Initialized Paper UIManagerModule cache", LogLevel.DEBUG);
            }

            if (cachedPaperUIManager != null) {
                View view = cachedPaperUIManager.resolveView(reactTag);
                if (view != null) {
                    printDebugLog(TAG, "viewsVisible: Resolved view using cached Paper UIManagerModule for tag " + reactTag, LogLevel.DEBUG);
                }
                return view;
            }
        } catch (Exception e) {
            printDebugLog(TAG, "viewsVisible: Paper UIManager failed: " + e.getMessage(), LogLevel.DEBUG);
        }
        return null;
    }

    /**
     * Try to resolve a view using Fabric UIManager via cached reflection.
     * Caches reflection artifacts (Method objects, UIManager instance) to avoid
     * repeated expensive reflection operations, ensuring compatibility with
     * React Native 0.66-0.67 where Fabric classes do not exist.
     * @param reactContext - the React application context
     * @param reactTag - the React tag to resolve
     * @return the resolved View, or null if not found or Fabric is not available
     */
    private View tryResolveFabricView(ReactApplicationContext reactContext, int reactTag) {
        try {
            // Initialize Fabric reflection artifacts on first use
            if (!fabricReflectionInitialized) {
                initializeFabricReflection(reactContext);
            }

            // If initialization failed (Fabric not available), return null
            if (cachedGetUIManagerMethod == null || cachedFabricType < 0) {
                return null;
            }

            // Get or refresh the Fabric UIManager instance
            if (cachedFabricUIManager == null) {
                cachedFabricUIManager = cachedGetUIManagerMethod.invoke(null, reactContext, cachedFabricType);
                if (cachedFabricUIManager != null && cachedResolveViewMethod == null) {
                    cachedResolveViewMethod = cachedFabricUIManager.getClass().getMethod("resolveView", int.class);
                    printDebugLog(TAG, "viewsVisible: Cached Fabric resolveView method", LogLevel.DEBUG);
                }
            }

            if (cachedFabricUIManager != null && cachedResolveViewMethod != null) {
                View view = (View) cachedResolveViewMethod.invoke(cachedFabricUIManager, reactTag);
                if (view != null) {
                    printDebugLog(TAG, "viewsVisible: Resolved view using cached Fabric UIManager for tag " + reactTag, LogLevel.DEBUG);
                }
                return view;
            }
        } catch (Exception e) {
            printDebugLog(TAG, "viewsVisible: Failed to resolve view via Fabric: " + e.getMessage(), LogLevel.DEBUG);
        }
        return null;
    }

    /**
     * Register for events from the native SDK.
     * Implements IReactNativeBridge interface.
     *
     * @param callback - the callback to receive events, can be null
     */
    @Override
    public void registerForEvents(@Nullable IReactNativeEventsImp callback) {
        this.rnEventsImp = callback;
        printDebugLog(TAG, "registerForEvents: callback " + (callback != null ? "registered" : "unregistered"), LogLevel.DEBUG);
    }

    /**
     * Initialize Fabric reflection artifacts (classes, methods, constants).
     * Called once on first Fabric view resolution attempt.
     *
     * @param reactContext - the React application context
     */
    private void initializeFabricReflection(ReactApplicationContext reactContext) {
        fabricReflectionInitialized = true;
        try {
            // Load Fabric-specific classes (available in RN 0.68+)
            Class<?> uiManagerHelperClass = Class.forName("com.facebook.react.uimanager.UIManagerHelper");
            Class<?> uiManagerTypeClass = Class.forName("com.facebook.react.uimanager.common.UIManagerType");

            // Cache the FABRIC constant
            cachedFabricType = uiManagerTypeClass.getField("FABRIC").getInt(null);

            // Cache the getUIManager method (uses ReactContext, not ReactApplicationContext)
            cachedGetUIManagerMethod = uiManagerHelperClass.getMethod(
                "getUIManager",
                ReactContext.class,
                int.class
            );

            // Try to get and cache the Fabric UIManager instance
            cachedFabricUIManager = cachedGetUIManagerMethod.invoke(null, reactContext, cachedFabricType);

            if (cachedFabricUIManager != null) {
                // Cache the resolveView method from the UIManager instance
                cachedResolveViewMethod = cachedFabricUIManager.getClass().getMethod("resolveView", int.class);
            }

            printDebugLog(TAG, "viewsVisible: Initialized Fabric reflection cache", LogLevel.DEBUG);

        } catch (ClassNotFoundException e) {
            // Fabric classes don't exist (expected on RN < 0.68)
            printDebugLog(TAG, "viewsVisible: Fabric classes not available (expected for RN < 0.68, using Paper)", LogLevel.DEBUG);
        } catch (Exception e) {
            printDebugLog(TAG, "viewsVisible: Failed to initialize Fabric reflection: " + e.getMessage(), LogLevel.DEBUG);
        }
    }
}
