#import "RNInsider.h"
#import "RNNotificationHandler.h"
#import "RNUtils.h"

#if __has_include(<InsiderMobile/Insider.h>)
#import <InsiderMobile/Insider.h>
#elif __has_include("Insider.h")
#import "Insider.h"
#endif

#if __has_include(<InsiderGeofence/InsiderGeofence.h>)
#import <InsiderGeofence/InsiderGeofence.h>
#elif __has_include("InsiderGeofence.h")
#import "InsiderGeofence.h"
#endif

#import <InsiderMobile/InsiderCallbackTypeEnum.h>

// The core stores one object inside an object and drops anything deeper; mirrored from JS.
static const NSUInteger RNInsiderMaxObjectNestingDepth = 2;

// Identifier keys sent by InsiderIdentifier.js; every other key becomes a custom identifier.
static NSString *const ADD_EMAIL = @"addEmail";
static NSString *const ADD_PHONE_NUMBER = @"addPhoneNumber";
static NSString *const ADD_USER_ID = @"addUserID";

// The SDK may hand back nil; an empty argument list reaches JS as undefined rather than crashing.
static NSArray *RNInsiderCallbackArguments(id value) {
    return value ? @[value] : @[];
}

#pragma mark - Insider event listener

/**
 * Dedicated observer for the SDK's recorded-event stream.
 *
 * iOS holds event observers WEAKLY (Android holds them strongly), so whatever object
 * implements InsiderEventListener must be kept alive by us. RNInsider instances are
 * created/destroyed by React Native and are not a safe owner, so this small class is
 * retained by the file-static `insiderEventListener` below for as long as the JS side
 * has at least one subscriber.
 */
@interface RNInsiderEventListener : NSObject <InsiderEventListener>
@end

@implementation RNInsiderEventListener

- (void)onEventRecorded:(NSString *)name
             parameters:(NSDictionary<NSString *, id> *)parameters
              timestamp:(NSTimeInterval)timestamp {
    @try {
        if (name == nil) return;

        // `parameters` is a defensive copy owned by the SDK — read only, never mutate.
        //
        // It MUST go through the converter rather than being passed straight through: RN's
        // `convertIdToFollyDynamic` only understands NSNumber/NSString/NSArray/NSDictionary/
        // NSNull, and turns anything else (NSDate in particular) into a NULL dynamic — the
        // value would reach JS as `null`. See RNUtils.convertParametersForBridge:.
        //
        // `timestamp` is forwarded in WHOLE SECONDS, matching the native SDK. JS must
        // do `new Date(timestamp * 1000)`. Note this differs deliberately from Date values
        // nested inside `parameters`, which are normalized to epoch MILLIS for Android parity.
        NSDictionary *payload = @{
            @"name": name,
            @"parameters": [RNUtils convertParametersForBridge:parameters],
            @"timestamp": @(timestamp)
        };

        RNNotificationHandler *handler = [[RNNotificationHandler alloc] init];

        // THREADING (verified against React Native 0.83 sources — do not re-litigate):
        // this runs on the SDK's internal listener thread, and that is fine. No manual
        // dispatch hop is needed, because RN marshals to the JS thread itself:
        //   RCTEventEmitter.sendEventWithName:body: (React/Modules/RCTEventEmitter.m:66)
        //     -> RCTCallableJSModules.invokeModule: (React/Base/RCTCallableJSModules.m:43/49)
        //     -> bridge:      RCTCxxBridge.enqueueJSCall (explicitly annotated "AnyThread")
        //                     -> NativeToJsBridge.cpp:298 m_executorMessageQueueThread->runOnQueue
        //                     -> CFRunLoopPerformBlock onto the dedicated JS thread (FIFO).
        //        bridgeless:  ReactInstance.cpp:299 bufferedRuntimeExecutor_->execute
        //                     -> RuntimeScheduler task queue, drained on the JS thread.
        // Adding our own queue here would buy nothing and would only add a second hop.
        // Ordering is preserved on the bridge path, so sequential emits from this one SDK
        // thread stay in order and a purchase cannot overtake a product_detail_page_view.
        [handler sendDataToJS:INSIDER_EVENT_RECORDED data:payload];
    } @catch (NSException *e) {
        [Insider sendError:e desc:[NSString stringWithFormat:@"%s:%d", __func__, __LINE__]];
    }
}

@end

/// Strong reference keeping the weakly-held observer alive. Non-nil while registered.
static RNInsiderEventListener *insiderEventListener;

@implementation RNInsider

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

+ (BOOL)requiresMainQueueSetup {
    return NO;
}

RCT_EXPORT_MODULE()

- (NSString *)resolveAppIdentifier:(NSString *)app {
    return app.length > 0 ? app : nil;
}

RCT_EXPORT_METHOD(initWithLaunchOptions:(NSDictionary *)launchOptions partnerName:(NSString *)partnerName appGroup:(NSString *)appGroup sdkVersion:(NSString *)sdkVersion appIdentifier:(NSString *)appIdentifier) {
    [Insider registerInsiderCallbackWithSelector:@selector(emitEvent:) sender:self];
    [Insider setHybridSDKVersion:sdkVersion];
    NSString *app = [self resolveAppIdentifier:appIdentifier];
    if (app) {
        [Insider initWithLaunchOptions:nil partnerName:partnerName appGroup:appGroup appIdentifier:app];
    } else {
        [Insider initWithLaunchOptions:nil partnerName:partnerName appGroup:appGroup];
    }
}

RCT_EXPORT_METHOD(initWithCustomEndpoint:(NSDictionary *)launchOptions partnerName:(NSString *)partnerName appGroup:(NSString *)appGroup customEndpoint:(NSString *)endpoint sdkVersion:(NSString *)sdkVersion appIdentifier:(NSString *)appIdentifier) {
    [Insider registerInsiderCallbackWithSelector:@selector(emitEvent:) sender:self];
    [Insider setHybridSDKVersion:sdkVersion];
    NSString *app = [self resolveAppIdentifier:appIdentifier];
    if (app) {
        [Insider initWithLaunchOptions:nil partnerName:partnerName appGroup:appGroup customEndpoint:endpoint appIdentifier:app];
    } else {
        [Insider initWithLaunchOptions:nil partnerName:partnerName appGroup:appGroup customEndpoint:endpoint];
    }
}

RCT_EXPORT_METHOD(reinitWithPartnerName:(NSString *)partnerName) {
    [Insider reinitWithPartnerName:partnerName];
}

RCT_EXPORT_METHOD(tagEvent:(NSString *)eventName withParameters:(NSArray *)parameters) {
    @try {
        [[RNUtils parseEventFromEventName:eventName andParameters:parameters] build];
    } @catch (NSException *e) {
        [Insider sendError:e desc:[NSString stringWithFormat:@"%s:%d", __func__, __LINE__]];
    }
}

// Insider User
RCT_EXPORT_METHOD(setGender:(int)gender) {
    @try {
        [Insider getCurrentUser].setGender([RNUtils genderFromInt:gender]);
    } @catch (NSException *e) {
        [Insider sendError:e desc:[NSString stringWithFormat:@"%s:%d", __func__, __LINE__]];
    }
}

