#import "RNRadar.h"
#include <Foundation/Foundation.h>
#import <CoreLocation/CoreLocation.h>
#import <React/RCTConvert.h>

static NSString *_publishableKey = nil;

@implementation RNRadar {
    CLLocationManager *locationManager;
    RCTPromiseResolveBlock permissionsRequestResolver;
    bool hasListeners;
    #ifdef RCT_NEW_ARCH_ENABLED
    bool jsEventEmitterReady;
    #endif
}

RCT_EXPORT_MODULE()

- (instancetype)init {
    self = [super init];
    if (self) {
        locationManager = [CLLocationManager new];
        locationManager.delegate = self;
        [Radar setDelegate:self];
        [Radar setVerifiedDelegate:self];
        [Radar setInAppMessageDelegate:self];
    }
    return self;
}

- (void)locationManager:(CLLocationManager *)manager didChangeAuthorizationStatus:(CLAuthorizationStatus)status {
    if (permissionsRequestResolver) {
        [self getPermissionsStatus:permissionsRequestResolver reject:nil];
        permissionsRequestResolver = nil;
    }
}

+ (BOOL)requiresMainQueueSetup {
    return YES;
}

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

- (NSArray<NSString *> *)supportedEvents {
    return @[@"eventsEmitter", @"locationEmitter", @"clientLocationEmitter", @"errorEmitter", @"logEmitter", @"tokenEmitter", @"newInAppMessageEmitter", @"inAppMessageDismissedEmitter", @"inAppMessageClickedEmitter"];
}

- (void)startObserving {
    hasListeners = YES;
}

- (void)stopObserving {
    hasListeners = NO;
}

- (void)invalidate {
#ifdef RCT_NEW_ARCH_ENABLED
    jsEventEmitterReady = NO;
#endif
    hasListeners = NO;

    // Detach from the process-level Radar singleton so a stale module
    // instance doesn't continue to receive events.
    [Radar setDelegate:nil];
    [Radar setVerifiedDelegate:nil];
    [Radar setInAppMessageDelegate:nil];

    locationManager.delegate = nil;
    [[NSNotificationCenter defaultCenter] removeObserver:self];
}

#ifdef RCT_NEW_ARCH_ENABLED
- (void)setEventEmitterCallback:(EventEmitterCallbackWrapper *)eventEmitterCallbackWrapper {
    [super setEventEmitterCallback:eventEmitterCallbackWrapper];
    jsEventEmitterReady = YES;
}
#endif

- (void)onNewInAppMessage:(RadarInAppMessage *)inAppMessage {

    NSMutableDictionary *body = [NSMutableDictionary new];
    [body setValue:[Radar dictionaryForInAppMessage:inAppMessage] forKey:@"inAppMessage"];
    #ifdef RCT_NEW_ARCH_ENABLED
    if (jsEventEmitterReady) {
        [self emitNewInAppMessageEmitter:body];
    }
    #else
    if (hasListeners) {
        [self sendEventWithName:@"newInAppMessageEmitter" body:body];
    }
    #endif
}

- (void)onInAppMessageDismissed:(RadarInAppMessage *)inAppMessage {
    NSMutableDictionary *body = [NSMutableDictionary new];
    [body setValue:[Radar dictionaryForInAppMessage:inAppMessage] forKey:@"inAppMessage"];
    #ifdef RCT_NEW_ARCH_ENABLED
    if (jsEventEmitterReady) {
        [self emitInAppMessageDismissedEmitter:body];
    }
    #else
    if (hasListeners) {
        [self sendEventWithName:@"inAppMessageDismissedEmitter" body:body];
    }
    #endif
}

- (void)onInAppMessageButtonClicked:(RadarInAppMessage *)inAppMessage {
    NSMutableDictionary *body = [NSMutableDictionary new];
    [body setValue:[Radar dictionaryForInAppMessage:inAppMessage] forKey:@"inAppMessage"];
    #ifdef RCT_NEW_ARCH_ENABLED
    if (jsEventEmitterReady) {
        [self emitInAppMessageClickedEmitter:body];
    }
    #else
    if (hasListeners) {
        [self sendEventWithName:@"inAppMessageClickedEmitter" body:body];
    }
    #endif
}

- (void) createInAppMessageView:(RadarInAppMessage * _Nonnull)message
                      onDismiss:(void (^)(void))onDismiss
          onInAppMessageClicked:(void (^)(void))onInAppMessageClicked
              completionHandler:(void (^)(UIViewController *))completionHandler {
                RadarInAppMessageDelegate *delegate = [RadarInAppMessageDelegate new];
                [delegate createInAppMessageView:message onDismiss:onDismiss onInAppMessageClicked:onInAppMessageClicked completionHandler:completionHandler];

}

- (void)didReceiveEvents:(NSArray<RadarEvent *> *)events user:(RadarUser * _Nullable )user {
    NSMutableDictionary *body = [NSMutableDictionary new];
    [body setValue:[RadarEvent arrayForEvents:events] forKey:@"events"];
    if (user) {
        [body setValue:[user dictionaryValue] forKey:@"user"];
    }
    #ifdef RCT_NEW_ARCH_ENABLED
    if (jsEventEmitterReady) {
        [self emitEventsEmitter:body];
    }
    #else
    if (hasListeners) {
        [self sendEventWithName:@"eventsEmitter" body:body];
    }
    #endif
}

- (void)didUpdateLocation:(CLLocation *)location user:(RadarUser *)user {
    #ifdef RCT_NEW_ARCH_ENABLED
    if (jsEventEmitterReady) {
        [self emitLocationEmitter:@{
            @"location": [Radar dictionaryForLocation:location],
            @"user": [user dictionaryValue]
        }];
    }
    #else
    if (hasListeners) {
        [self sendEventWithName:@"locationEmitter" body:@{
            @"location": [Radar dictionaryForLocation:location],
            @"user": [user dictionaryValue]
        }];
    }
    #endif
}

- (void)didUpdateClientLocation:(CLLocation *)location stopped:(BOOL)stopped source:(RadarLocationSource)source {
    #ifdef RCT_NEW_ARCH_ENABLED
    if (jsEventEmitterReady) {
        [self emitClientLocationEmitter:@{
            @"location": [Radar dictionaryForLocation:location],
            @"stopped": @(stopped),
            @"source": [Radar stringForLocationSource:source]
        }];
    }
    #else
    if (hasListeners) {
        [self sendEventWithName:@"clientLocationEmitter" body:@{
            @"location": [Radar dictionaryForLocation:location],
            @"stopped": @(stopped),
            @"source": [Radar stringForLocationSource:source]
        }];
    }
    #endif
}

- (void)didFailWithStatus:(RadarStatus)status {
    NSDictionary *body = @{
        @"status": [Radar stringForStatus:status]
    };
    #ifdef RCT_NEW_ARCH_ENABLED
    if (jsEventEmitterReady) {
        [self emitErrorEmitter:body];
    }
    #else
    if (hasListeners) {
        [self sendEventWithName:@"errorEmitter" body:body];
    }
    #endif
}

- (void)didLogMessage:(NSString *)message {
    NSDictionary *body = @{
        @"message": message
    };
    #ifdef RCT_NEW_ARCH_ENABLED
    if (jsEventEmitterReady) {
        [self emitLogEmitter:body];
    }
    #else
    if (hasListeners) {
        [self sendEventWithName:@"logEmitter" body:body];
    }
    #endif
}

- (void)didUpdateToken:(RadarVerifiedLocationToken *)token {
    NSDictionary *body = @{
        @"token": [token dictionaryValue]
    };
    #ifdef RCT_NEW_ARCH_ENABLED
    if (jsEventEmitterReady) {
        [self emitTokenEmitter:body];
    }
    #else
    if (hasListeners) {
        [self sendEventWithName:@"tokenEmitter" body:body];
    }
    #endif
}

