@import Pendo;


#import "ReactNativePendo.h"

typedef void(^CompletionBlock)(void);

//Enum duplication: the same enum is defined inside ScreenManagerRN but can't be imported here
typedef NS_ENUM(NSInteger, PNDScanRequestReason) {
    PNDScanRequestReasonUnknown = 0,
    PNDScanRequestReasonCapture,
    PNDScanRequestReasonReturnFromAlert,
    PNDScanRequestReasonDynamicElement
};

@interface ReactNativePendo ()
@property (nonatomic) BOOL hasListeners;
@property (nonatomic) CompletionBlock completionBlock;
@property (nonatomic) BOOL shouldWaitForResponseFromJS;
@property (atomic) int scanRequestCounter;
@property (nonatomic) PNDScanRequestReason scanReason; // if scan failure occurs we want to send the reason we requested the scan
@property (nonatomic) BOOL shouldScanDynamicElements;
@property (nonatomic, nullable) id uiManager;
@property (nonatomic) BOOL runtimeFabricDetected; // Runtime detection cache for Fabric architecture
@end

static NSString *const kDebugMode = @"debugMode";
static NSString *const kOptions = @"options";
static NSString *const kReactNativeVersion = @"reactNativeVersion";
static NSString *const kPluginVersion = @"pluginVersion";
static NSString *const kEnvironmentName = @"environmentName";
static NSString *const kVisitorId = @"visitorId";
static NSString *const kAccountId = @"accountId";
static NSString *const kVisitorData = @"visitorData";
static NSString *const kAccountData = @"accountData";
static NSString *const kOnDefaultRequestAnalyzer = @"onDefaultRequestAnalyzer";
static NSString *const kOnDynamicContentRequestAnalyzer = @"onDynamicContentRequestAnalyzer";
static NSString *const kOnCaptureRequestAnalyzer = @"onCaptureRequestAnalyzer";
static NSString *const kOnReturnFromAlertRequestAnalyzer = @"onReturnFromAlertRequestAnalyzer";
static NSString *const kOnScreenContentChange = @"onScreenContentChange";
static NSString *const kOnModalStateVisible = @"onModalStateVisible";
static NSString *const kOnModalStateHidden = @"onModalStateHidden";
static NSString *const kOnInitComplete = @"onInitComplete";
static NSString *const kOnInitFailed = @"onInitFailed";
static NSString *const kCompletionBlock = @"block";
static NSString *const kScanRequestReason = @"scanReason";
static NSString *const kShouldIgnoreDynamicContent = @"shouldIgnoreDynamicContentRN";
static NSString *const kDynamicScreenScanDebouncerDelay = @"dynamicScreenScanDebouncerDelayRN";

static NSUInteger const kReactNavigation = 2;
static NSUInteger const kExpoRouter = 5;

#if RCT_NEW_ARCH_ENABLED
  static BOOL isNewArchitectureEnabled = YES;
#else
  static BOOL isNewArchitectureEnabled = NO;
#endif

@implementation ReactNativePendo

+ (BOOL)requiresMainQueueSetup {
    return YES;
}


RCT_EXPORT_MODULE()

- (instancetype)init {
    if (self = [super init]) {
        _shouldWaitForResponseFromJS = NO;
        _scanRequestCounter = 0;
        _runtimeFabricDetected = NO;
        [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(JSHierarchyScan:) name:PNDRequiresJSHierarchyScan object:nil];
        [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(onSDKInitSuccess:) name:kPNDDidSuccessfullyInitializeSDKNotification object:nil];
        [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(onSDKInitFailure:) name:kPNDErrorInitializeSDKNotification object:nil];
        // Use compile-time as default, will be validated at runtime when bridge is set
        [PendoManagerReactNative setIsFabricEnabled:isNewArchitectureEnabled];
    }
    return self;
}

- (void)JSHierarchyScan:(NSNotification *)notification {
    self.scanRequestCounter++;
    self.shouldWaitForResponseFromJS = YES;
    if (self.hasListeners) {
        self.scanReason = [notification.userInfo[kScanRequestReason] integerValue];

        if (self.scanReason == PNDScanRequestReasonDynamicElement && !self.shouldScanDynamicElements) {
            self.shouldWaitForResponseFromJS = NO;
            self.scanRequestCounter--;
            return;
        }

        id block = notification.userInfo[kCompletionBlock];

        if (block != nil) {
            self.completionBlock = (CompletionBlock)block;
        }

        dispatch_async([self methodQueue], ^{
            [self sendEventWithName:[self scanRequestEvent] body:nil];
        });

        // Timeout in case we get no response from JS about the new scan or failure
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(5.0 * NSEC_PER_SEC)), [self methodQueue], ^{
            self.scanRequestCounter--;
            if (self.shouldWaitForResponseFromJS) {
                if (self.scanRequestCounter == 0) {
                    NSDictionary *info = @{ @"errorMessage" : @"timeout - failed to receive screen data from JS in time" };
                    [PendoManagerReactNative sendFailureInfo:info shouldSendErrorToBE:NO scanReason:self.scanReason];
                    self.completionBlock = nil;
                } else {
                    NSString *message = [NSString stringWithFormat:@"Response for JS scan request is ignored due to more recent requests. Scan request counter: %d", self.scanRequestCounter+1];
                    [PendoManagerReactNative logMessage:message];
                }
            }
        });
    }
}