RCT_EXPORT_METHOD(setBirthday:(NSString *)birthday) {
    @try {
        long long epochMillis = birthday.longLongValue;
        NSTimeInterval epochSeconds = ((NSTimeInterval)epochMillis) / 1000.0;
        NSDate *dateValue = [NSDate dateWithTimeIntervalSince1970:epochSeconds];
        [Insider getCurrentUser].setBirthday(dateValue);
    } @catch (NSException *e) {
        [Insider sendError:e desc:[NSString stringWithFormat:@"%s:%d", __func__, __LINE__]];
    }
}

RCT_EXPORT_METHOD(setName:(NSString *)value) {
    [Insider getCurrentUser].setName(value);
}

RCT_EXPORT_METHOD(setSurname:(NSString *)value) {
    [Insider getCurrentUser].setSurname(value);
}

RCT_EXPORT_METHOD(setAge:(int)value) {
    [Insider getCurrentUser].setAge(value);
}

RCT_EXPORT_METHOD(setLanguage:(NSString *)value) {
    [Insider getCurrentUser].setLanguage(value);
}

RCT_EXPORT_METHOD(setLocale:(NSString *)value) {
    [Insider getCurrentUser].setLocale(value);
}

RCT_EXPORT_METHOD(setFacebookID:(NSString *)value) {
    [Insider getCurrentUser].setFacebookID(value);
}

RCT_EXPORT_METHOD(setTwitterID:(NSString *)value) {
    [Insider getCurrentUser].setTwitterID(value);
}

RCT_EXPORT_METHOD(setEmailOptin:(BOOL)value) {
    [Insider getCurrentUser].setEmailOptin(value);
}

RCT_EXPORT_METHOD(setEmail:(NSString *)value) {
    [Insider getCurrentUser].setEmail(value);
}

RCT_EXPORT_METHOD(setSMSOptin:(BOOL)value) {
    [Insider getCurrentUser].setSMSOptin(value);
}

RCT_EXPORT_METHOD(setPhoneNumber:(NSString *)value) {
    [Insider getCurrentUser].setPhoneNumber(value);
}

RCT_EXPORT_METHOD(handleURL:(NSString *)value) {
    @try {
        NSURL *url = [NSURL URLWithString:value];

        if (url == nil) {
            return;
        }

        [Insider handleUrl:url];
    } @catch (NSException *e) {
        [Insider sendError:e desc:[NSString stringWithFormat:@"%s:%d", __func__, __LINE__]];
    }
}

RCT_EXPORT_METHOD(setPushOptin:(BOOL)value) {
    [Insider getCurrentUser].setPushOptin(value);
}

RCT_EXPORT_METHOD(setLocationOptin:(BOOL)value) {
    [Insider getCurrentUser].setLocationOptin(value);
}

RCT_EXPORT_METHOD(setWhatsappOptin:(BOOL)value) {
    [Insider getCurrentUser].setWhatsappOptin(value);
}

RCT_EXPORT_METHOD(setPromotionalOptin:(BOOL)value) {
    @try {
        [Insider getCurrentUser].setPromotionalOptin(value);
    } @catch (NSException *e) {
        [Insider sendError:e desc:[NSString stringWithFormat:@"%s:%d", __func__, __LINE__]];
    }
}

- (BOOL)hasAnyIdentifierValue:(NSDictionary *)identifiers {
    for (NSString *key in identifiers.allKeys) {
        if (![[identifiers objectForKey:key] isKindOfClass:[NSNull class]]) {
            return YES;
        }
    }

    return NO;
}

RCT_EXPORT_METHOD(login:(NSDictionary *)identifiers) {
    @try {
        if (!identifiers) return;

        InsiderIdentifiers *insiderIdentifiers = [[InsiderIdentifiers alloc] init];
        for (NSString *key in identifiers.allKeys) {
            if ([key isEqualToString:ADD_EMAIL]) {
                insiderIdentifiers.addEmail([identifiers objectForKey:key]);
            } else if ([key isEqualToString:ADD_PHONE_NUMBER]) {
                insiderIdentifiers.addPhoneNumber([identifiers objectForKey:key]);
            } else if ([key isEqualToString:ADD_USER_ID]) {
                insiderIdentifiers.addUserID([identifiers objectForKey:key]);
            } else {
                insiderIdentifiers.addCustomIdentifier(key, [identifiers objectForKey:key]);
            }
        }
        [[Insider getCurrentUser] login:insiderIdentifiers];
    } @catch (NSException *e) {
        [Insider sendError:e desc:[NSString stringWithFormat:@"%s:%d", __func__, __LINE__]];
    }
}
RCT_EXPORT_METHOD(loginWithReturningID:(NSDictionary *)identifiers insiderIDResult:(RCTResponseSenderBlock) insiderIDResult) {
    @try {
        // The core returns on an empty identifier set without invoking insiderIDResult, stranding a
        // caller that waits. An NSNull value counts as absent, so a key-only count is not enough.
        if (![self hasAnyIdentifierValue:identifiers]) {
            insiderIDResult(@[[NSNull null]]);
            return;
        }

        InsiderIdentifiers *insiderIdentifiers = [[InsiderIdentifiers alloc] init];
        for (NSString *key in identifiers.allKeys) {
            if ([key isEqualToString:ADD_EMAIL]) {
                insiderIdentifiers.addEmail([identifiers objectForKey:key]);
            } else if ([key isEqualToString:ADD_PHONE_NUMBER]) {
                insiderIdentifiers.addPhoneNumber([identifiers objectForKey:key]);
            } else if ([key isEqualToString:ADD_USER_ID]) {
                insiderIdentifiers.addUserID([identifiers objectForKey:key]);
            } else {
                insiderIdentifiers.addCustomIdentifier(key, [identifiers objectForKey:key]);
            }
        }
        [[Insider getCurrentUser] login:insiderIdentifiers insiderIDResult:^(NSString *insiderID) {
            insiderIDResult(@[insiderID ?: [NSNull null]]);
        }];
    } @catch (NSException *e) {
        [Insider sendError:e desc:[NSString stringWithFormat:@"%s:%d", __func__, __LINE__]];
        insiderIDResult(@[[NSNull null]]);
    }
}

RCT_EXPORT_METHOD(logout) {
    [[Insider getCurrentUser] logout];
}

RCT_EXPORT_METHOD(logoutResettingInsiderID:(NSArray *)identifiersArray insiderIDResult:(RCTResponseSenderBlock)insiderIDResult) {
    @try {
        NSArray<InsiderIdentifiers *> *identifiers = [self convertReadableArrayToInsiderIdentifiersArray:identifiersArray];

        if (insiderIDResult != nil) {
            [[Insider getCurrentUser] logoutResettingInsiderID:identifiers insiderIDResult:^(NSString *insiderID) {
                NSString *insiderIDToPass = insiderID ? insiderID : @"";
                insiderIDResult(@[insiderIDToPass]);
            }];
        } else {
            [[Insider getCurrentUser] logoutResettingInsiderID:identifiers];
        }
    } @catch (NSException *e) {
        [Insider sendError:e desc:[NSString stringWithFormat:@"%s:%d", __func__, __LINE__]];

        if (insiderIDResult != nil) {
            insiderIDResult(@[[NSNull null]]);
        }
    }
}