RCT_EXPORT_METHOD(initialize:(NSString *)publishableKey fraud:(BOOL)fraud options:(NSDictionary *)options) {
    _publishableKey = publishableKey; 
    [[NSUserDefaults standardUserDefaults] setObject:@"ReactNative" forKey:@"radar-xPlatformSDKType"];
    [[NSUserDefaults standardUserDefaults] setObject:@"4.35.0" forKey:@"radar-xPlatformSDKVersion"];
    
    RadarInitializeOptions *radarOptions = [[RadarInitializeOptions alloc] init];
    if (options != nil) {
        id silentPushValue = options[@"silentPush"];
        if (silentPushValue && silentPushValue != [NSNull null]) {
            radarOptions.silentPush = [silentPushValue boolValue];
        }
        id autoLogValue = options[@"autoLogNotificationConversions"];
        if (autoLogValue && autoLogValue != [NSNull null]) {
            radarOptions.autoLogNotificationConversions = [autoLogValue boolValue];
        }
        id autoHandleValue = options[@"autoHandleNotificationDeepLinks"];
        if (autoHandleValue && autoHandleValue != [NSNull null]) {
            radarOptions.autoHandleNotificationDeepLinks = [autoHandleValue boolValue];
        }
    }
    [Radar initializeWithPublishableKey:publishableKey options:radarOptions];
}

RCT_EXPORT_METHOD(initializeWithAuthToken:(NSString *)authToken fraud:(BOOL)fraud options:(NSDictionary *)options) {
    _publishableKey = nil;
    [[NSUserDefaults standardUserDefaults] setObject:@"ReactNative" forKey:@"radar-xPlatformSDKType"];
    [[NSUserDefaults standardUserDefaults] setObject:@"4.35.0" forKey:@"radar-xPlatformSDKVersion"];
    RadarInitializeOptions *radarOptions = [[RadarInitializeOptions alloc] init];
    if (options != nil) {
        id silentPushValue = options[@"silentPush"];
        if (silentPushValue && silentPushValue != [NSNull null]) {
            radarOptions.silentPush = [silentPushValue boolValue];
        }
        id autoLogValue = options[@"autoLogNotificationConversions"];
        if (autoLogValue && autoLogValue != [NSNull null]) {
            radarOptions.autoLogNotificationConversions = [autoLogValue boolValue];
        }
        id autoHandleValue = options[@"autoHandleNotificationDeepLinks"];
        if (autoHandleValue && autoHandleValue != [NSNull null]) {
            radarOptions.autoHandleNotificationDeepLinks = [autoHandleValue boolValue];
        }
    }
    [Radar initializeWithAuthToken:authToken options:radarOptions];
}

RCT_EXPORT_METHOD(setLogLevel:(NSString *)level) {
    RadarLogLevel logLevel = RadarLogLevelNone;
    if (level) {
        if ([level isEqualToString:@"error"] || [level isEqualToString:@"ERROR"]) {
            logLevel = RadarLogLevelError;
        } else if ([level isEqualToString:@"warning"] || [level isEqualToString:@"WARNING"]) {
            logLevel = RadarLogLevelWarning;
        } else if ([level isEqualToString:@"info"] || [level isEqualToString:@"INFO"]) {
            logLevel = RadarLogLevelInfo;
        } else if ([level isEqualToString:@"debug"] || [level isEqualToString:@"DEBUG"]) {
            logLevel = RadarLogLevelDebug;
        }
    }
    [Radar setLogLevel:logLevel];
}

RCT_EXPORT_METHOD(setUserId:(NSString *)userId) {
    [Radar setUserId:userId];
}

RCT_EXPORT_METHOD(getUserId:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject) {
    resolve([Radar getUserId]);
}

RCT_EXPORT_METHOD(setDescription:(NSString *)description) {
    [Radar setDescription:description];
}

RCT_EXPORT_METHOD(getDescription:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject) {
    resolve([Radar getDescription]);
}

RCT_EXPORT_METHOD(setMetadata:(NSDictionary *)metadataDict) {
    [Radar setMetadata:metadataDict];
}

RCT_EXPORT_METHOD(getMetadata:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject) {
    resolve([Radar getMetadata]);
}

RCT_EXPORT_METHOD(setTags:(NSArray<NSString *> *)tags) {
    [Radar setTags:tags];
}

RCT_EXPORT_METHOD(getTags:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject) {
    resolve([Radar getTags]);
}

RCT_EXPORT_METHOD(addTags:(NSArray<NSString *> *)tags) {
    [Radar addTags:tags];
}

RCT_EXPORT_METHOD(removeTags:(NSArray<NSString *> *)tags) {
    [Radar removeTags:tags];
}

RCT_EXPORT_METHOD(setProduct:(NSString *)product) {
    [Radar setProduct:product];
}

RCT_EXPORT_METHOD(getProduct:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject) {
    resolve([Radar getProduct]);
}

RCT_EXPORT_METHOD(setUserLanguage:(NSString *)userLanguage) {
    [Radar setUserLanguage:userLanguage];
}

RCT_EXPORT_METHOD(getUserLanguage:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject) {
    resolve([Radar getUserLanguage]);
}

RCT_EXPORT_METHOD(isSharing:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject) {
    resolve(@([Radar isSharing]));
}

RCT_EXPORT_METHOD(clearSharing) {
    [Radar clearSharing];
}

RCT_EXPORT_METHOD(setAnonymousTrackingEnabled:(BOOL)enabled) {
    [Radar setAnonymousTrackingEnabled:enabled];
}

RCT_EXPORT_METHOD(getHost:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject) {
    resolve(@"https://api.radar.io");
}

RCT_EXPORT_METHOD(getPublishableKey:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject) {
    resolve(_publishableKey);
}

RCT_EXPORT_METHOD(getMobileOrigin:(RCTPromiseResolveBlock) resolve reject:(RCTPromiseRejectBlock)reject) {
    resolve([[NSBundle mainBundle] bundleIdentifier]);
}

RCT_EXPORT_METHOD(getPermissionsStatus:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject) {
    CLAuthorizationStatus status = [CLLocationManager authorizationStatus];
    NSString *statusStr;
    switch (status) {
        case kCLAuthorizationStatusDenied:
            statusStr = @"DENIED";
            break;
        case kCLAuthorizationStatusRestricted:
            statusStr = @"DENIED";
            break;
        case kCLAuthorizationStatusAuthorizedAlways:
            statusStr = @"GRANTED_BACKGROUND";
            break;
        case kCLAuthorizationStatusAuthorizedWhenInUse:
            statusStr = @"GRANTED_FOREGROUND";
            break;
        case kCLAuthorizationStatusNotDetermined:
            statusStr = @"NOT_DETERMINED";
            break;
        default:
            statusStr = @"DENIED";
            break;
    }
    resolve(statusStr);
}

RCT_EXPORT_METHOD(requestPermissions:(BOOL)background resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject) {
    CLAuthorizationStatus status = [CLLocationManager authorizationStatus];
    if (background && status == kCLAuthorizationStatusAuthorizedWhenInUse) {
        permissionsRequestResolver = resolve;
        [locationManager requestAlwaysAuthorization];
        [[NSNotificationCenter defaultCenter] addObserver:self
                                             selector:@selector(handleAppBecomingActive)
                                                 name:UIApplicationDidBecomeActiveNotification
                                               object:nil];
    } else if (status == kCLAuthorizationStatusNotDetermined) {
        permissionsRequestResolver = resolve;
        [locationManager requestWhenInUseAuthorization];
    } else {
        [self getPermissionsStatus:resolve reject:reject];
        permissionsRequestResolver = nil;
    }
}

- (void)handleAppBecomingActive
{
  [[NSNotificationCenter defaultCenter] removeObserver:self];
     if (permissionsRequestResolver) {
        [self getPermissionsStatus:permissionsRequestResolver reject:nil];
        permissionsRequestResolver = nil;
    }
}

RCT_EXPORT_METHOD(getLocation:(NSString *)desiredAccuracy resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject) {
    __block RCTPromiseResolveBlock resolver = resolve;
    __block RCTPromiseRejectBlock rejecter = reject;
    RadarTrackingOptionsDesiredAccuracy accuracy = RadarTrackingOptionsDesiredAccuracyMedium;

    if (desiredAccuracy) {
        NSString *lowerAccuracy = [desiredAccuracy lowercaseString];
        if ([lowerAccuracy isEqualToString:@"high"]) {
            accuracy = RadarTrackingOptionsDesiredAccuracyHigh;
        } else if ([lowerAccuracy isEqualToString:@"medium"]) {
            accuracy = RadarTrackingOptionsDesiredAccuracyMedium;
        } else if ([lowerAccuracy isEqualToString:@"low"]) {
            accuracy = RadarTrackingOptionsDesiredAccuracyLow;
        } else {
            if (reject) {
                reject([Radar stringForStatus:RadarStatusErrorBadRequest], [Radar stringForStatus:RadarStatusErrorBadRequest], nil);
            }

            return;
        }
    }

    [Radar getLocationWithDesiredAccuracy:accuracy completionHandler:^(RadarStatus status, CLLocation * _Nullable location, BOOL stopped) {
        if (status == RadarStatusSuccess && resolver) {
            NSMutableDictionary *dict = [NSMutableDictionary new];
            [dict setObject:[Radar stringForStatus:status] forKey:@"status"];
            if (location) {
                [dict setObject:[Radar dictionaryForLocation:location] forKey:@"location"];
            }
            [dict setObject:@(stopped) forKey:@"stopped"];
            resolver(dict);
        } else if (rejecter) {
            rejecter([Radar stringForStatus:status], [Radar stringForStatus:status], nil);
        }
        resolver = nil;
        rejecter = nil;
    }];
}