#pragma mark - Facebook Overrides (Event Emitter)
- (void)startObserving {
    self.hasListeners = YES;
}

- (void)stopObserving {
    self.hasListeners = NO;
}

- (dispatch_queue_t)methodQueue {
    return dispatch_get_main_queue();
}

- (NSArray<NSString *> *)supportedEvents {
    return @[kOnDefaultRequestAnalyzer, kOnReturnFromAlertRequestAnalyzer, kOnCaptureRequestAnalyzer, kOnDynamicContentRequestAnalyzer, kOnScreenContentChange, kOnModalStateHidden, kOnModalStateVisible, kOnInitComplete, kOnInitFailed];
}

- (void)setBridge:(RCTBridge *)bridge {
    [super setBridge:bridge];
    [PendoManagerReactNative setUiManager:bridge.uiManager];
    // Perform runtime detection when bridge is available
    [self detectFabricAtRuntime];
}

#pragma mark - Private
/**
 * @brief Detects Fabric architecture at runtime by checking UIManager class type
 * Compares runtime detection with compile-time flag and logs warning if they differ
 */
- (void)detectFabricAtRuntime {
    BOOL runtimeFabric = NO;

    if (self.bridge != nil && self.bridge.uiManager != nil) {
        // Check if UIManager is Fabric-specific class
        NSString *uiManagerClassName = NSStringFromClass([self.bridge.uiManager class]);
        runtimeFabric = [uiManagerClassName isEqualToString:@"RCTFabricUIManager"];

        self.runtimeFabricDetected = runtimeFabric;

        // Log UIManager class for debugging
        NSString *logMessage = [NSString stringWithFormat:@"UIManager class: %@, Fabric runtime: %d, Fabric compile-time: %d",
                                uiManagerClassName, runtimeFabric, isNewArchitectureEnabled];
        [PendoManagerReactNative logMessage:logMessage];

        // Log warning if runtime differs from compile-time
        if (runtimeFabric != isNewArchitectureEnabled) {
            NSString *warning = [NSString stringWithFormat:
                @"Warning: Fabric detection mismatch - Compile-time: %d, Runtime: %d. Using runtime value.",
                isNewArchitectureEnabled, runtimeFabric];
            [PendoManagerReactNative logMessage:warning];

            // Update Pendo SDK with correct runtime value
            [PendoManagerReactNative setIsFabricEnabled:runtimeFabric];
        }
    }
}

/**
 * @brief Returns current Fabric architecture status
 * @return YES if Fabric (new architecture) is enabled, NO if Paper (old architecture)
 * Prefers runtime detection if bridge is set, otherwise returns compile-time default
 */
- (BOOL)isFabricEnabled {
    // Prefer runtime detection if bridge is set, otherwise use compile-time default
    return self.bridge != nil ? self.runtimeFabricDetected : isNewArchitectureEnabled;
}

- (NSString *)scanRequestEvent {
    NSString *eventName = nil;
    switch (self.scanReason) {
        case PNDScanRequestReasonCapture: {
            eventName = kOnCaptureRequestAnalyzer;
            break;
        }
        case PNDScanRequestReasonReturnFromAlert: {
            eventName = kOnReturnFromAlertRequestAnalyzer;
            break;
        }
        case PNDScanRequestReasonDynamicElement: {
            eventName = kOnDynamicContentRequestAnalyzer;
            break;
        }
        default:
            eventName = kOnDefaultRequestAnalyzer;
            break;
    }
    return eventName;
}

- (void)emitInitComplete {
    if (self.hasListeners) {
        dispatch_async([self methodQueue], ^{
            [self sendEventWithName:kOnInitComplete body:nil];
        });
    }
}

- (void)emitInitFailed {
    if (self.hasListeners) {
        dispatch_async([self methodQueue], ^{
            [self sendEventWithName:kOnInitFailed body:nil];
        });
    }
}

- (void)onSDKInitSuccess:(NSNotification *)notification {
    [self emitInitComplete];
}

- (void)onSDKInitFailure:(NSNotification *)notification {
    [self emitInitFailed];
}

#pragma mark - Pendo Module
/*
 *  Call this method on the sharedManger with your application key.
 *
 *  @param appKey The app key for your account
 *  @param reactPlugin The react plugin navigation
 *  @param pendoOptions additional options for internal use only, default nil
 */