- (NSArray<InsiderIdentifiers *> *)convertReadableArrayToInsiderIdentifiersArray:(NSArray *)identifiersArray {
    if (identifiersArray == nil || identifiersArray.count == 0) {
        return nil;
    }

    NSMutableArray<InsiderIdentifiers *> *identifiers = [NSMutableArray array];
    for (NSDictionary *identifierDict in identifiersArray) {
        InsiderIdentifiers *insiderIdentifiers = [[InsiderIdentifiers alloc] init];
        for (NSString *key in identifierDict.allKeys) {
            if ([key isEqualToString:ADD_EMAIL]) {
                insiderIdentifiers.addEmail([identifierDict objectForKey:key]);
            } else if ([key isEqualToString:ADD_PHONE_NUMBER]) {
                insiderIdentifiers.addPhoneNumber([identifierDict objectForKey:key]);
            } else if ([key isEqualToString:ADD_USER_ID]) {
                insiderIdentifiers.addUserID([identifierDict objectForKey:key]);
            } else {
                insiderIdentifiers.addCustomIdentifier(key, [identifierDict objectForKey:key]);
            }
        }
        [identifiers addObject:insiderIdentifiers];
    }
    return identifiers;
}

RCT_EXPORT_METHOD(setCustomAttributeWithString:(NSString *)key value:(NSString *)value) {
    [Insider getCurrentUser].setCustomAttributeWithString(key, value);
}

RCT_EXPORT_METHOD(setCustomAttributeWithInt:(NSString *)key value:(int)value) {
    [Insider getCurrentUser].setCustomAttributeWithInt(key, value);
}

RCT_EXPORT_METHOD(setCustomAttributeWithDouble:(NSString *)key value:(double)value) {
    [Insider getCurrentUser].setCustomAttributeWithDouble(key, value);
}

RCT_EXPORT_METHOD(setCustomAttributeWithBoolean:(NSString *)key value:(BOOL)value) {
    [Insider getCurrentUser].setCustomAttributeWithBoolean(key, value);
}

RCT_EXPORT_METHOD(setCustomAttributeWithDate:(NSString *)key value:(NSString *)value) {
    @try {
        long long epochMillis = value.longLongValue;
        NSTimeInterval epochSeconds = ((NSTimeInterval)epochMillis) / 1000.0;
        NSDate *dateValue = [NSDate dateWithTimeIntervalSince1970:epochSeconds];
        [Insider getCurrentUser].setCustomAttributeWithDate(key, dateValue);
    } @catch (NSException *e) {
        [Insider sendError:e desc:[NSString stringWithFormat:@"%s:%d", __func__, __LINE__]];
    }
}

RCT_EXPORT_METHOD(setCustomAttributeWithArray:(NSString *)key value:(NSArray *)value) {
    [Insider getCurrentUser].setCustomAttributeWithArray(key, value);
}

RCT_EXPORT_METHOD(addObjectsToCustomAttribute:(NSString *)key objects:(NSArray *)objects) {
    @try {
        [Insider getCurrentUser].addObjectsToCustomAttribute(key, [RNUtils parseCustomParametersList:objects]);
    } @catch (NSException *e) {
        [Insider sendError:e desc:[NSString stringWithFormat:@"%s:%d", __func__, __LINE__]];
    }
}

// A key-count comparison only sees the top level, so a nested object that lost a child still counts
// as intact - and a narrowed match selects MORE objects than asked. This walks the nesting.
+ (BOOL)isIntactMatch:(NSArray *)source parsed:(NSDictionary *)parsed {
    return [RNInsider isIntactMatch:source parsed:parsed depth:1];
}

+ (BOOL)isIntactMatch:(NSArray *)source parsed:(NSDictionary *)parsed depth:(NSUInteger)depth {
    // The parser recurses without a depth limit, so a match nested deeper than the core stores parses
    // intact at every level while the core drops the deepest one - the same narrowing the rest of this
    // method refuses. JS refuses the shape too; this covers a payload that bypassed it.
    if (depth > RNInsiderMaxObjectNestingDepth) {
        return NO;
    }

    if (parsed.count != source.count) {
        return NO;
    }

    for (id element in source) {
        if (![element isKindOfClass:[NSDictionary class]]) continue;

        NSDictionary *entry = (NSDictionary *)element;
        if (![entry[@"type"] isEqual:@"object"]) continue;

        id nestedSource = entry[@"value"];
        id nestedParsed = parsed[entry[@"key"]];
        // An empty nested object carries nothing to match on, and native drops a child that kept no
        // key - the loss cascades until the match is the wildcard. Same count on both sides, so the
        // check above cannot see it.
        if (![nestedSource isKindOfClass:[NSArray class]] || ((NSArray *)nestedSource).count == 0
                || ![nestedParsed isKindOfClass:[NSDictionary class]]) {
            return NO;
        }

        if (![RNInsider isIntactMatch:(NSArray *)nestedSource parsed:(NSDictionary *)nestedParsed depth:depth + 1]) {
            return NO;
        }
    }

    return YES;
}

+ (void)reportRefusedMatchInMethod:(NSString *)methodName reason:(NSString *)reason desc:(NSString *)desc {
    NSString *message = [NSString stringWithFormat:@"%@: %@", methodName, reason];
    // sendError only reaches Insider's own error channel, which the integrating developer never
    // sees. The refusal is silent from their side, so also write it to the platform log - the
    // Xcode console - alongside it. The refusal itself is unchanged.
    NSLog(@"[ReactNative] %@ ignored: the call was refused because %@.", methodName, reason);
    [Insider sendError:[NSException exceptionWithName:NSInvalidArgumentException reason:message userInfo:nil] desc:desc];
}

RCT_EXPORT_METHOD(mergeObjectsInCustomAttribute:(NSString *)key match:(id)match value:(NSArray *)value) {
    @try {
        // Typed `id` so the bridge does not collapse a non-array value to nil, which would be
        // indistinguishable from an intentional nil match. Merge has no wildcard, so refuse it.
        if (![match isKindOfClass:[NSArray class]]) {
            [RNInsider reportRefusedMatchInMethod:@"mergeObjectsInCustomAttribute" reason:@"match must be an array" desc:[NSString stringWithFormat:@"%s:%d", __func__, __LINE__]];
            return;
        }
        NSArray *matchParameters = (NSArray *)match;
        if (![value isKindOfClass:[NSArray class]] || ((NSArray *)value).count == 0) {
            // A value that carries nothing merges nothing while still carrying a match, so the call
            // reads as applied while changing no object - the shape JS refuses too.
            [RNInsider reportRefusedMatchInMethod:@"mergeObjectsInCustomAttribute" reason:@"value must be a non-empty array" desc:[NSString stringWithFormat:@"%s:%d", __func__, __LINE__]];
            return;
        }

        NSArray *valueParameters = (NSArray *)value;

        if (matchParameters.count == 0) {
            // An empty match is the core's irreversible wildcard, so it would merge into every
            // object. JS refuses the same shape; this covers a payload that bypassed it.
            [RNInsider reportRefusedMatchInMethod:@"mergeObjectsInCustomAttribute" reason:@"an empty match would merge into every object" desc:[NSString stringWithFormat:@"%s:%d", __func__, __LINE__]];
            return;
        }

        NSDictionary *matchDictionary = [RNUtils parseCustomParameters:matchParameters];
        if (![RNInsider isIntactMatch:matchParameters parsed:matchDictionary]) {
            // A malformed match entry parses to fewer keys than the incoming array had elements;
            // calling native with the reduced dictionary would widen the merge beyond what was
            // requested, so refuse it instead of failing open.
            [RNInsider reportRefusedMatchInMethod:@"mergeObjectsInCustomAttribute" reason:@"match contains an invalid entry" desc:[NSString stringWithFormat:@"%s:%d", __func__, __LINE__]];
            return;
        }

        [Insider getCurrentUser].mergeObjectsInCustomAttribute(key,
                                                               matchDictionary,
                                                               [RNUtils parseCustomParameters:valueParameters]);
    } @catch (NSException *e) {
        [Insider sendError:e desc:[NSString stringWithFormat:@"%s:%d", __func__, __LINE__]];
    }
}