RCT_EXPORT_METHOD(trackOnce:(NSDictionary *)optionsDict resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject) {
    RadarTrackingOptionsDesiredAccuracy desiredAccuracy;
    BOOL beacons = NO;
    desiredAccuracy = RadarTrackingOptionsDesiredAccuracyMedium;

    __block RCTPromiseResolveBlock resolver = resolve;
    __block RCTPromiseRejectBlock rejecter = reject;

    NSDictionary *locationDict = optionsDict[@"location"];

    CLLocation *location;
    if (locationDict != nil && [locationDict isKindOfClass:[NSDictionary class]]) {
        double latitude = [RCTConvert double:locationDict[@"latitude"]];
        double longitude = [RCTConvert double:locationDict[@"longitude"]];
        double accuracy = [RCTConvert double:locationDict[@"accuracy"]];
        NSDate *timestamp = [NSDate new];
        location = [[CLLocation alloc] initWithCoordinate:CLLocationCoordinate2DMake(latitude, longitude) altitude:-1 horizontalAccuracy:accuracy verticalAccuracy:-1 timestamp:timestamp];
    }

    RadarTrackCompletionHandler completionHandler = ^(RadarStatus status, CLLocation * _Nullable location, NSArray<RadarEvent *> * _Nullable events, RadarUser * _Nullable user) {
        if (status == RadarStatusSuccess && resolver) {
            NSMutableDictionary *dict = [NSMutableDictionary new];
            [dict setObject:[Radar stringForStatus:status] forKey:@"status"];
            if (location) {
                [dict setObject:[Radar dictionaryForLocation:location] forKey:@"location"];
            }
            if (events) {
                [dict setObject:[RadarEvent arrayForEvents:events] forKey:@"events"];
            }
            if (user) {
                [dict setObject:[user dictionaryValue] forKey:@"user"];
            }
            resolver(dict);
        } else if (rejecter) {
            rejecter([Radar stringForStatus:status], [Radar stringForStatus:status], nil);
        }
        resolver = nil;
        rejecter = nil;
    };

    if (location) {
        [Radar trackOnceWithLocation:location completionHandler:completionHandler];
    } else {
        NSString *accuracy = optionsDict[@"desiredAccuracy"];

        if (accuracy != nil && [accuracy isKindOfClass:[NSString class]]) {
            NSString *lowerAccuracy = [accuracy lowercaseString];
            if ([lowerAccuracy isEqualToString:@"high"]) {
                desiredAccuracy = RadarTrackingOptionsDesiredAccuracyHigh;
            } else if ([lowerAccuracy isEqualToString:@"medium"]) {
                desiredAccuracy = RadarTrackingOptionsDesiredAccuracyMedium;
            } else if ([lowerAccuracy isEqualToString:@"low"]) {
                desiredAccuracy = RadarTrackingOptionsDesiredAccuracyLow;
            }
        }

        NSNumber *beaconsNumber = optionsDict[@"beacons"];
        if (beaconsNumber != nil && [beaconsNumber isKindOfClass:[NSNumber class]]) {
            beacons = [beaconsNumber boolValue];
        }

        [Radar trackOnceWithDesiredAccuracy:desiredAccuracy beacons:beacons completionHandler:completionHandler];
    }
}


RCT_EXPORT_METHOD(trackVerified:(NSDictionary *)optionsDict resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject) {
    if (!NSClassFromString(@"RadarSDKFraud")) {
        reject(@"ERROR_PLUGIN",
               @"trackVerified requires the RadarSDKFraud module. "
               @"Set iosFraud: true in your react-native-radar Expo config plugin "
               @"(or add `pod 'RadarSDKFraud', :path => '../node_modules/react-native-radar'` "
               @"to your ios/Podfile manually), then re-run `npx expo prebuild --clean` "
               @"and `pod install`.",
               nil);
        return;
    }

    BOOL beacons = NO;
    RadarTrackingOptionsDesiredAccuracy desiredAccuracy = RadarTrackingOptionsDesiredAccuracyMedium;
    NSString *reason = nil;
    NSString *transactionId = nil;

    if (optionsDict != nil) {
        NSNumber *beaconsNumber = optionsDict[@"beacons"];
        if (beaconsNumber != nil && [beaconsNumber isKindOfClass:[NSNumber class]]) {
            beacons = [beaconsNumber boolValue];
        }

        NSString *accuracy = optionsDict[@"desiredAccuracy"];
        if (accuracy != nil && [accuracy isKindOfClass:[NSString class]]) {
            NSString *lowerAccuracy = [accuracy lowercaseString];
            if ([lowerAccuracy isEqualToString:@"high"]) {
                desiredAccuracy = RadarTrackingOptionsDesiredAccuracyHigh;
            } else if ([lowerAccuracy isEqualToString:@"medium"]) {
                desiredAccuracy = RadarTrackingOptionsDesiredAccuracyMedium;
            } else if ([lowerAccuracy isEqualToString:@"low"]) {
                desiredAccuracy = RadarTrackingOptionsDesiredAccuracyLow;
            }
        }
        reason = optionsDict[@"reason"];
        transactionId = optionsDict[@"transactionId"];
    }

    __block RCTPromiseResolveBlock resolver = resolve;
    __block RCTPromiseRejectBlock rejecter = reject;

    RadarTrackVerifiedCompletionHandler completionHandler = ^(RadarStatus status, RadarVerifiedLocationToken * _Nullable token) {
        if (status == RadarStatusSuccess && resolver) {
            NSMutableDictionary *dict = [NSMutableDictionary new];
            [dict setObject:[Radar stringForStatus:status] forKey:@"status"];
            if (token != nil) {
                [dict setObject:[token dictionaryValue] forKey:@"token"];
            }
            resolver(dict);
        } else if (rejecter) {
            rejecter([Radar stringForStatus:status], [Radar stringForStatus:status], nil);
        }
        resolver = nil;
        rejecter = nil;
    };

    [Radar trackVerifiedWithBeacons:beacons desiredAccuracy:desiredAccuracy reason:reason transactionId:transactionId completionHandler:completionHandler];
}

RCT_EXPORT_METHOD(isTrackingVerified:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject) {
    BOOL res = [Radar isTrackingVerified];
    resolve(@(res));
}

RCT_EXPORT_METHOD(getVerifiedLocationToken:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject) {
    __block RCTPromiseResolveBlock resolver = resolve;
    __block RCTPromiseRejectBlock rejecter = reject;

    RadarTrackVerifiedCompletionHandler completionHandler = ^(RadarStatus status, RadarVerifiedLocationToken * _Nullable token) {
        if (status == RadarStatusSuccess && resolver) {
            NSMutableDictionary *dict = [NSMutableDictionary new];
            [dict setObject:[Radar stringForStatus:status] forKey:@"status"];
            if (token != nil) {
                [dict setObject:[token dictionaryValue] forKey:@"token"];
            }
            resolver(dict);
        } else if (rejecter) {
            rejecter([Radar stringForStatus:status], [Radar stringForStatus:status], nil);
        }
        resolver = nil;
        rejecter = nil;
    };

    [Radar getVerifiedLocationToken:completionHandler];
}

RCT_EXPORT_METHOD(clearVerifiedLocationToken) {
    [Radar clearVerifiedLocationToken];
}

RCT_EXPORT_METHOD(startTrackingEfficient) {
    [Radar startTrackingWithOptions:RadarTrackingOptions.presetEfficient];
}

RCT_EXPORT_METHOD(startTrackingResponsive) {
    [Radar startTrackingWithOptions:RadarTrackingOptions.presetResponsive];
}

RCT_EXPORT_METHOD(startTrackingContinuous) {
    [Radar startTrackingWithOptions:RadarTrackingOptions.presetContinuous];
}