RCT_EXPORT_METHOD(setup:(NSString *_Nonnull)appKey reactPlugin:(NSUInteger)plugin withOptions:(NSDictionary * _Nullable)pendoOptions) {
    PendoOptions *options = [[PendoOptions alloc] init];
    //SDK Doesn't distinguish between ExpoRouter and ReactNavigation
    if(plugin == kExpoRouter) {
        plugin = kReactNavigation;
    }
     options.reactPlugin = plugin;
      NSString * pluginVersion;
      NSString * reactNativeVersion;
      if (pendoOptions != nil) {
          BOOL isDebugModeEnabled = [pendoOptions[kDebugMode] boolValue];
          if (isDebugModeEnabled) {
              [self setDebugMode:YES];
          }

          options.environmentName = [RCTConvert NSString:pendoOptions[kEnvironmentName]];
          options.shouldIgnoreDynamicContentRN = [pendoOptions[kShouldIgnoreDynamicContent] boolValue];
          options.dynamicScreenScanDebouncerDelayRN = [RCTConvert NSNumber:pendoOptions[kDynamicScreenScanDebouncerDelay]];

          pluginVersion = [RCTConvert NSString:pendoOptions[kPluginVersion]];
          if (pluginVersion != nil) {
              options.pluginVersion = pluginVersion;
          }
          reactNativeVersion = [RCTConvert NSString:pendoOptions[kReactNativeVersion]];
          if(reactNativeVersion != nil) {
            options.platformVersion = [RCTConvert NSString:pendoOptions[kReactNativeVersion]];
          }
      }
      NSString *message = [NSString stringWithFormat:@"Pendo Plugin: %@, React Native: %@, New Architecture (current): %d",
                          pluginVersion, reactNativeVersion, [self isFabricEnabled]];
      [PendoManagerReactNative logMessage: message];
      [[PendoManager sharedManager] setup:appKey withOptions:options];
}

/**
 * Must be called <b>after</b> the SDK was setup.
 *
 * @brief start a session with a new visitor.
 * In case a session was already started before, end the previous one and start a new one.
 *
 * @param visitorId The visitor's ID.
 * @param accountId The account's ID.
 * @param visitorData The visitor's data.
 * @param accountData The account's data.
 */
RCT_EXPORT_METHOD(startSession:(NSString *_Nullable)visitorId
                  accountId:(NSString *_Nullable)accountId
                  visitorData:(NSDictionary *_Nullable)visitorData
                  accountData:(NSDictionary *_Nullable)accountData) {
    [[PendoManager sharedManager] startSession:visitorId accountId:accountId visitorData:visitorData accountData:accountData];
}

/**
 * Set a visitor data
 * This data is used by Pendo Mobile for creating audiences or reporting analytics.
 * For instance you might want to provide data on the visitor's age or if the visitor is logged into a service.
 * @code
 * [[PendoManager sharedManager].setVisitorData:@{@"age" :@"15",@"isLoggedIn",true,...}];
 * @endcode
 *
 * @param visitorData Dictionary containing visitor data
 */
RCT_EXPORT_METHOD(setVisitorData:(NSDictionary * _Nonnull)visitorData) {
    [[PendoManager sharedManager] setVisitorData:visitorData];
}

/**
 * Set a account data
 * This data is used by Pendo Mobile for creating audiences or reporting analytics.
 * For instance you might want to provide data on the account's subscription or if the account is active or not.
 * @code
 * [[PendoManager sharedManager].setAccountData:@{@"key1" :@"value1", ...}];
 * @endcode
 *
 * @param accountData Dictionary containing  account data
 */
RCT_EXPORT_METHOD(setAccountData:(NSDictionary * _Nonnull)accountData) {
    [[PendoManager sharedManager] setAccountData:accountData];
}

/**
 * Must be called <b>after</b> the SDK was initialized.
 * @brief Ends the current session. New session will not be started until switchVisitor is called.
 */
RCT_EXPORT_METHOD(endSession) {
    [[PendoManager sharedManager] endSession];
}

/**
 * @brief Pause showing guides during this session
 */
RCT_EXPORT_METHOD(pauseGuides:(BOOL)dismissGuides) {
    [[PendoManager sharedManager] pauseGuides:dismissGuides];
}

/**
 * @brief Resume showing guides during this session
 */
RCT_EXPORT_METHOD(resumeGuides) {
    [[PendoManager sharedManager] resumeGuides];
}

/**
 * @brief Dismiss all visible guides
 */
RCT_EXPORT_METHOD(dismissVisibleGuides) {
    [[PendoManager sharedManager] dismissVisibleGuides];
}

/**
 * @brief Get current visitorId
 */