RCT_EXPORT_METHOD(removeObjectsFromCustomAttribute:(NSString *)key match:(nullable id)match) {
    @try {
        // Typed `id` for the same reason as merge above: a non-array value must not become nil and
        // fall into the wildcard branch. Only nil or NSNull is the native wildcard remove.
        if (match == nil || [match isKindOfClass:[NSNull class]]) {
            [Insider getCurrentUser].removeObjectsFromCustomAttribute(key, nil);
            return;
        }
        if (![match isKindOfClass:[NSArray class]]) {
            [RNInsider reportRefusedMatchInMethod:@"removeObjectsFromCustomAttribute" reason:@"match must be an array" desc:[NSString stringWithFormat:@"%s:%d", __func__, __LINE__]];
            return;
        }

        NSArray *matchArray = (NSArray *)match;
        if (matchArray.count == 0) {
            // A match that was passed but is empty is not the wildcard; the core collapses both into
            // one branch, so a hand-built payload would otherwise wipe the attribute.
            [RNInsider reportRefusedMatchInMethod:@"removeObjectsFromCustomAttribute" reason:@"an empty match would remove every object" desc:[NSString stringWithFormat:@"%s:%d", __func__, __LINE__]];
            return;
        }

        NSDictionary *matchDictionary = [RNUtils parseCustomParameters:matchArray];
        if (![RNInsider isIntactMatch:matchArray parsed:matchDictionary]) {
            // Same fail-closed guard as mergeObjectsInCustomAttribute above.
            [RNInsider reportRefusedMatchInMethod:@"removeObjectsFromCustomAttribute" reason:@"match contains an invalid entry" desc:[NSString stringWithFormat:@"%s:%d", __func__, __LINE__]];
            return;
        }

        [Insider getCurrentUser].removeObjectsFromCustomAttribute(key, matchDictionary);
    } @catch (NSException *e) {
        [Insider sendError:e desc:[NSString stringWithFormat:@"%s:%d", __func__, __LINE__]];
    }
}

RCT_EXPORT_METHOD(unsetCustomAttribute:(NSString *)key) {
    [Insider getCurrentUser].unsetCustomAttribute(key);
}

RCT_EXPORT_METHOD(setGDPRConsent:(BOOL)gdprConsent) {
    [Insider setGDPRConsent:gdprConsent];
}

RCT_EXPORT_METHOD(setMobileAppAccess:(BOOL)mobileAppAccess) {
    [Insider setMobileAppAccess:mobileAppAccess];
}

RCT_EXPORT_METHOD(signUpConfirmation:(NSArray *)customParameters) {
    @try {
        NSDictionary<NSString *, id> *mappedCustomParameters = [RNUtils parseCustomParameters:customParameters];

        if (mappedCustomParameters.count > 0) {
            [Insider signUpConfirmationWithCustomParameters:mappedCustomParameters];
        } else {
            [Insider signUpConfirmation];
        }
    } @catch (NSException *e) {
        [Insider sendError:e desc:[NSString stringWithFormat:@"%s:%d", __func__, __LINE__]];
    }
}

// Insider Product
RCT_EXPORT_METHOD(itemPurchased:(NSString *)saleID requiredFields:(NSDictionary *)requiredFields optionalFields:(NSDictionary *)optionalFields customParameters:(NSArray *)customParameters eventCustomParameters:(NSArray *)eventCustomParameters) {
    @try {
        InsiderProduct *product = [RNUtils parseProductFromRequiredFields:requiredFields andOptionalFields:optionalFields andCustomParameters:customParameters];
        NSDictionary<NSString *, id> *mappedCustomParameters = [RNUtils parseCustomParameters:eventCustomParameters];

        if (mappedCustomParameters.count > 0) {
            [Insider itemPurchasedWithSaleID:saleID product:product customParameters:mappedCustomParameters];
        } else {
            [Insider itemPurchasedWithSaleID:saleID product:product];
        }
    } @catch (NSException *e) {
        [Insider sendError:e desc:[NSString stringWithFormat:@"%s:%d", __func__, __LINE__]];
    }
}

RCT_EXPORT_METHOD(itemAddedToCart:(NSDictionary *)requiredFields optionalFields:(NSDictionary *)optionalFields customParameters:(NSArray *)customParameters eventCustomParameters:(NSArray *)eventCustomParameters) {
    @try {
        InsiderProduct *product = [RNUtils parseProductFromRequiredFields:requiredFields andOptionalFields:optionalFields andCustomParameters:customParameters];
        NSDictionary<NSString *, id> *mappedCustomParameters = [RNUtils parseCustomParameters:eventCustomParameters];

        if (mappedCustomParameters.count > 0) {
            [Insider itemAddedToCartWithProduct:product customParameters:mappedCustomParameters];
        } else {
            [Insider itemAddedToCartWithProduct:product];
        }
    } @catch (NSException *e) {
        [Insider sendError:e desc:[NSString stringWithFormat:@"%s:%d", __func__, __LINE__]];
    }
}

RCT_EXPORT_METHOD(itemRemovedFromCart:(NSString *)productID saleID:(NSString *)saleID customParameters:(NSArray *)customParameters) {
    NSDictionary<NSString *, id> *mappedCustomParameters = [RNUtils parseCustomParameters:customParameters];

    if (saleID.length > 0) {
        [Insider itemRemovedFromCartWithProductID:productID saleID:saleID customParameters:mappedCustomParameters];
    } else if (mappedCustomParameters.count > 0) {
        [Insider itemRemovedFromCartWithProductID:productID customParameters:mappedCustomParameters];
    } else {
        [Insider itemRemovedFromCartWithProductID:productID];
    }
}

RCT_EXPORT_METHOD(cartCleared:(NSArray *)customParameters) {
    NSDictionary<NSString *, id> *mappedCustomParameters = [RNUtils parseCustomParameters:customParameters];

    if (mappedCustomParameters.count > 0) {
        [Insider cartClearedWithCustomParameters:mappedCustomParameters];
    } else {
        [Insider cartCleared];
    }
}