RCT_EXPORT_METHOD(startTrackingCustom:(NSDictionary *)optionsDict) {
    RadarTrackingOptions *options = [RadarTrackingOptions trackingOptionsFromDictionary:optionsDict];
    [Radar startTrackingWithOptions:options];
}

RCT_EXPORT_METHOD(startTrackingVerified:(NSDictionary *)optionsDict) {
    BOOL token = NO;
    BOOL beacons = NO;
    double interval = 1200;

    if (optionsDict != nil) {
        NSNumber *beaconsNumber = optionsDict[@"beacons"];
        if (beaconsNumber != nil && [beaconsNumber isKindOfClass:[NSNumber class]]) {
            beacons = [beaconsNumber boolValue];
        }
        NSNumber *intervalNumber = optionsDict[@"interval"];
        if (intervalNumber != nil && [intervalNumber isKindOfClass:[NSNumber class]]) {
            interval = [intervalNumber doubleValue];
        }
    }

    [Radar startTrackingVerifiedWithInterval:interval beacons:beacons];
}

RCT_EXPORT_METHOD(mockTracking:(NSDictionary *)optionsDict) {
    if (optionsDict == nil) {
        return;
    }

    NSDictionary *originDict = optionsDict[@"origin"];
    double originLatitude = [RCTConvert double:originDict[@"latitude"]];
    double originLongitude = [RCTConvert double:originDict[@"longitude"]];
    CLLocation *origin = [[CLLocation alloc] initWithCoordinate:CLLocationCoordinate2DMake(originLatitude, originLongitude) altitude:-1 horizontalAccuracy:5 verticalAccuracy:-1 timestamp:[NSDate new]];
    NSDictionary *destinationDict = optionsDict[@"destination"];
    double destinationLatitude = [RCTConvert double:destinationDict[@"latitude"]];
    double destinationLongitude = [RCTConvert double:destinationDict[@"longitude"]];
    CLLocation *destination = [[CLLocation alloc] initWithCoordinate:CLLocationCoordinate2DMake(destinationLatitude, destinationLongitude) altitude:-1 horizontalAccuracy:5 verticalAccuracy:-1 timestamp:[NSDate new]];
    NSString *modeStr = optionsDict[@"mode"];
    RadarRouteMode mode = RadarRouteModeCar;
    if ([modeStr isEqualToString:@"FOOT"] || [modeStr isEqualToString:@"foot"]) {
        mode = RadarRouteModeFoot;
    } else if ([modeStr isEqualToString:@"BIKE"] || [modeStr isEqualToString:@"bike"]) {
        mode = RadarRouteModeBike;
    } else if ([modeStr isEqualToString:@"CAR"] || [modeStr isEqualToString:@"car"]) {
        mode = RadarRouteModeCar;
    }
    NSNumber *stepsNumber = optionsDict[@"steps"];
    __block int steps;
    if (stepsNumber != nil && [stepsNumber isKindOfClass:[NSNumber class]]) {
        steps = [stepsNumber intValue];
    } else {
        steps = 10;
    }
    NSNumber *intervalNumber = optionsDict[@"interval"];
    double interval;
    if (intervalNumber != nil && [intervalNumber isKindOfClass:[NSNumber class]]) {
        interval = [intervalNumber doubleValue];
    } else {
        interval = 1;
    }

    [Radar mockTrackingWithOrigin:origin destination:destination mode:mode steps:steps interval:interval completionHandler:nil];
}

RCT_EXPORT_METHOD(stopTracking) {
    [Radar stopTracking];
}

RCT_EXPORT_METHOD(stopTrackingVerified) {
    [Radar stopTrackingVerified];
}

RCT_EXPORT_METHOD(isTracking:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject) {
    BOOL res = [Radar isTracking];
    resolve(@(res));
}

RCT_EXPORT_METHOD(isUsingRemoteTrackingOptions:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject) {
    BOOL res = [Radar isUsingRemoteTrackingOptions];
    resolve(@(res));
}

RCT_EXPORT_METHOD(getTrackingOptions:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject) {
    if (resolve == nil) {
        return;
    }

    RadarTrackingOptions* options = [Radar getTrackingOptions];
    resolve([options dictionaryValue]);
}

RCT_EXPORT_METHOD(setForegroundServiceOptions:(NSDictionary *)optionsDict) {
    // not implemented
}

RCT_EXPORT_METHOD(setNotificationOptions:(NSDictionary *)optionsDict) {
    // not implemented
}

RCT_EXPORT_METHOD(acceptEvent:(NSString *)eventId verifiedPlaceId:(NSString *)verifiedPlaceId) {
    [Radar acceptEventId:eventId verifiedPlaceId:verifiedPlaceId];
}

RCT_EXPORT_METHOD(rejectEvent:(NSString *)eventId) {
    [Radar rejectEventId:eventId];
}

RCT_EXPORT_METHOD(getTripOptions:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject) {
    if (resolve == nil) {
        return;
    }

    RadarTripOptions* options = [Radar getTripOptions];
    if (options != nil) {
        resolve([options dictionaryValue]);
    } else {
        resolve(nil);
    }
}

RCT_EXPORT_METHOD(startTrip:(NSDictionary *)optionsDict resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject) {
    // { tripOptions, trackingOptions } is the new req format.
    // fallback to reading trip options from the top level options.
    NSDictionary *tripOptionsDict = optionsDict[@"tripOptions"];
    if (tripOptionsDict == nil) {
        tripOptionsDict = optionsDict;
    }

    RadarTripOptions *options = [RadarTripOptions tripOptionsFromDictionary:tripOptionsDict];

    RadarTrackingOptions *trackingOptions;
    NSDictionary *trackingOptionsDict = optionsDict[@"trackingOptions"];
    if (trackingOptionsDict != nil) {
      trackingOptions = [RadarTrackingOptions trackingOptionsFromDictionary:trackingOptionsDict];
    }

    __block RCTPromiseResolveBlock resolver = resolve;
    __block RCTPromiseRejectBlock rejecter = reject;

    [Radar startTripWithOptions:options trackingOptions:trackingOptions completionHandler:^(RadarStatus status, RadarTrip * _Nullable trip, NSArray<RadarEvent *> * _Nullable events) {
        if (status == RadarStatusSuccess && resolver) {
            NSMutableDictionary *dict = [NSMutableDictionary new];
            [dict setObject:[Radar stringForStatus:status] forKey:@"status"];
            if (trip) {
                [dict setObject:[trip dictionaryValue] forKey:@"trip"];
            }
            if (events) {
                [dict setObject:[RadarEvent arrayForEvents:events] forKey:@"events"];
            }
            resolver(dict);
        } else if (rejecter) {
            rejecter([Radar stringForStatus:status], [Radar stringForStatus:status], nil);
        }
        resolver = nil;
        rejecter = nil;
    }];
}

RCT_EXPORT_METHOD(completeTrip:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject) {

    __block RCTPromiseResolveBlock resolver = resolve;
    __block RCTPromiseRejectBlock rejecter = reject;

    [Radar completeTripWithCompletionHandler:^(RadarStatus status, RadarTrip * _Nullable trip, NSArray<RadarEvent *> * _Nullable events) {
        if (status == RadarStatusSuccess && resolver) {
            NSMutableDictionary *dict = [NSMutableDictionary new];
            [dict setObject:[Radar stringForStatus:status] forKey:@"status"];
            if (trip) {
                [dict setObject:[trip dictionaryValue] forKey:@"trip"];
            }
            if (events) {
                [dict setObject:[RadarEvent arrayForEvents:events] forKey:@"events"];
            }
            resolver(dict);
        } else if (rejecter) {
            rejecter([Radar stringForStatus:status], [Radar stringForStatus:status], nil);
        }
        resolver = nil;
        rejecter = nil;
    }];
}

RCT_EXPORT_METHOD(cancelTrip:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject) {
    __block RCTPromiseResolveBlock resolver = resolve;
    __block RCTPromiseRejectBlock rejecter = reject;

    [Radar cancelTripWithCompletionHandler:^(RadarStatus status, RadarTrip * _Nullable trip, NSArray<RadarEvent *> * _Nullable events) {
        if (status == RadarStatusSuccess && resolver) {
            NSMutableDictionary *dict = [NSMutableDictionary new];
            [dict setObject:[Radar stringForStatus:status] forKey:@"status"];
            if (trip) {
                [dict setObject:[trip dictionaryValue] forKey:@"trip"];
            }
            if (events) {
                [dict setObject:[RadarEvent arrayForEvents:events] forKey:@"events"];
            }
            resolver(dict);
        } else if (rejecter) {
            rejecter([Radar stringForStatus:status], [Radar stringForStatus:status], nil);
        }
        resolver = nil;
        rejecter = nil;
    }];
}