RCT_REMAP_METHOD(getVisitorId, visitorResolver:(RCTPromiseResolveBlock)resolve visitorRejecter:(RCTPromiseRejectBlock)reject) {
    NSString *visitorId = [[PendoManager sharedManager] visitorId];
    resolve(visitorId);
}

/**
 * @brief Get current accountId
 */
RCT_REMAP_METHOD(getAccountId,accountResolver:(RCTPromiseResolveBlock)resolve accountRejecter:(RCTPromiseRejectBlock)reject) {
    NSString *accountId = [[PendoManager sharedManager] accountId];
    resolve(accountId);
}

/**
 * @brief Get current deviceId
 */
RCT_REMAP_METHOD(getDeviceId, deviceResolver:(RCTPromiseResolveBlock)resolve deviceRejecter:(RCTPromiseRejectBlock)reject) {
    NSString *deviceId = [[PendoManager sharedManager] getDeviceId];
    resolve(deviceId);
}

/**
 * When your application needs to send additional events about actions your users perform.
 *
 * @param event The event name describing the user’s action.
 * @param properties dictionary of event properties (optional).
 * @brief Queue a track eventsfor transmission, optionally including properties as the payload of the event.
 */
RCT_EXPORT_METHOD(track:(NSString * _Nonnull)event properties:(NSDictionary * _Nullable)properties) {
    [[PendoManager sharedManager] track:event properties:properties];
}

RCT_EXPORT_METHOD(screenChanged:(NSString *)screenName rootTags:(NSArray *)rootTags nodes:(NSArray *)clickableNodes info:(NSDictionary *)info) {
    self.shouldWaitForResponseFromJS = NO;
    [PendoManagerReactNative screenChanged:screenName rootTags:rootTags nodes:clickableNodes info:info];
    if (self.completionBlock != nil) {
        self.completionBlock();
        self.completionBlock = nil;
    }
    self.shouldScanDynamicElements = YES;
}

/**
 * @brief When We want to send indication to the native that something went wrong on the JS side
 *
 * @param userInfo a dictionary to pass in the relevant data (i.e error message).
 * @param shouldSendErrorToBE a boolean indicating whether we should inform BE about the error or not.
 */
RCT_EXPORT_METHOD(sendFailureInfo:(NSDictionary *)userInfo shouldSendErrorToBE:(BOOL)shouldSendErrorToBE) {
    self.shouldWaitForResponseFromJS = NO;
    self.completionBlock = nil;
    [PendoManagerReactNative sendFailureInfo:userInfo shouldSendErrorToBE:shouldSendErrorToBE scanReason:self.scanReason];
}

/**
 * @brief set debug mode to true to get log messages
 */
RCT_EXPORT_METHOD(setDebugMode:(BOOL)isDebugEnabled) {
    [[PendoManager sharedManager] setDebugMode:isDebugEnabled];
}

/**
 * @brief Resume showing guides during this session
 */
RCT_EXPORT_METHOD(sendClickAnalytic:(NSString *)nativeID) {
    // This method is not implemented on the iOS native SDK side since it already handles
    // clicks for every element that is recognized as clickable (passed in as using nativeID)
}

RCT_EXPORT_METHOD(shouldScanForDynamicElements:(BOOL)scanDynamicElements) {
    self.shouldScanDynamicElements = scanDynamicElements;
}

RCT_EXPORT_METHOD(screenContentChanged) {
     [self sendEventWithName:kOnScreenContentChange body:nil];
}

RCT_EXPORT_METHOD(modalStateChanged:(BOOL)isVisible) {
    [self sendEventWithName:(isVisible ? kOnModalStateVisible :kOnModalStateHidden) body:nil];
}

RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD(viewsVisible:(NSArray *)reactTags) {
  __block BOOL isCalibrated = NO;

   if ([NSThread isMainThread]) {
     // Already on main thread
     isCalibrated = [self verifyViews:reactTags];
   } else {
     // Force to main thread synchronously
     dispatch_sync(dispatch_get_main_queue(), ^{
       isCalibrated = [self verifyViews:reactTags];
     });
   }

   return @(isCalibrated);

}

- (BOOL) verifyViews:(NSArray *)reactTags {
  for(NSNumber *reactTag in reactTags) {
    UIView *view = [self.bridge.uiManager viewForReactTag:reactTag];
    if(view != nil && view.window != nil && view.alpha > 0 && !view.isHidden && view.frame.size.width > 0 && view.frame.size.height > 0) {
       return YES;
     }
  }
  return NO;
}

- (void)dealloc {
    [[NSNotificationCenter defaultCenter] removeObserver:self name:PNDRequiresJSHierarchyScan object:nil];
    [[NSNotificationCenter defaultCenter] removeObserver:self name:kPNDDidSuccessfullyInitializeSDKNotification object:nil];
    [[NSNotificationCenter defaultCenter] removeObserver:self name:kPNDErrorInitializeSDKNotification object:nil];
}

@end