RCT_EXPORT_METHOD(itemAddedToWishlist:(NSDictionary *)requiredFields optionalFields:(NSDictionary *)optionalFields customParameters:(NSArray *)customParameters eventCustomParameters:(NSArray *)eventCustomParameters) {
    @try {
        InsiderProduct *product = [RNUtils parseProductFromRequiredFields:requiredFields andOptionalFields:optionalFields andCustomParameters:customParameters];
        NSDictionary<NSString *, id> *mappedCustomParameters = [RNUtils parseCustomParameters:eventCustomParameters];

        if (mappedCustomParameters.count > 0) {
            [Insider itemAddedToWishlistWithProduct:product customParameters:mappedCustomParameters];
        } else {
            [Insider itemAddedToWishlistWithProduct:product];
        }
    } @catch (NSException *e) {
        [Insider sendError:e desc:[NSString stringWithFormat:@"%s:%d", __func__, __LINE__]];
    }
}

RCT_EXPORT_METHOD(itemRemovedFromWishlist:(NSString *)productID customParameters:(NSArray *)customParameters) {
    NSDictionary<NSString *, id> *mappedCustomParameters = [RNUtils parseCustomParameters:customParameters];

    if (mappedCustomParameters.count > 0) {
        [Insider itemRemovedFromWishlistWithProductID:productID customParameters:mappedCustomParameters];
    } else {
        [Insider itemRemovedFromWishlistWithProductID:productID];
    }
}

RCT_EXPORT_METHOD(wishlistCleared:(NSArray *)customParameters) {
        NSDictionary<NSString *, id> *mappedCustomParameters = [RNUtils parseCustomParameters:customParameters];

        if (mappedCustomParameters.count > 0) {
            [Insider wishlistClearedWithCustomParameters:mappedCustomParameters];
        } else {
            [Insider wishlistCleared];
        }
}

RCT_EXPORT_METHOD(getMessageCenterData:(NSInteger)limit startDate:(NSString *)startDate endDate:(NSString *)endDate callback:(RCTResponseSenderBlock) callback) {
    // The SDK can answer and throw in either order; a second invocation of an RN callback asserts.
    __block BOOL answered = NO;
    NSObject *answerLock = [NSObject new];
    void (^answer)(NSArray *) = ^(NSArray *messageCenterData) {
        BOOL alreadyAnswered;
        @synchronized (answerLock) {
            alreadyAnswered = answered;
            answered = YES;
        }
        if (alreadyAnswered) {
            NSException *duplicate = [NSException exceptionWithName:@"RNInsiderDuplicateAnswer" reason:@"getMessageCenterData answered more than once; the later answer was dropped" userInfo:nil];
            [Insider sendError:duplicate desc:[NSString stringWithFormat:@"%s:%d", __func__, __LINE__]];
            return;
        }
        callback(@[@{@"data": messageCenterData ?: @[]}]);
    };

    @try {
        NSDate *start = [NSDate dateWithTimeIntervalSince1970:[startDate integerValue] / 1000.0];
        NSDate *end = [NSDate dateWithTimeIntervalSince1970:[endDate integerValue] / 1000.0];
        [Insider getMessageCenterDataWithLimit:limit startDate:start endDate:end success:^(NSArray *messageCenterData) {
            answer(messageCenterData);
        }];
    } @catch (NSException *e) {
        [Insider sendError:e desc:[NSString stringWithFormat:@"%s:%d", __func__, __LINE__]];
        answer(@[]);
    }
}

RCT_EXPORT_METHOD(getMessageCenterDataWithIdentifiers:(NSInteger)limit startDate:(NSString *)startDate endDate:(NSString *)endDate identifiers:(NSDictionary *)identifiers callback:(RCTResponseSenderBlock) callback) {
    __block BOOL answered = NO;
    NSObject *answerLock = [NSObject new];
    void (^answer)(NSArray *) = ^(NSArray *payload) {
        BOOL alreadyAnswered;
        @synchronized (answerLock) {
            alreadyAnswered = answered;
            answered = YES;
        }
        if (alreadyAnswered) {
            NSException *duplicate = [NSException exceptionWithName:@"RNInsiderDuplicateAnswer" reason:@"getMessageCenterDataWithIdentifiers answered more than once; the later answer was dropped" userInfo:nil];
            [Insider sendError:duplicate desc:[NSString stringWithFormat:@"%s:%d", __func__, __LINE__]];
            return;
        }
        callback(payload);
    };

    @try {
        NSTimeInterval startDateEpoch = [startDate doubleValue] / 1000.0;
        NSTimeInterval endDateEpoch = [endDate doubleValue] / 1000.0;
        NSDate *start = [NSDate dateWithTimeIntervalSince1970:startDateEpoch];
        NSDate *end = [NSDate dateWithTimeIntervalSince1970:endDateEpoch];

        InsiderIdentifiers *insiderIdentifiers = [[InsiderIdentifiers alloc] init];
        for (NSString *key in identifiers.allKeys) {
            if ([key isEqualToString:ADD_EMAIL]) {
                insiderIdentifiers.addEmail([identifiers objectForKey:key]);
            } else if ([key isEqualToString:ADD_PHONE_NUMBER]) {
                insiderIdentifiers.addPhoneNumber([identifiers objectForKey:key]);
            } else if ([key isEqualToString:ADD_USER_ID]) {
                insiderIdentifiers.addUserID([identifiers objectForKey:key]);
            } else {
                insiderIdentifiers.addCustomIdentifier(key, [identifiers objectForKey:key]);
            }
        }

        [Insider getMessageCenterDataWithLimit:limit
                                     startDate:start
                                       endDate:end
                                   identifiers:insiderIdentifiers
                                       success:^(NSArray *messageCenterData) {
            answer(@[messageCenterData ?: @[]]);
        }];
    } @catch (NSException *e) {
        [Insider sendError:e desc:[NSString stringWithFormat:@"%s:%d", __func__, __LINE__]];
        answer(@[@[]]);
    }
}

RCT_EXPORT_METHOD(getSmartRecommendation:(int)recommendationID locale:(NSString *)locale currency:(NSString *)currency smartRecommendation:(RCTResponseSenderBlock) smartRecommendation) {
    @try {
        [Insider getSmartRecommendationWithID:recommendationID locale:locale currency:currency smartRecommendation:^(NSDictionary *recommendation) {
            smartRecommendation([NSArray arrayWithObject:recommendation]);
        }];
    } @catch (NSException *e) {
        [Insider sendError:e desc:[NSString stringWithFormat:@"%s:%d", __func__, __LINE__]];
        smartRecommendation(@[@{}]);
    }
}

RCT_EXPORT_METHOD(getSmartRecommendationWithProduct:(NSDictionary *)requiredFields optionalFields:(NSDictionary *)optionalFields customParameters:(NSArray *)customParameters recommendationID:(int)recommendationID locale:(NSString *)locale smartRecommendation:(RCTResponseSenderBlock) smartRecommendation) {
    @try {
        InsiderProduct *product = [RNUtils parseProductFromRequiredFields:requiredFields andOptionalFields:optionalFields andCustomParameters:customParameters];
        [Insider getSmartRecommendationWithProduct:product recommendationID:recommendationID locale:locale smartRecommendation:^(NSDictionary *recommendation) {
            smartRecommendation([NSArray arrayWithObject:recommendation]);
        }];
    } @catch (NSException *e) {
        [Insider sendError:e desc:[NSString stringWithFormat:@"%s:%d", __func__, __LINE__]];
        smartRecommendation(@[@{}]);
    }
}