RCT_EXPORT_METHOD(updateTrip:(NSDictionary *)optionsDict resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject) {
    if (optionsDict == nil) {
        if (reject) {
            reject([Radar stringForStatus:RadarStatusErrorBadRequest], [Radar stringForStatus:RadarStatusErrorBadRequest], nil);
        }

        return;
    }

    RadarTripOptions *options = [RadarTripOptions tripOptionsFromDictionary:optionsDict[@"options"]];
    NSString *statusStr = optionsDict[@"status"];

    RadarTripStatus status = RadarTripStatusUnknown;
    if (statusStr) {
        if ([statusStr isEqualToString:@"started"]) {
            status = RadarTripStatusStarted;
        } else if ([statusStr isEqualToString:@"approaching"]) {
            status = RadarTripStatusApproaching;
        } else if ([statusStr isEqualToString:@"arrived"]) {
            status = RadarTripStatusArrived;
        } else if ([statusStr isEqualToString:@"completed"]) {
            status = RadarTripStatusCompleted;
        } else if ([statusStr isEqualToString:@"canceled"]) {
            status = RadarTripStatusCanceled;
        } else if ([statusStr isEqualToString:@"unknown"]) {
            status = RadarTripStatusUnknown;
        } else {
            if (reject) {
                reject([Radar stringForStatus:RadarStatusErrorBadRequest], [Radar stringForStatus:RadarStatusErrorBadRequest], nil);
            }

            return;
        }
    } else {
        if (reject) {
            reject([Radar stringForStatus:RadarStatusErrorBadRequest], [Radar stringForStatus:RadarStatusErrorBadRequest], nil);
        }

        return;
    }

    __block RCTPromiseResolveBlock resolver = resolve;
    __block RCTPromiseRejectBlock rejecter = reject;

    [Radar updateTripWithOptions:options status:status completionHandler:^(RadarStatus status, RadarTrip * _Nullable trip, NSArray<RadarEvent *> * _Nullable events) {
        if (status == RadarStatusSuccess && resolver) {
            NSMutableDictionary *dict = [NSMutableDictionary new];
            [dict setObject:[Radar stringForStatus:status] forKey:@"status"];
            if (trip) {
                [dict setObject:[trip dictionaryValue] forKey:@"trip"];
            }
            if (events) {
                [dict setObject:[RadarEvent arrayForEvents:events] forKey:@"events"];
            }
            resolver(dict);
        } else if (rejecter) {
            rejecter([Radar stringForStatus:status], [Radar stringForStatus:status], nil);
        }
        resolver = nil;
        rejecter = nil;
    }];
}

RCT_EXPORT_METHOD(updateTripLeg:(NSDictionary *)optionsDict resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject) {
    if (optionsDict == nil) {
        if (reject) {
            reject([Radar stringForStatus:RadarStatusErrorBadRequest], [Radar stringForStatus:RadarStatusErrorBadRequest], nil);
        }
        return;
    }

    NSString *legId = optionsDict[@"legId"];
    NSString *statusStr = optionsDict[@"status"];

    if (legId == nil || statusStr == nil) {
        if (reject) {
            reject([Radar stringForStatus:RadarStatusErrorBadRequest], [Radar stringForStatus:RadarStatusErrorBadRequest], nil);
        }
        return;
    }

    RadarTripLegStatus legStatus = [RadarTripLeg statusForString:statusStr];

    __block RCTPromiseResolveBlock resolver = resolve;
    __block RCTPromiseRejectBlock rejecter = reject;

    RadarTripLegCompletionHandler completionHandler = ^(RadarStatus status, RadarTrip * _Nullable trip, RadarTripLeg * _Nullable leg, NSArray<RadarEvent *> * _Nullable events) {
        if (status == RadarStatusSuccess && resolver) {
            NSMutableDictionary *dict = [NSMutableDictionary new];
            [dict setObject:[Radar stringForStatus:status] forKey:@"status"];
            if (trip) {
                [dict setObject:[trip dictionaryValue] forKey:@"trip"];
            }
            if (leg) {
                [dict setObject:[leg dictionaryValue] forKey:@"leg"];
            }
            if (events) {
                [dict setObject:[RadarEvent arrayForEvents:events] forKey:@"events"];
            }
            resolver(dict);
        } else if (rejecter) {
            rejecter([Radar stringForStatus:status], [Radar stringForStatus:status], nil);
        }
        resolver = nil;
        rejecter = nil;
    };

    id tripIdValue = optionsDict[@"tripId"];
    NSString *tripId = ([tripIdValue isKindOfClass:[NSString class]]) ? tripIdValue : nil;
    if (tripId) {
        [Radar updateTripLegWithTripId:tripId legId:legId status:legStatus completionHandler:completionHandler];
    } else {
        [Radar updateTripLegWithLegId:legId status:legStatus completionHandler:completionHandler];
    }
}

RCT_EXPORT_METHOD(reorderTripLegs:(NSDictionary *)optionsDict resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject) {
    if (optionsDict == nil) {
        if (reject) {
            reject([Radar stringForStatus:RadarStatusErrorBadRequest], [Radar stringForStatus:RadarStatusErrorBadRequest], nil);
        }
        return;
    }

    NSArray<NSString *> *legIds = optionsDict[@"legIds"];
    if (legIds == nil) {
        if (reject) {
            reject([Radar stringForStatus:RadarStatusErrorBadRequest], [Radar stringForStatus:RadarStatusErrorBadRequest], nil);
        }
        return;
    }

    __block RCTPromiseResolveBlock resolver = resolve;
    __block RCTPromiseRejectBlock rejecter = reject;

    RadarTripCompletionHandler completionHandler = ^(RadarStatus status, RadarTrip * _Nullable trip, NSArray<RadarEvent *> * _Nullable events) {
        if (status == RadarStatusSuccess && resolver) {
            NSMutableDictionary *dict = [NSMutableDictionary new];
            [dict setObject:[Radar stringForStatus:status] forKey:@"status"];
            if (trip) {
                [dict setObject:[trip dictionaryValue] forKey:@"trip"];
            }
            if (events) {
                [dict setObject:[RadarEvent arrayForEvents:events] forKey:@"events"];
            }
            resolver(dict);
        } else if (rejecter) {
            rejecter([Radar stringForStatus:status], [Radar stringForStatus:status], nil);
        }
        resolver = nil;
        rejecter = nil;
    };

    id tripIdValue = optionsDict[@"tripId"];
    NSString *tripId = ([tripIdValue isKindOfClass:[NSString class]]) ? tripIdValue : nil;
    if (tripId) {
        [Radar reorderTripLegsWithTripId:tripId legIds:legIds completionHandler:completionHandler];
    } else {
        [Radar reorderTripLegsWithLegIds:legIds completionHandler:completionHandler];
    }
}

RCT_EXPORT_METHOD(getContext:(NSDictionary *)locationDict resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject) {
    CLLocation *location;
    if (locationDict != nil && [locationDict isKindOfClass:[NSDictionary class]]) {
        double latitude = [RCTConvert double:locationDict[@"latitude"]];
        double longitude = [RCTConvert double:locationDict[@"longitude"]];
        NSDate *timestamp = [NSDate new];
        location = [[CLLocation alloc] initWithCoordinate:CLLocationCoordinate2DMake(latitude, longitude) altitude:-1 horizontalAccuracy:5 verticalAccuracy:-1 timestamp:timestamp];
    }

    __block RCTPromiseResolveBlock resolver = resolve;
    __block RCTPromiseRejectBlock rejecter = reject;

    RadarContextCompletionHandler completionHandler = ^(RadarStatus status, CLLocation * _Nullable location, RadarContext * _Nullable context) {
        if (status == RadarStatusSuccess && resolver) {
            NSMutableDictionary *dict = [NSMutableDictionary new];
            [dict setObject:[Radar stringForStatus:status] forKey:@"status"];
            if (location) {
                [dict setObject:[Radar dictionaryForLocation:location] forKey:@"location"];
            }
            if (context) {
                [dict setObject:[context dictionaryValue] forKey:@"context"];
            }
            resolver(dict);
        } else if (rejecter) {
            rejecter([Radar stringForStatus:status], [Radar stringForStatus:status], nil);
        }
        resolver = nil;
        rejecter = nil;
    };

    if (location) {
        [Radar getContextForLocation:location completionHandler:completionHandler];
    } else {
        [Radar getContextWithCompletionHandler:completionHandler];
    }
}