RCT_EXPORT_METHOD(getSmartRecommendationWithProductIDs:(NSArray *)productIDs recommendationID:(int)recommendationID locale:(NSString *)locale currency:(NSString *)currency smartRecommendation:(RCTResponseSenderBlock) smartRecommendation) {
    @try {
        [Insider getSmartRecommendationWithProductIDs:productIDs recommendationID:recommendationID locale:locale currency:currency smartRecommendation:^(NSDictionary *recommendation) {
            smartRecommendation([NSArray arrayWithObject:recommendation]);
        }];
    } @catch (NSException *e) {
        [Insider sendError:e desc:[NSString stringWithFormat:@"%s:%d", __func__, __LINE__]];
        smartRecommendation(@[@{}]);
    }
}

RCT_EXPORT_METHOD(clickSmartRecommendationProduct:(int)recommendationID requiredFields:(NSDictionary *)requiredFields optionalFields:(NSDictionary *)optionalFields customParameters:(NSArray *)customParameters) {
    @try {
        InsiderProduct *product = [RNUtils parseProductFromRequiredFields:requiredFields andOptionalFields:optionalFields andCustomParameters:customParameters];
        [Insider clickSmartRecommendationProductWithID:recommendationID product:product];
    } @catch (NSException *e) {
        [Insider sendError:e desc:[NSString stringWithFormat:@"%s:%d", __func__, __LINE__]];
    }
}

// An unknown dataType answers the default silently, without asking the SDK.
RCT_EXPORT_METHOD(getContentStringWithName:(NSString *)variableName defaultValue:(NSString *)defaultValue dataType:(int)dataType callback:(RCTResponseSenderBlock)callback) {
    NSArray *arguments = RNInsiderCallbackArguments(defaultValue);
    @try {
        BOOL isValid = NO;
        ContentOptimizerDataType type = [RNUtils contentDataTypeFromInt:dataType isValid:&isValid];
        if (isValid) {
            arguments = RNInsiderCallbackArguments([Insider getContentStringWithName:variableName defaultString:defaultValue dataType:type]);
        }
    } @catch (NSException *e) {
        [Insider sendError:e desc:[NSString stringWithFormat:@"%s:%d", __func__, __LINE__]];
    }
    callback(arguments);
}

RCT_EXPORT_METHOD(getContentBoolWithName:(NSString *)variableName defaultValue:(BOOL)defaultValue dataType:(int)dataType callback:(RCTResponseSenderBlock)callback) {
    BOOL value = defaultValue;
    @try {
        BOOL isValid = NO;
        ContentOptimizerDataType type = [RNUtils contentDataTypeFromInt:dataType isValid:&isValid];
        if (isValid) {
            value = [Insider getContentBoolWithName:variableName defaultBool:defaultValue dataType:type];
        }
    } @catch (NSException *e) {
        [Insider sendError:e desc:[NSString stringWithFormat:@"%s:%d", __func__, __LINE__]];
        value = defaultValue;
    }
    callback(@[@(value)]);
}

RCT_EXPORT_METHOD(getContentIntWithName:(NSString *)variableName defaultValue:(int)defaultValue dataType:(int)dataType callback:(RCTResponseSenderBlock)callback) {
    int value = defaultValue;
    @try {
        BOOL isValid = NO;
        ContentOptimizerDataType type = [RNUtils contentDataTypeFromInt:dataType isValid:&isValid];
        if (isValid) {
            value = (int)[Insider getContentIntWithName:variableName defaultInt:defaultValue dataType:type];
        }
    } @catch (NSException *e) {
        [Insider sendError:e desc:[NSString stringWithFormat:@"%s:%d", __func__, __LINE__]];
        value = defaultValue;
    }
    callback(@[@(value)]);
}

RCT_EXPORT_METHOD(getContentStringWithoutCache:(NSString *)variableName defaultValue:(NSString *)defaultValue dataType:(int)dataType callback:(RCTResponseSenderBlock)callback) {
    NSArray *arguments = RNInsiderCallbackArguments(defaultValue);
    @try {
        BOOL isValid = NO;
        ContentOptimizerDataType type = [RNUtils contentDataTypeFromInt:dataType isValid:&isValid];
        if (isValid) {
            arguments = RNInsiderCallbackArguments([Insider getContentStringWithoutCache:variableName defaultString:defaultValue dataType:type]);
        }
    } @catch (NSException *e) {
        [Insider sendError:e desc:[NSString stringWithFormat:@"%s:%d", __func__, __LINE__]];
    }
    callback(arguments);
}

RCT_EXPORT_METHOD(getContentBoolWithoutCache:(NSString *)variableName defaultValue:(BOOL)defaultValue dataType:(int)dataType callback:(RCTResponseSenderBlock)callback) {
    BOOL value = defaultValue;
    @try {
        BOOL isValid = NO;
        ContentOptimizerDataType type = [RNUtils contentDataTypeFromInt:dataType isValid:&isValid];
        if (isValid) {
            value = [Insider getContentBoolWithoutCache:variableName defaultBool:defaultValue dataType:type];
        }
    } @catch (NSException *e) {
        [Insider sendError:e desc:[NSString stringWithFormat:@"%s:%d", __func__, __LINE__]];
        value = defaultValue;
    }
    callback(@[@(value)]);
}

RCT_EXPORT_METHOD(getContentIntWithoutCache:(NSString *)variableName defaultValue:(int)defaultValue dataType:(int)dataType callback:(RCTResponseSenderBlock)callback) {
    int value = defaultValue;
    @try {
        BOOL isValid = NO;
        ContentOptimizerDataType type = [RNUtils contentDataTypeFromInt:dataType isValid:&isValid];
        if (isValid) {
            value = (int)[Insider getContentIntWithoutCache:variableName defaultInt:defaultValue dataType:type];
        }
    } @catch (NSException *e) {
        [Insider sendError:e desc:[NSString stringWithFormat:@"%s:%d", __func__, __LINE__]];
        value = defaultValue;
    }
    callback(@[@(value)]);
}

RCT_EXPORT_METHOD(markContentOptimizerAsSeen:(NSString *)variableName) {
    @try {
        [Insider markContentOptimizerAsSeen:variableName];
    } @catch (NSException *e){
        [Insider sendError:e desc:[NSString stringWithFormat:@"%s:%d", __func__, __LINE__]];
    }
}

RCT_EXPORT_METHOD(visitHomePage:(NSArray *)customParameters) {
    NSDictionary<NSString *, id> *mappedCustomParameters = [RNUtils parseCustomParameters:customParameters];

    if (mappedCustomParameters.count > 0) {
        [Insider visitHomepageWithCustomParameters:mappedCustomParameters];
    } else {
        [Insider visitHomepage];
    }
}

RCT_EXPORT_METHOD(visitListingPage:(NSArray *)taxonomy customParameters:(NSArray *)customParameters) {
    NSDictionary<NSString *, id> *mappedCustomParameters = [RNUtils parseCustomParameters:customParameters];

    if (mappedCustomParameters.count > 0) {
        [Insider visitListingPageWithTaxonomy:taxonomy customParameters:mappedCustomParameters];
    } else {
        [Insider visitListingPageWithTaxonomy:taxonomy];
    }
}

RCT_EXPORT_METHOD(visitProductDetailPage:(NSDictionary *)requiredFields optionalFields:(NSDictionary *)optionalFields customParameters:(NSArray *)customParameters eventCustomParameters:(NSArray *)eventCustomParameters) {
    @try {
        InsiderProduct *product = [RNUtils parseProductFromRequiredFields:requiredFields andOptionalFields:optionalFields andCustomParameters:customParameters];
        NSDictionary<NSString *, id> *mappedCustomParameters = [RNUtils parseCustomParameters:eventCustomParameters];

        if (mappedCustomParameters.count > 0) {
            [Insider visitProductDetailPageWithProduct:product customParameters:mappedCustomParameters];
        } else {
            [Insider visitProductDetailPageWithProduct:product];
        }
    } @catch (NSException *e) {
        [Insider sendError:e desc:[NSString stringWithFormat:@"%s:%d", __func__, __LINE__]];
    }
}

RCT_EXPORT_METHOD(visitCartPage:(NSArray *)products saleID:(NSString *)saleID customParameters:(NSArray *)customParameters) {
    @try {
        NSMutableArray<InsiderProduct *> *mappedProducts = [NSMutableArray array];
        for (NSDictionary *productDict in products) {
            NSDictionary *requiredFields = productDict[@"requiredFields"];
            NSDictionary *optionalFields = productDict[@"optionalFields"];
            NSArray *productCustomParameters = productDict[@"customParameters"];
            InsiderProduct *product = [RNUtils parseProductFromRequiredFields:requiredFields andOptionalFields:optionalFields andCustomParameters:productCustomParameters];
            [mappedProducts addObject:product];
        }

        NSDictionary<NSString *, id> *mappedCustomParameters = [RNUtils parseCustomParameters:customParameters];

        if (saleID.length > 0) {
            [Insider visitCartPageWithProducts:[mappedProducts copy] saleID:saleID customParameters:mappedCustomParameters];
        } else if (mappedCustomParameters.count > 0) {
            [Insider visitCartPageWithProducts:[mappedProducts copy] customParameters:mappedCustomParameters];
        } else {
            [Insider visitCartPageWithProducts:[mappedProducts copy]];
        }
    } @catch (NSException *e) {
        [Insider sendError:e desc:[NSString stringWithFormat:@"%s:%d", __func__, __LINE__]];
    }
}

RCT_EXPORT_METHOD(visitWishlistPage:(NSArray *)products customParameters:(NSArray *)customParameters) {
    @try {
        NSMutableArray<InsiderProduct *> *mappedProducts = [NSMutableArray array];
        for (NSDictionary *productDict in products) {
            NSDictionary *requiredFields = productDict[@"requiredFields"];
            NSDictionary *optionalFields = productDict[@"optionalFields"];
            NSArray *productCustomParameters = productDict[@"customParameters"];
            InsiderProduct *product = [RNUtils parseProductFromRequiredFields:requiredFields andOptionalFields:optionalFields andCustomParameters:productCustomParameters];
            [mappedProducts addObject:product];
        }

        NSDictionary<NSString *, id> *mappedCustomParameters = [RNUtils parseCustomParameters:customParameters];

        if (mappedCustomParameters.count > 0) {
            [Insider visitWishlistWithProducts:[mappedProducts copy] customParameters:mappedCustomParameters];
        } else {
            [Insider visitWishlistWithProducts:[mappedProducts copy]];
        }
    } @catch (NSException *e) {
        [Insider sendError:e desc:[NSString stringWithFormat:@"%s:%d", __func__, __LINE__]];
    }
}

RCT_EXPORT_METHOD(startTrackingGeofence) {
    [InsiderGeofence startTracking];
}

RCT_EXPORT_METHOD(setAllowsBackgroundLocationUpdates:(BOOL)allowsBackgroundLocationUpdates) {
    [InsiderGeofence setAllowsBackgroundLocationUpdates:allowsBackgroundLocationUpdates];
}

RCT_EXPORT_METHOD(handleNotification:(NSDictionary *)notification) {
    @try {
        NSMutableDictionary *mutableNotification = [notification mutableCopy];
        mutableNotification[@"aps"] = @"insider";
        [Insider handlePushLogWithUserInfo:mutableNotification];
        [Insider trackInteractiveLogWithUserInfo:mutableNotification];
    } @catch (NSException *e) {
        [Insider sendError:e desc:[NSString stringWithFormat:@"%s:%d", __func__, __LINE__]];
    }
}

RCT_EXPORT_METHOD(triggerPushProcessWithNotificationData:(NSDictionary *)notification) {
    [self handleNotification:notification];
}

RCT_EXPORT_METHOD(enableIDFACollection:(BOOL)enableIDFACollection) {
    [Insider enableIDFACollection:enableIDFACollection];
}

RCT_EXPORT_METHOD(removeInapp) {
    [Insider removeInapp];
}

RCT_EXPORT_METHOD(registerWithQuietPermission:(BOOL)enabled) {
    [Insider registerWithQuietPermission:enabled];
}

RCT_EXPORT_METHOD(putErrorLog:(NSString *)errorString) {
    NSException *exception = [NSException exceptionWithName:@"[JavaScript Error]" reason:errorString userInfo:nil];
    [Insider sendError:exception desc:[NSString stringWithFormat:@"%s:%d", __func__, __LINE__]];
}

RCT_EXPORT_METHOD(showNativeAppReview) {
    [Insider showNativeAppReview];
}

RCT_EXPORT_METHOD(enableLocationCollection:(BOOL)enabled) {
    [Insider enableLocationCollection:enabled];
}

RCT_EXPORT_METHOD(enableIpCollection:(BOOL)enabled) {
    [Insider enableIpCollection:enabled];
}

RCT_EXPORT_METHOD(enableCarrierCollection:(BOOL)enabled) {
    [Insider enableCarrierCollection:enabled];
}

RCT_EXPORT_METHOD(setForegroundPushCallback) {
    [Insider setForegroundPushCallback:@selector(foregroundPushCallback:) sender:self];
}

RCT_EXPORT_METHOD(setActiveForegroundPushView) {
    [Insider setActiveForegroundPushView];
}

RCT_EXPORT_METHOD(disableTemplates:(NSString *)value) {
    [Insider disableTemplates];
}

RCT_EXPORT_METHOD(reenableTemplates:(NSString *)value) {
    [Insider reenableTemplates];
}

RCT_EXPORT_METHOD(getInsiderID:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) {
    @try {
        resolve([Insider getInsiderID]);
    } @catch (NSException *exception) {
        [Insider sendError:exception desc:[NSString stringWithFormat:@"%s:%d", __func__, __LINE__]];

        resolve(nil);
    }
}

RCT_EXPORT_METHOD(registerInsiderIDListener) {
    [Insider registerInsiderIDListenerWithSelector:@selector(insiderIDChangeListener:) sender:self];
}