RCT_EXPORT_METHOD(searchPlaces:(NSDictionary *)optionsDict resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject) {
    if (optionsDict == nil) {
        if (reject) {
            reject([Radar stringForStatus:RadarStatusErrorBadRequest], [Radar stringForStatus:RadarStatusErrorBadRequest], nil);
        }

        return;
    }

    NSDictionary *nearDict = optionsDict[@"near"];
    CLLocation *near;
    if (nearDict != nil && [nearDict isKindOfClass:[NSDictionary class]]) {
        double latitude = [RCTConvert double:nearDict[@"latitude"]];
        double longitude = [RCTConvert double:nearDict[@"longitude"]];
        NSDate *timestamp = [NSDate new];
        near = [[CLLocation alloc] initWithCoordinate:CLLocationCoordinate2DMake(latitude, longitude) altitude:-1 horizontalAccuracy:5 verticalAccuracy:-1 timestamp:timestamp];
    }
    NSNumber *radiusNumber = optionsDict[@"radius"];
    int radius;
    if (radiusNumber != nil && [radiusNumber isKindOfClass:[NSNumber class]]) {
        radius = [radiusNumber intValue];
    } else {
        radius = 1000;
    }
    NSArray *chains = optionsDict[@"chains"];
    NSDictionary *chainMetadata = optionsDict[@"chainMetadata"];
    NSArray *categories = optionsDict[@"categories"];
    NSArray *groups = optionsDict[@"groups"];
    NSArray *countryCodes = optionsDict[@"countryCodes"];
    NSNumber *limitNumber = optionsDict[@"limit"];
    int limit;
    if (limitNumber != nil && [limitNumber isKindOfClass:[NSNumber class]]) {
        limit = [limitNumber intValue];
    } else {
        limit = 10;
    }

    __block RCTPromiseResolveBlock resolver = resolve;
    __block RCTPromiseRejectBlock rejecter = reject;

    RadarSearchPlacesCompletionHandler completionHandler = ^(RadarStatus status, CLLocation * _Nullable location, NSArray<RadarPlace *> * _Nullable places) {
        if (status == RadarStatusSuccess && resolver) {
            NSMutableDictionary *dict = [NSMutableDictionary new];
            [dict setObject:[Radar stringForStatus:status] forKey:@"status"];
            if (location) {
                [dict setObject:[Radar dictionaryForLocation:location] forKey:@"location"];
            }
            if (places) {
                [dict setObject:[RadarPlace arrayForPlaces:places] forKey:@"places"];
            }
            resolver(dict);
        } else if (rejecter) {
            rejecter([Radar stringForStatus:status], [Radar stringForStatus:status], nil);
        }
        resolver = nil;
        rejecter = nil;
    };

    if (near) {
        [Radar searchPlacesNear:near radius:radius chains:chains chainMetadata:chainMetadata categories:categories groups:groups countryCodes:countryCodes limit:limit completionHandler:completionHandler];
    } else {
        [Radar searchPlacesWithRadius:radius chains:chains chainMetadata:chainMetadata categories:categories groups:groups countryCodes:countryCodes limit:limit completionHandler:completionHandler];
    }
}

RCT_EXPORT_METHOD(searchGeofences:(NSDictionary *)optionsDict resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject) {
    if (optionsDict == nil) {
        if (reject) {
            reject([Radar stringForStatus:RadarStatusErrorBadRequest], [Radar stringForStatus:RadarStatusErrorBadRequest], nil);
        }

        return;
    }

    NSDictionary *nearDict = optionsDict[@"near"];
    CLLocation *near;
    if (nearDict != nil && [nearDict isKindOfClass:[NSDictionary class]]) {
        double latitude = [RCTConvert double:nearDict[@"latitude"]];
        double longitude = [RCTConvert double:nearDict[@"longitude"]];
        NSDate *timestamp = [NSDate new];
        near = [[CLLocation alloc] initWithCoordinate:CLLocationCoordinate2DMake(latitude, longitude) altitude:-1 horizontalAccuracy:5 verticalAccuracy:-1 timestamp:timestamp];
    }
    NSNumber *radiusNumber = optionsDict[@"radius"];
    int radius;
    if (radiusNumber != nil && [radiusNumber isKindOfClass:[NSNumber class]]) {
        radius = [radiusNumber intValue];
    } else {
        radius = -1;
    }
    NSArray *tags = optionsDict[@"tags"];
    NSDictionary *metadata = optionsDict[@"metadata"];
    NSNumber *limitNumber = optionsDict[@"limit"];
    int limit;
    if (limitNumber != nil && [limitNumber isKindOfClass:[NSNumber class]]) {
        limit = [limitNumber intValue];
    } else {
        limit = 100;
    }

    BOOL includeGeometry = NO;

    NSNumber *includeGeometryNumber = optionsDict[@"includeGeometry"];
    if (includeGeometryNumber != nil && [includeGeometryNumber isKindOfClass:[NSNumber class]]) {
        includeGeometry = [includeGeometryNumber boolValue];
    }


    __block RCTPromiseResolveBlock resolver = resolve;
    __block RCTPromiseRejectBlock rejecter = reject;

    RadarSearchGeofencesCompletionHandler completionHandler = ^(RadarStatus status, CLLocation * _Nullable location, NSArray<RadarGeofence *> * _Nullable geofences) {
        if (status == RadarStatusSuccess && resolver) {
            NSMutableDictionary *dict = [NSMutableDictionary new];
            [dict setObject:[Radar stringForStatus:status] forKey:@"status"];
            if (location) {
                [dict setObject:[Radar dictionaryForLocation:location] forKey:@"location"];
            }
            if (geofences) {
                [dict setObject:[RadarGeofence arrayForGeofences:geofences] forKey:@"geofences"];
            }
            resolver(dict);
        } else if (rejecter) {
            rejecter([Radar stringForStatus:status], [Radar stringForStatus:status], nil);
        }
        resolver = nil;
        rejecter = nil;
    };


    [Radar searchGeofencesNear:near radius:radius tags:tags metadata:metadata limit:limit includeGeometry:includeGeometry completionHandler:completionHandler];

}

RCT_EXPORT_METHOD(autocomplete:(NSDictionary *)optionsDict resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject) {
    if (optionsDict == nil) {
        if (reject) {
            reject([Radar stringForStatus:RadarStatusErrorBadRequest], [Radar stringForStatus:RadarStatusErrorBadRequest], nil);
        }

        return;
    }

    NSDictionary *nearDict = optionsDict[@"near"];
    CLLocation *near = nil;
    if (nearDict && [nearDict isKindOfClass:[NSDictionary class]]) {
        id latitudeObj = nearDict[@"latitude"];
        id longitudeObj = nearDict[@"longitude"];

        if (latitudeObj && longitudeObj &&
            [latitudeObj isKindOfClass:[NSNumber class]] &&
            [longitudeObj isKindOfClass:[NSNumber class]]) {

            double latitude = [RCTConvert double:latitudeObj];
            double longitude = [RCTConvert double:longitudeObj];
            NSDate *timestamp = [NSDate new];
            near = [[CLLocation alloc] initWithCoordinate:CLLocationCoordinate2DMake(latitude, longitude) altitude:-1 horizontalAccuracy:5 verticalAccuracy:-1 timestamp:timestamp];
        }
    }


    NSString *query = optionsDict[@"query"];
    NSNumber *limitNumber = optionsDict[@"limit"];
    int limit;
    if (limitNumber != nil && [limitNumber isKindOfClass:[NSNumber class]]) {
        limit = [limitNumber intValue];
    } else {
        limit = 10;
    }

    NSArray *layers = optionsDict[@"layers"];
    NSString *country = optionsDict[@"countryCode"];
    if (country == nil) {
        country = optionsDict[@"country"];
    }

    BOOL mailable = false;
    NSNumber *mailableNumber = optionsDict[@"mailable"];
    if (mailableNumber != nil && [mailableNumber isKindOfClass:[NSNumber class]]) {
        mailable = [mailableNumber boolValue];
    }

    __block RCTPromiseResolveBlock resolver = resolve;
    __block RCTPromiseRejectBlock rejecter = reject;

    [Radar autocompleteQuery:query near:near layers:layers limit:limit country:country mailable:mailable completionHandler:^(RadarStatus status, NSArray<RadarAddress *> * _Nullable addresses) {
        if (status == RadarStatusSuccess && resolver) {
            NSMutableDictionary *dict = [NSMutableDictionary new];
            [dict setObject:[Radar stringForStatus:status] forKey:@"status"];
            if (addresses) {
                [dict setObject:[RadarAddress arrayForAddresses:addresses] forKey:@"addresses"];
            }
            resolver(dict);
        } else if (rejecter) {
            rejecter([Radar stringForStatus:status], [Radar stringForStatus:status], nil);
        }
        resolver = nil;
        rejecter = nil;
    }];
}

RCT_EXPORT_METHOD(geocode:(NSDictionary *)optionsDict resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject) {
    if (optionsDict[@"address"] == nil) {
        if (reject) {
            reject([Radar stringForStatus:RadarStatusErrorBadRequest], [Radar stringForStatus:RadarStatusErrorBadRequest], nil);
        }

        return;
    }

    NSString *query = optionsDict[@"address"];
    NSArray *layers = optionsDict[@"layers"];
    NSArray *countries = optionsDict[@"countries"];

    __block RCTPromiseResolveBlock resolver = resolve;
    __block RCTPromiseRejectBlock rejecter = reject;

    [Radar geocodeAddress:query layers:layers countries:countries completionHandler:^(RadarStatus status, NSArray<RadarAddress *> * _Nullable addresses) {
        if (status == RadarStatusSuccess && resolver) {
            NSMutableDictionary *dict = [NSMutableDictionary new];
            [dict setObject:[Radar stringForStatus:status] forKey:@"status"];
            if (addresses) {
                [dict setObject:[RadarAddress arrayForAddresses:addresses] forKey:@"addresses"];
            }
            resolver(dict);
        } else if (rejecter) {
            rejecter([Radar stringForStatus:status], [Radar stringForStatus:status], nil);
        }
        resolver = nil;
        rejecter = nil;
    }];
}

RCT_EXPORT_METHOD(reverseGeocode:(NSDictionary *)optionsDict resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject) {
    NSDictionary *locationDict = nil;
    NSArray *layers = nil;

    if (optionsDict != nil) {
        locationDict = optionsDict[@"location"];
        layers = optionsDict[@"layers"];
    }

    __block RCTPromiseResolveBlock resolver = resolve;
    __block RCTPromiseRejectBlock rejecter = reject;

    RadarGeocodeCompletionHandler completionHandler = ^(RadarStatus status, NSArray<RadarAddress *> * _Nullable addresses) {
        if (status == RadarStatusSuccess && resolver) {
            NSMutableDictionary *dict = [NSMutableDictionary new];
            [dict setObject:[Radar stringForStatus:status] forKey:@"status"];
            if (addresses) {
                [dict setObject:[RadarAddress arrayForAddresses:addresses] forKey:@"addresses"];
            }
            resolver(dict);
        } else if (rejecter) {
            rejecter([Radar stringForStatus:status], [Radar stringForStatus:status], nil);
        }
        resolver = nil;
        rejecter = nil;
    };

    if (locationDict != nil && [locationDict isKindOfClass:[NSDictionary class]]) {
        double latitude = [RCTConvert double:locationDict[@"latitude"]];
        double longitude = [RCTConvert double:locationDict[@"longitude"]];
        NSDate *timestamp = [NSDate new];
        CLLocation *location = [[CLLocation alloc] initWithCoordinate:CLLocationCoordinate2DMake(latitude, longitude)
                                                             altitude:-1
                                                   horizontalAccuracy:5
                                                     verticalAccuracy:-1
                                                            timestamp:timestamp];
        [Radar reverseGeocodeLocation:location layers:layers completionHandler:completionHandler];
    } else {
        [Radar reverseGeocodeWithLayers:layers completionHandler:completionHandler];
    }
}

RCT_EXPORT_METHOD(ipGeocode:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject) {
    __block RCTPromiseResolveBlock resolver = resolve;
    __block RCTPromiseRejectBlock rejecter = reject;

    [Radar ipGeocodeWithCompletionHandler:^(RadarStatus status, RadarAddress * _Nullable address, BOOL proxy) {
        if (status == RadarStatusSuccess && resolver) {
            NSMutableDictionary *dict = [NSMutableDictionary new];
            [dict setObject:[Radar stringForStatus:status] forKey:@"status"];
            if (address) {
                [dict setObject:[address dictionaryValue] forKey:@"address"];
                [dict setValue:@(proxy) forKey:@"proxy"];
            }
            resolver(dict);
        } else if (rejecter) {
            rejecter([Radar stringForStatus:status], [Radar stringForStatus:status], nil);
        }
        resolver = nil;
        rejecter = nil;
    }];
}

RCT_EXPORT_METHOD(validateAddress:(NSDictionary *)addressDict resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject) {
    __block RCTPromiseResolveBlock resolver = resolve;
    __block RCTPromiseRejectBlock rejecter = reject;

    RadarAddress *address = [RadarAddress addressFromObject:addressDict];

    if (address == nil) {
        reject([Radar stringForStatus:RadarStatusErrorBadRequest], [Radar stringForStatus:RadarStatusErrorBadRequest], nil);
    }

    [Radar validateAddress:address completionHandler:^(RadarStatus status, RadarAddress * _Nullable address, RadarAddressVerificationStatus verificationStatus) {
        if (status == RadarStatusSuccess && resolver) {
            NSMutableDictionary *dict = [NSMutableDictionary new];
            [dict setObject:[Radar stringForStatus:status] forKey:@"status"];
            if (address) {
                [dict setObject:[address dictionaryValue] forKey:@"address"];
            }
            dict[@"verificationStatus"] = [Radar stringForVerificationStatus: verificationStatus];
            resolver(dict);
        } else if (rejecter) {
            rejecter([Radar stringForStatus:status], [Radar stringForStatus:status], nil);
        }
        resolver = nil;
        rejecter = nil;
    }];
}

RCT_EXPORT_METHOD(getDistance:(NSDictionary *)optionsDict resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject) {
    if (optionsDict == nil) {
        if (reject) {
            reject([Radar stringForStatus:RadarStatusErrorBadRequest], [Radar stringForStatus:RadarStatusErrorBadRequest], nil);
        }

        return;
    }

    NSDictionary *destinationDict = optionsDict[@"destination"];
    if (destinationDict == nil || ![destinationDict isKindOfClass:[NSDictionary class]]) {
        if (reject) {
            reject([Radar stringForStatus:RadarStatusErrorBadRequest], [Radar stringForStatus:RadarStatusErrorBadRequest], nil);
        }

        return;
    }

    NSDictionary *originDict = optionsDict[@"origin"];
    CLLocation *origin;
    if (originDict != nil && [originDict isKindOfClass:[NSDictionary class]]) {
        double latitude = [RCTConvert double:originDict[@"latitude"]];
        double longitude = [RCTConvert double:originDict[@"longitude"]];
        NSDate *timestamp = [NSDate new];
        origin = [[CLLocation alloc] initWithCoordinate:CLLocationCoordinate2DMake(latitude, longitude) altitude:-1 horizontalAccuracy:5 verticalAccuracy:-1 timestamp:timestamp];
    }
    double latitude = [RCTConvert double:destinationDict[@"latitude"]];
    double longitude = [RCTConvert double:destinationDict[@"longitude"]];
    NSDate *timestamp = [NSDate new];
    CLLocation *destination = [[CLLocation alloc] initWithCoordinate:CLLocationCoordinate2DMake(latitude, longitude) altitude:-1 horizontalAccuracy:5 verticalAccuracy:-1 timestamp:timestamp];
    NSArray *modesArr = optionsDict[@"modes"];
    RadarRouteMode modes = 0;
    if (modesArr != nil && [modesArr isKindOfClass:[NSArray class]]) {
        if ([modesArr containsObject:@"FOOT"] || [modesArr containsObject:@"foot"]) {
            modes = modes | RadarRouteModeFoot;
        }
        if ([modesArr containsObject:@"BIKE"] || [modesArr containsObject:@"bike"]) {
            modes = modes | RadarRouteModeBike;
        }
        if ([modesArr containsObject:@"CAR"] || [modesArr containsObject:@"car"]) {
            modes = modes | RadarRouteModeCar;
        }
    } else {
        modes = RadarRouteModeCar;
    }
    NSString *unitsStr = optionsDict[@"units"];
    RadarRouteUnits units;
    if (unitsStr != nil && [unitsStr isKindOfClass:[NSString class]]) {
        units = [unitsStr isEqualToString:@"METRIC"] || [unitsStr isEqualToString:@"metric"] ? RadarRouteUnitsMetric : RadarRouteUnitsImperial;
    } else {
        units = RadarRouteUnitsImperial;
    }

    __block RCTPromiseResolveBlock resolver = resolve;
    __block RCTPromiseRejectBlock rejecter = reject;

    RadarRouteCompletionHandler completionHandler = ^(RadarStatus status, RadarRoutes * _Nullable routes) {
        if (status == RadarStatusSuccess && resolver) {
            NSMutableDictionary *dict = [NSMutableDictionary new];
            [dict setObject:[Radar stringForStatus:status] forKey:@"status"];
            if (routes) {
                [dict setObject:[routes dictionaryValue] forKey:@"routes"];
            }
            resolver(dict);
        } else if (rejecter) {
            rejecter([Radar stringForStatus:status], [Radar stringForStatus:status], nil);
        }
        resolver = nil;
        rejecter = nil;
    };

    if (origin) {
        [Radar getDistanceFromOrigin:origin destination:destination modes:modes units:units completionHandler:completionHandler];
    } else {
        [Radar getDistanceToDestination:destination modes:modes units:units completionHandler:completionHandler];
    }
}