RCT_EXPORT_METHOD(registerInsiderEventListener) {
    @try {
        if (insiderEventListener != nil) return;

        insiderEventListener = [[RNInsiderEventListener alloc] init];

        [[Insider events] addObserver:insiderEventListener];
    } @catch (NSException *e) {
        insiderEventListener = nil;
        [Insider sendError:e desc:[NSString stringWithFormat:@"%s:%d", __func__, __LINE__]];
    }
}

RCT_EXPORT_METHOD(unregisterInsiderEventListener) {
    @try {
        if (insiderEventListener == nil) return;

        [[Insider events] removeObserver:insiderEventListener];

        // Dropping our strong reference is what actually deallocates the observer,
        // since the SDK only holds it weakly.
        insiderEventListener = nil;
    } @catch (NSException *e) {
        [Insider sendError:e desc:[NSString stringWithFormat:@"%s:%d", __func__, __LINE__]];
    }
}

RCT_EXPORT_METHOD(disableInAppMessages) {
    [Insider disableInAppMessages];
}

RCT_EXPORT_METHOD(enableInAppMessages) {
    [Insider enableInAppMessages];
}

RCT_EXPORT_METHOD(getAppCardsCampaigns:(RCTResponseSenderBlock)callback) {
    @try {
        [[Insider appCards] getCampaigns:^(InsiderAppCardCampaignsResponseModel *campaignResponse, NSError *error) {
            if (error) {
                NSDictionary *errorDetails = [RNUtils appCardsErrorToDictionary:error];
                callback(@[errorDetails]);
            } else {
                callback(@[[NSNull null], campaignResponse.toDictionary ?: @{}]);
            }
        }];
    } @catch (NSException *e) {
        [Insider sendError:e desc:[NSString stringWithFormat:@"%s:%d", __func__, __LINE__]];
        callback(@[@{@"code": @"unknown", @"message": @"An unexpected error occurred."}]);
    }
}

RCT_EXPORT_METHOD(markAppCardsAsRead:(NSArray<NSString *> *)appCardIds callback:(RCTResponseSenderBlock)callback) {
    @try {
        [[Insider appCards] markAsRead:[NSSet setWithArray:appCardIds] completion:^(NSError *error) {
            if (error) {
                NSDictionary *errorDetails = [RNUtils appCardsErrorToDictionary:error];
                callback(@[errorDetails]);
            } else {
                callback(@[]);
            }
        }];
    } @catch (NSException *e) {
        [Insider sendError:e desc:[NSString stringWithFormat:@"%s:%d", __func__, __LINE__]];
        callback(@[@{@"code": @"unknown", @"message": @"An unexpected error occurred."}]);
    }
}

RCT_EXPORT_METHOD(markAppCardsAsUnread:(NSArray<NSString *> *)appCardIds callback:(RCTResponseSenderBlock)callback) {
    @try {
        [[Insider appCards] markAsUnread:[NSSet setWithArray:appCardIds] completion:^(NSError *error) {
            if (error) {
                NSDictionary *errorDetails = [RNUtils appCardsErrorToDictionary:error];
                callback(@[errorDetails]);
            } else {
                callback(@[]);
            }
        }];
    } @catch (NSException *e) {
        [Insider sendError:e desc:[NSString stringWithFormat:@"%s:%d", __func__, __LINE__]];
        callback(@[@{@"code": @"unknown", @"message": @"An unexpected error occurred."}]);
    }
}

RCT_EXPORT_METHOD(deleteAppCards:(NSArray<NSString *> *)appCardIds callback:(RCTResponseSenderBlock)callback) {
    @try {
        [[Insider appCards] deleteAppCards:[NSSet setWithArray:appCardIds] completion:^(NSError *error) {
            if (error) {
                NSDictionary *errorDetails = [RNUtils appCardsErrorToDictionary:error];
                callback(@[errorDetails]);
            } else {
                callback(@[]);
            }
        }];
    } @catch (NSException *e) {
        [Insider sendError:e desc:[NSString stringWithFormat:@"%s:%d", __func__, __LINE__]];
        callback(@[@{@"code": @"unknown", @"message": @"An unexpected error occurred."}]);
    }
}

RCT_EXPORT_METHOD(clickAppCardButton:(NSString *)appCardId data:(NSDictionary *)data) {
    InsiderAppCardButtonModel *model = [[InsiderAppCardButtonModel alloc] initWithDictionary:data error:nil];
    if (model) {
        [model setAppCardId:appCardId];
        [model click];
    }
}

RCT_EXPORT_METHOD(viewAppCard:(NSDictionary *)data) {
    InsiderAppCardModel *model = [[InsiderAppCardModel alloc] initWithDictionary:data error:nil];
    if (model) {
        [model view];
    }
}

RCT_EXPORT_METHOD(clickAppCard:(NSDictionary *)data) {
    InsiderAppCardModel *model = [[InsiderAppCardModel alloc] initWithDictionary:data error:nil];
    if (model) {
        [model click];
    }
}

- (void)insiderIDChangeListener:(NSString *) insiderID {
    @try {
        if (insiderID == nil) return;

        RNNotificationHandler *handler = [[RNNotificationHandler alloc] init];

        [handler sendDataToJS:INSIDER_ID_LISTENER data:insiderID];
    } @catch (NSException *e) {
        [Insider sendError:e desc:[NSString stringWithFormat:@"%s:%d", __func__, __LINE__]];
    }
}

- (void)foregroundPushCallback:(UNNotification *) notification {
  @try {
        if (notification == nil) return;

        RNNotificationHandler *handler = [[RNNotificationHandler alloc] init];

        [handler sendDataToJS:@"FOREGROUND_PUSH" data:notification.request.content.userInfo];
    } @catch (NSException *e) {
        [Insider sendError:e desc:[NSString stringWithFormat:@"%s:%d", __func__, __LINE__]];
    }
}

- (void)emitEvent:(NSDictionary *)eventData {
    @try {
        RNNotificationHandler *handler = [[RNNotificationHandler alloc] init];
        if (!eventData || [eventData count] == 0)
        return;
        InsiderCallbackType type = (InsiderCallbackType)[[eventData objectForKey:@"type"] intValue];
        switch (type) {
            case InsiderCallbackTypeNotificationOpen:
                [handler sendDataToJS:NOTIFICATION_OPEN data:eventData];
                break;
            case InsiderCallbackTypeInappButtonClick:
                [handler sendDataToJS:INAPP_BUTTON_CLICK data:eventData];
                break;
            case InsiderCallbackTypeTempStorePurchase:
                [handler sendDataToJS:TEMP_STORE_PURCHASE data:eventData];
                break;
            case InsiderCallbackTypeTempStoreAddedToCart:
                [handler sendDataToJS:TEMP_STORE_ADDED_TO_CART data:eventData];
                break;
            case InsiderCallbackTypeTempStoreCustomAction:
                [handler sendDataToJS:TEMP_STORE_CUSTOM_ACTION data:eventData];
                break;
            case InsiderCallbackTypeInAppSeen:
                [handler sendDataToJS:INAPP_SEEN data:eventData];
                break;
            case InsiderCallbackTypeSessionStarted:
                [handler sendDataToJS:SESSION_STARTED data:eventData];
                break;
            default:
                break;
        }
    } @catch (NSException *e) {
        [Insider sendError:e desc:[NSString stringWithFormat:@"%s:%d", __func__, __LINE__]];
    }
}

@end