RCT_EXPORT_METHOD(getMatrix:(NSDictionary *)optionsDict resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject) {
    if (optionsDict == nil) {
        if (reject) {
            reject([Radar stringForStatus:RadarStatusErrorBadRequest], [Radar stringForStatus:RadarStatusErrorBadRequest], nil);
        }

        return;
    }

    NSArray<NSDictionary *> *originsArr = optionsDict[@"origins"];
    NSMutableArray<CLLocation *> *origins = [NSMutableArray new];
    for (NSDictionary *originDict in originsArr) {
        double latitude = [RCTConvert double:originDict[@"latitude"]];
        double longitude = [RCTConvert double:originDict[@"longitude"]];
        NSDate *timestamp = [NSDate new];
        CLLocation *origin = [[CLLocation alloc] initWithCoordinate:CLLocationCoordinate2DMake(latitude, longitude) altitude:-1 horizontalAccuracy:5 verticalAccuracy:-1 timestamp:timestamp];
        [origins addObject:origin];
    }
    NSArray<NSDictionary *> *destinationsArr = optionsDict[@"destinations"];
    NSMutableArray<CLLocation *> *destinations = [NSMutableArray new];
    for (NSDictionary *destinationDict in destinationsArr) {
        double latitude = [RCTConvert double:destinationDict[@"latitude"]];
        double longitude = [RCTConvert double:destinationDict[@"longitude"]];
        NSDate *timestamp = [NSDate new];
        CLLocation *destination = [[CLLocation alloc] initWithCoordinate:CLLocationCoordinate2DMake(latitude, longitude) altitude:-1 horizontalAccuracy:5 verticalAccuracy:-1 timestamp:timestamp];
        [destinations addObject:destination];
    }
    NSString *modeStr = optionsDict[@"mode"];
    RadarRouteMode mode = RadarRouteModeCar;
    if ([modeStr isEqualToString:@"FOOT"] || [modeStr isEqualToString:@"foot"]) {
        mode = RadarRouteModeFoot;
    } else if ([modeStr isEqualToString:@"BIKE"] || [modeStr isEqualToString:@"bike"]) {
        mode = RadarRouteModeBike;
    } else if ([modeStr isEqualToString:@"CAR"] || [modeStr isEqualToString:@"car"]) {
        mode = RadarRouteModeCar;
    } else if ([modeStr isEqualToString:@"TRUCK"] || [modeStr isEqualToString:@"truck"]) {
        mode = RadarRouteModeTruck;
    } else if ([modeStr isEqualToString:@"MOTORBIKE"] || [modeStr isEqualToString:@"motorbike"]) {
        mode = RadarRouteModeMotorbike;
    }
    NSString *unitsStr = optionsDict[@"units"];
    RadarRouteUnits units;
    if (unitsStr != nil && [unitsStr isKindOfClass:[NSString class]]) {
        units = [unitsStr isEqualToString:@"METRIC"] || [unitsStr isEqualToString:@"metric"] ? RadarRouteUnitsMetric : RadarRouteUnitsImperial;
    } else {
        units = RadarRouteUnitsImperial;
    }

    __block RCTPromiseResolveBlock resolver = resolve;
    __block RCTPromiseRejectBlock rejecter = reject;

    [Radar getMatrixFromOrigins:origins destinations:destinations mode:mode units:units completionHandler:^(RadarStatus status, RadarRouteMatrix * _Nullable matrix) {
        if (status == RadarStatusSuccess && resolver) {
            NSMutableDictionary *dict = [NSMutableDictionary new];
            [dict setObject:[Radar stringForStatus:status] forKey:@"status"];
            if (matrix) {
                [dict setObject:[matrix arrayValue] forKey:@"matrix"];
            }
            resolver(dict);
        } else if (rejecter) {
            rejecter([Radar stringForStatus:status], [Radar stringForStatus:status], nil);
        }
        resolver = nil;
        rejecter = nil;
    }];
}

RCT_EXPORT_METHOD(showInAppMessage:(NSDictionary *)inAppMessageDict) {
    RadarInAppMessage *inAppMessage = [RadarInAppMessage fromDictionary:inAppMessageDict];
    if (inAppMessage != nil) {
        [Radar showInAppMessage:inAppMessage];
    }
}

RCT_EXPORT_METHOD(logConversion:(NSDictionary *)optionsDict resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject) {
    if (optionsDict == nil) {
        if (reject) {
            reject([Radar stringForStatus:RadarStatusErrorBadRequest], [Radar stringForStatus:RadarStatusErrorBadRequest], nil);
        }

        return;
    }

    NSString *name = optionsDict[@"name"];
    NSNumber *revenue = optionsDict[@"revenue"];
    NSDictionary *metadata = optionsDict[@"metadata"];
    if (name == nil) {
        if (reject) {
            reject([Radar stringForStatus:RadarStatusErrorBadRequest], [Radar stringForStatus:RadarStatusErrorBadRequest], nil);
        }

        return;
    }

    __block RCTPromiseResolveBlock resolver = resolve;
    __block RCTPromiseRejectBlock rejecter = reject;

    RadarLogConversionCompletionHandler completionHandler = ^(RadarStatus status, RadarEvent * _Nullable event) {
        if (status == RadarStatusSuccess && resolver) {
            NSMutableDictionary *dict = [NSMutableDictionary new];
            [dict setObject:[Radar stringForStatus:status] forKey:@"status"];
            if (event) {
                [dict setObject:[event dictionaryValue] forKey:@"event"];
            }
            resolver(dict);
        } else if (rejecter) {
            rejecter([Radar stringForStatus:status], [Radar stringForStatus:status], nil);
        }
        resolver = nil;
        rejecter = nil;
    };

    if (revenue) {
        [Radar logConversionWithName:name revenue:revenue metadata:metadata completionHandler:completionHandler];
    } else {
        [Radar logConversionWithName:name metadata:metadata completionHandler:completionHandler];
    }
}

RCT_EXPORT_METHOD(nativeSdkVersion:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject){
    resolve([Radar sdkVersion]);
}

RCT_EXPORT_METHOD(setPushNotificationToken:(NSString *)token) {
    // No-op on iOS - push notifications are configured via AppDelegate
}

RCT_EXPORT_METHOD(setAppGroup:(NSString *)groupId) {
    [Radar setAppGroup:groupId];
}

RCT_EXPORT_METHOD(setLocationExtensionToken:(NSString *)token) {
    [Radar setLocationExtensionToken:token];
}

RCT_EXPORT_METHOD(isInitialized:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject) {
    resolve(@(Radar.isInitialized));
}

#ifdef RCT_NEW_ARCH_ENABLED
- (std::shared_ptr<facebook::react::TurboModule>)getTurboModule:
    (const facebook::react::ObjCTurboModule::InitParams &)params
{
    return std::make_shared<facebook::react::NativeRadarSpecJSI>(params);
}
#endif

@end
