//
//  DynatraceRNBridge.mm
//

#import "DynatraceRNBridge.h"

// For Turbo Module
#ifdef RCT_NEW_ARCH_ENABLED
#import "DynatraceBridgeSpec.h"
#endif

#import <Foundation/Foundation.h>
#import <CoreLocation/CoreLocation.h>
#import <React/RCTPerformanceLogger.h>
#import <React/RCTRootView.h>
#import <React/RCTBridge+Private.h>
#import <cxxreact/ReactMarker.h>
#import <cxxreact/ReactNativeVersion.h>

#include <chrono>
#include <type_traits>

@interface DynatraceRNBridge ()
- (void)handleRuntimeConfiguration:(NSDictionary<NSString*, id>*)configuration;
- (void)setupRuntimeConfigurationListenerIfNeeded;
- (void)emitToJS:(NSString*)event body:(id)body;
@end

@interface RNConfigurationSubscriber : NSObject <ConfigurationSubscriber>
@property (nonatomic, weak) DynatraceRNBridge* bridge;
- (instancetype)initWithBridge:(DynatraceRNBridge*)bridge;
@end

@implementation RNConfigurationSubscriber

- (instancetype)initWithBridge:(DynatraceRNBridge*)bridge
{
    self = [super init];
    if (self) {
        _bridge = bridge;
    }
    return self;
}

- (void)notifyWithConfiguration:(NSDictionary<NSString*, id>*)configuration
{
    DynatraceRNBridge* bridge = self.bridge;
    if (bridge == nil) {
        return;
    }
    [bridge handleRuntimeConfiguration:configuration];
}

@end

@implementation DynatraceRNBridge

NSMutableDictionary *actionDict;
NSMutableDictionary *webTimingsDict;

// Platform types
NSString *const PlatformAndroid = @"android";
NSString *const PlatformiOS = @"ios";

// Data collection levels
NSString *const DataCollectionOff = @"OFF";
NSString *const DataCollectionPerformance = @"PERFORMANCE";
NSString *const DataCollectionUserBehavior = @"USER_BEHAVIOR";

/**
* Emitting app start event with this identifier
*/
NSString *const EmitAppMeasurementsIdentifier = @"dynatraceAppStartMeasurements";
NSString *const EmitDynatraceConfiguration = @"dynatraceConfiguration";

// All types which are interesting for us
NSString *const DownloadStart = @"downloadStart";
NSString *const DownloadEnd = @"downloadEnd";
NSString *const RunJSBundleStart = @"runJsBundleStart";
NSString *const RunJSBundleEnd = @"runJsBundleEnd";
NSString *const Reload = @"reload";
NSString *const ContentAppeared = @"contentAppeared";

/**
* Telling us if we can actually emit events
*/
bool hasListeners;

/**
* When events have been emitted this value will be true, so we don't emit again
*/
bool didEmit;

/**
* When content finally appeared
*/
int64_t contentAppeared;

/**
* When the JavaScript bundle started running
*/
int64_t runJSBundleStartTime;

/**
* When the JavaScript bundle stopped running
*/
int64_t runJSBundleEndTime;

// Runtime configuration (remote config)
static bool remoteConfigSubscribed;
static NSDictionary<NSString*, id>* lastRuntimeConfiguration;
static RNConfigurationSubscriber* configurationSubscriber;

RCT_EXPORT_MODULE(DynatraceBridge);

- (instancetype) init
{
  if (self = [super init]) {
    actionDict = [[NSMutableDictionary alloc] init];
    webTimingsDict = [[NSMutableDictionary alloc] init];
    hasListeners = NO;
    didEmit = NO;
    contentAppeared = -1;
    runJSBundleStartTime = -1;
    runJSBundleEndTime = -1;

    remoteConfigSubscribed = NO;
    lastRuntimeConfiguration = nil;
    configurationSubscriber = [[RNConfigurationSubscriber alloc] initWithBridge:self];

    registerLogTaggedMarkerCallbacks();
  }
  return self;
}

// We have to make sure facebook::react::ReactMarker::StartupLogger::getInstance().getRunJSBundleEndTime() is always available even for the RN < 72s cases.
// Relying on the if constexpr else does not work since namespaces get resolved before the if branch in the if constexpr gets discarded.
// SFINAE does not work for top level symbols in namespaces (because it actually is an error).
// We cannot use preprocessor directives since we don't have the React Native version as preprocessor macro.
// We cannot use C++ concepts since C++20 is not available.
// Thus we provide a fallback definition for StartupLogger.
// Calling the class methods without a definition cannot happen and won't result in a linker error since the if branch in the if constexpr does not get compiled.
namespace fallback {
    class StartupLogger {
        public:
        static StartupLogger& getInstance();
        double getRunJSBundleStartTime();
        double getRunJSBundleEndTime();
    };
}

namespace facebook::react::ReactMarker {
    using namespace fallback;
}

int64_t getRunJSBundleStartTime() {
    if constexpr(facebook::react::ReactNativeVersion.Minor >= 72) {
        auto time = facebook::react::ReactMarker::StartupLogger::getInstance().getRunJSBundleStartTime();
        return std::isnan(time) ? -1 : time;
    } else {
        return runJSBundleStartTime;
    }
}

int64_t getRunJSBundleEndTime() {
    if constexpr(facebook::react::ReactNativeVersion.Minor >= 72) {
        auto time = facebook::react::ReactMarker::StartupLogger::getInstance().getRunJSBundleEndTime();
        return std::isnan(time) ? -1 : time;
    } else {
        return runJSBundleEndTime;
    }
}

- (NSArray<NSString *> *)supportedEvents {
    return @[EmitAppMeasurementsIdentifier, EmitDynatraceConfiguration];
}

int64_t GetTimestampUnix()
{
    auto millisecondsUTC = std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::system_clock::now().time_since_epoch()).count();
    return millisecondsUTC;
}

/*
In RN < 72, logTaggedMarker and logTaggedMarkerBridgeless are lambdas. In RN >= 72, these symbols are functions.
Relying on an if constexpr else does not work since types get checked before one branch in an if constexpr else gets discarded.
We cannot use a fallback declaration since since the symbol is declared as a function in RN >=72.
We cannot use preprocessor directives since we don't have the React Native version as preprocessor macro.
We cannot use C++ concepts since C++20 is not available.
Thus we use SFINAE. This works since types only get checked after template resolution.
*/
template<typename T = void> std::enable_if_t<(facebook::react::ReactNativeVersion.Minor < 72), T> registerLogTaggedMarkerCallbacks() {
    using namespace facebook::react::ReactMarker;
    auto logTaggedMarkerCommon = [](const ReactMarkerId markerId, const char* tag, auto previousLogTaggedMarker) {
        if (previousLogTaggedMarker) {
            previousLogTaggedMarker(markerId, tag);
        }

        if (markerId == RUN_JS_BUNDLE_START) {
            runJSBundleStartTime = GetTimestampUnix();
        } else if (markerId == RUN_JS_BUNDLE_STOP) {
            runJSBundleEndTime = GetTimestampUnix();
        }
    };

    auto previousLogTaggedMarker = logTaggedMarker;
    logTaggedMarker = [=](const ReactMarkerId markerId, const char* tag) {
        logTaggedMarkerCommon(markerId, tag, previousLogTaggedMarker);
    };

    auto previousLogTaggedMarkerBridgeless = logTaggedMarkerBridgeless;
    logTaggedMarkerBridgeless = [=](const ReactMarkerId markerId, const char* tag) {
        logTaggedMarkerCommon(markerId, tag, previousLogTaggedMarkerBridgeless);
    };
}

template<typename T = void> std::enable_if_t<(facebook::react::ReactNativeVersion.Minor >= 72), T> registerLogTaggedMarkerCallbacks() {}

/**
* When bridge is starting we will add an observer for content did appear so we can emit our app start measurements
*/
- (void)setBridge:(RCTBridge *)bridge
{
    [super setBridge:bridge];
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(contentAppeared) name:RCTContentDidAppearNotification object:nil];
}

- (BOOL)isReady
{
    return contentAppeared != -1 && getRunJSBundleEndTime() != -1;
}

- (void) contentAppeared
{
    contentAppeared = GetTimestampUnix();
    [self emitMeasurements];
}

/**
* Emitting all events which are available and important for our app start measurements
*/
- (void)emitMeasurements
{
    if (!didEmit && hasListeners && [self isReady]) {
        didEmit = YES;

        NSMutableDictionary<NSString*, NSNumber*> *appStartMeasurements = [[NSMutableDictionary alloc] init];

        [appStartMeasurements setObject:[self correctionOfTimestamp:getRunJSBundleStartTime()] forKey:RunJSBundleStart];
        [appStartMeasurements setObject:[self correctionOfTimestamp:getRunJSBundleEndTime()] forKey:RunJSBundleEnd];
        [appStartMeasurements setObject:[NSNumber numberWithLongLong:contentAppeared] forKey:ContentAppeared];

        if (hasListeners) {
            [self sendEventWithName:EmitAppMeasurementsIdentifier body:appStartMeasurements];
        }
    }
}

- (NSNumber* )correctionOfTimestamp:(int64_t) timestamp
{
    if constexpr (facebook::react::ReactNativeVersion.Minor < 72) {
         // In RN < 72, timestamps are unix timestamps
        return [NSNumber numberWithLongLong:timestamp];
    } else {
        // IN RN >= 72, timestamps are relative to the system start, which is we we add the unix timestamp of the system start
        return [NSNumber numberWithLongLong:timestamp + GetTimestampUnix() - (CACurrentMediaTime() * 1000)];
    }
}

/**
* Invalidation should remove the observer for certain events
*/
- (void)invalidate
{
    [super invalidate];
    [NSNotificationCenter.defaultCenter removeObserver:self];
}

/**
* Triggered when starting to emit events
*/

- (void)startObserving
{
    hasListeners = YES;
    [self emitMeasurements];

    [self setupRuntimeConfigurationListenerIfNeeded];

    if (lastRuntimeConfiguration != nil) {
        [self emitToJS:EmitDynatraceConfiguration body:lastRuntimeConfiguration];
    }
}

/**
* Triggered when stopping to emit events
*/
-(void)stopObserving
{
    hasListeners = NO;
}

- (void)setupRuntimeConfigurationListenerIfNeeded
{
    if (remoteConfigSubscribed) {
        return;
    }
    remoteConfigSubscribed = YES;
    [HybridBridge addConfigurationSubscriber:(id)configurationSubscriber];
}

- (void)emitToJS:(NSString*)event body:(id)body
{
    if (self.bridge == nil) {
        return;
    }

    id payload = body == nil ? [NSNull null] : body;

    dispatch_async(dispatch_get_main_queue(), ^{
        [self.bridge.eventDispatcher sendDeviceEventWithName:event body:payload];
    });
}

- (void)handleRuntimeConfiguration:(NSDictionary<NSString*, id>*)configuration
{
    lastRuntimeConfiguration = configuration;

    if (!hasListeners) {
        return;
    }

    [self emitToJS:EmitDynatraceConfiguration body:configuration];
}

/**
* Constants which are exported to the JS part
*/
- (NSDictionary *)constantsToExport
{
    return @{ @"PLATFORM_ANDROID" : PlatformAndroid,
              @"PLATFORM_IOS" : PlatformiOS,
              @"DATA_COLLECTION_OFF" : DataCollectionOff,
              @"DATA_COLLECTION_PERFORMANCE" : DataCollectionPerformance,
              @"DATA_COLLECTION_USERBEHAVIOR" : DataCollectionUserBehavior };
}

RCT_EXPORT_METHOD(start:(NSDictionary *) options)
{
    if (options == nil) {
        return;
    }

    NSMutableDictionary<NSString*, id> *properties = [[NSMutableDictionary alloc] init];

    if (options[@"applicationId"] != NULL) {
        properties[@"DTXApplicationID"] = options[@"applicationId"];
    }

    if (options[@"beaconUrl"] != NULL) {
        properties[@"DTXBeaconURL"] = options[@"beaconUrl"];
    }

    if (options[@"userOptIn"] != NULL && [[options valueForKey:@"userOptIn"] isEqual: @(YES)]) {
        properties[@"DTXUserOptIn"] = @YES;
    }

    if (options[@"reportCrash"] != NULL && [[options valueForKey:@"reportCrash"] isEqual: @(NO)]) {
        properties[@"DTXCrashReportingEnabled"] = @NO;
    }

    if (options[@"logLevel"] != NULL && [((NSNumber *) options[@"logLevel"]) intValue] == 0){
        properties[@"DTXLogLevel"] = @"ALL";
    }

    if (properties[@"DTXBeaconURL"] != NULL && properties[@"DTXApplicationID"] != NULL) {
        dispatch_async(dispatch_get_main_queue(), ^{
            NSLog(@"Dynatrace Agent runs on the main thread.");
            [Dynatrace startupWithConfig:properties];
        });
    }
}

RCT_EXPORT_METHOD(enterAction:(NSString *)name key:(nonnull NSString *)key platform: (NSString *) platform)
{
  if ([self shouldWorkOnIosWithPlatform: platform])
  {
    [self newAction:name key:key parentAction:nil];
  }
}

RCT_EXPORT_METHOD(enterManualAction:(NSString *)name key:(nonnull NSString *)key platform: (NSString *) platform)
{
  if ([self shouldWorkOnIosWithPlatform: platform])
  {
    DTXAction *action = [DTXAction enterActionWithName:name];

    if (action)
    {
        [actionDict setObject:action forKey:key];
    }
  }
}

RCT_EXPORT_METHOD(enterManualActionWithParent:(NSString *)name key:(nonnull NSString *)key parentKey:(nonnull NSString *)parentKey platform: (NSString *) platform)
{
    if ([self shouldWorkOnIosWithPlatform: platform])
    {
        DTXAction *parentAction = [self getAction:parentKey];

        if (parentAction != nil)
        {
            DTXAction *childAction = [DTXAction enterActionWithName:name parentAction:parentAction];

            if (childAction)
            {
                [actionDict setObject:childAction forKey:key];
            }
        }
        else
        {
            [self enterManualAction:name key:key platform:platform];
        }
    }
}

RCT_EXPORT_METHOD(leaveAction:(nonnull NSString *)key leave: (BOOL) leave platform: (NSString *) platform)
{
    if ([self shouldWorkOnIosWithPlatform: platform])
    {
        DTXAction *action = [self getAction:key];
        if (action == nil) return;
        [actionDict removeObjectForKey:key];

        if(leave)
        {
            [action leaveAction];
        }
    }
}

RCT_EXPORT_METHOD(cancelAction:(nonnull NSString *)key platform: (NSString *) platform)
{
    if ([self shouldWorkOnIosWithPlatform: platform])
    {
        DTXAction *action = [self getAction:key];
        if (action == nil) return;
        [actionDict removeObjectForKey:key];
        [action cancelAction];
    }
}

RCT_EXPORT_METHOD(endVisit: (NSString *) platform)
{
    if ([self shouldWorkOnIosWithPlatform: platform])
    {
        [Dynatrace endVisit];
    }
}

RCT_EXPORT_METHOD(startView: (NSString *) name)
{
    [Dynatrace startViewWithName:name];
}

RCT_EXPORT_METHOD(reportErrorWithoutStacktrace:(NSString *)errorName errorCode:(nonnull NSNumber *)errorCode platform: (NSString *) platform)
{
    if ([self shouldWorkOnIosWithPlatform: platform])
    {
        DTXAction *action = [DTXAction enterActionWithName:@"Error"];
        [action reportErrorWithName:errorName errorValue:[errorCode intValue]];
        [action leaveAction];
    }
}

RCT_EXPORT_METHOD(reportError:(NSString *)errorName errorValue:(NSString *)errorValue errorReason:(NSString *)errorReason stacktrace:(NSString *)stacktrace platform: (NSString *) platform)
{
    if ([self shouldWorkOnIosWithPlatform: platform])
    {
        [DTXAction reportExternalErrorForPlatformType:DTXActionPlatformCustom errorName:errorName errorValue:errorValue reason:errorReason stacktrace:stacktrace];
    }
}

RCT_EXPORT_METHOD(reportCrash:(NSString *)errorName errorReason:(NSString *)errorReason stacktrace:(NSString *)stacktrace isRealError:(BOOL)isRealError platform: (NSString *) platform)
{
    if ([self shouldWorkOnIosWithPlatform: platform])
    {
        if(isRealError){
            [DTXAction reportExternalCrashForPlatformType:DTXActionPlatformJavaScript crashName:errorName reason:errorReason stacktrace:stacktrace];
        }else{
            [DTXAction reportExternalCrashForPlatformType:DTXActionPlatformCustom crashName:errorName reason:errorReason stacktrace:stacktrace];
        }

        // Always end the session as the iOS agent has no troubles with this behavior
        [Dynatrace endVisit];
    }
}

RCT_EXPORT_METHOD(storeCrash:(NSString *)crashName reason:(NSString *)reason stacktrace:(NSString *)stacktrace) {
    NSString* dirPath = [[[[NSFileManager defaultManager] URLsForDirectory:NSApplicationSupportDirectory inDomains:NSUserDomainMask] firstObject] path];
    dirPath = [dirPath stringByAppendingPathComponent:@"DTXExternalCrashes"];
    // Create the Application Support/DTXExternalCrashes directory if it does not exist
    if (![[NSFileManager defaultManager] fileExistsAtPath:dirPath]) {
        NSError *e;
        [[NSFileManager defaultManager] createDirectoryAtPath:dirPath withIntermediateDirectories:YES attributes:nil error:&e];
        NSAssert(e == nil, @"Couldn't create Application Support directory, %@", e.localizedDescription);
    }
    NSDictionary* crashDict = @{ @"crashName": crashName, @"reason": reason, @"stacktrace": stacktrace, @"technologyType": @"j" };
    NSData* jsonData = [NSJSONSerialization dataWithJSONObject:crashDict options:NSJSONWritingPrettyPrinted error:nil];
    NSString* crashJson= [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding];
    // Store react native crash
    [[NSFileManager defaultManager] createFileAtPath:[dirPath stringByAppendingPathComponent:@"RNCrash.txt"] contents:[crashJson dataUsingEncoding:NSUTF8StringEncoding] attributes:nil];
}

RCT_EXPORT_METHOD(reportErrorInAction:(nonnull NSString *)key errorName:(NSString *)errorName errorCode:(nonnull NSNumber *)errorCode platform: (NSString *) platform)
{
    if ([self shouldWorkOnIosWithPlatform: platform])
    {
        DTXAction *action = [self getAction:key];
        if (action == nil)
            return;
        [action internalReportError:errorName errorValue:[errorCode intValue]];
    }
}

RCT_EXPORT_METHOD(reportStringValueInAction:(nonnull NSString *)actionKey withName:(NSString *)name value: (NSString *)value platform: (NSString *) platform)
{
  if ([self shouldWorkOnIosWithPlatform: platform])
  {
    DTXAction *action = [self getAction:actionKey];
    if (action == nil) return;
    [action reportValueWithName:name stringValue:value];
  }
}

RCT_EXPORT_METHOD(reportIntValueInAction:(nonnull NSString *)actionKey withName:(NSString *)name value: (nonnull NSNumber *)value platform: (NSString *) platform)
{
  if ([self shouldWorkOnIosWithPlatform: platform])
  {
    DTXAction *action = [self getAction:actionKey];
    if (action == nil) return;
    [action reportValueWithName:name intValue:value.intValue];
  }
}

RCT_EXPORT_METHOD(reportDoubleValueInAction:(nonnull NSString *)actionKey withName:(NSString *)name value: (nonnull NSNumber *)value platform: (NSString *) platform)
{
  if ([self shouldWorkOnIosWithPlatform: platform])
  {
    DTXAction *action = [self getAction:actionKey];
    if (action == nil) return;
    [action reportValueWithName:name doubleValue:value.doubleValue];
  }
}

RCT_EXPORT_METHOD(getRequestTag:(nonnull NSString *)actionKey withUrl:(NSString *)url findEventsWithResolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject)
{
    DTXAction* action = [self getAction:actionKey];
    resolve([action getTagForURL:[NSURL URLWithString:url]]);
}

RCT_EXPORT_METHOD(startWebRequestTiming:(NSString*) requestTag url:(NSString*) url)
{
    if(requestTag != NULL && url != NULL){
        DTXWebRequestTiming* timing = [DTXWebRequestTiming getDTXWebRequestTiming:requestTag requestUrl:[NSURL URLWithString:url]];
        if (timing != NULL) {
            [webTimingsDict setObject:timing forKey:[NSString stringWithString:requestTag]];
            [timing startWebRequestTiming];
        }
    }
}

- (void)stopWebRequestTimingHelper:(NSString*) requestTag url:(NSString*)url responseCode:(nonnull NSNumber*) responseCode responseMessage:(NSString*)responseMessage bytesSent:(nonnull NSNumber*)bytesSent bytesReceived:(nonnull NSNumber*)bytesReceived
{
    if(requestTag != NULL){
        DTXWebRequestTiming* timing = [webTimingsDict objectForKey:requestTag];
        if(timing){
            [timing stopWebRequestTiming:[responseCode stringValue] bytesSent:[bytesSent longLongValue] bytesReceived:[bytesReceived longLongValue]];
            [webTimingsDict removeObjectForKey:requestTag];
        }
    }
}

RCT_EXPORT_METHOD(stopWebRequestTiming:(NSString*) requestTag url:(NSString*)url responseCode:(nonnull NSNumber*) responseCode responseMessage:(NSString*)responseMessage)
{
    [self stopWebRequestTimingHelper:requestTag url:url responseCode:responseCode responseMessage:responseMessage bytesSent:0 bytesReceived:0];
}

RCT_EXPORT_METHOD(stopWebRequestTimingWithSize:(NSString*) requestTag url:(NSString*)url responseCode:(nonnull NSNumber*) responseCode responseMessage:(NSString*)responseMessage requestSize:(nonnull NSNumber*)requestSize responseSize:(nonnull NSNumber*)responseSize)
{
    [self stopWebRequestTimingHelper:requestTag url:url responseCode:responseCode responseMessage:responseMessage bytesSent:requestSize bytesReceived:responseSize];
}

RCT_EXPORT_METHOD(identifyUser:(NSString *)user platform: (NSString *) platform)
{
    if ([self shouldWorkOnIosWithPlatform: platform])
    {
        [Dynatrace identifyUser:user];
    }
}

RCT_EXPORT_METHOD(reportEventInAction:(nonnull NSString *)actionKey withName: (NSString *)name platform: (NSString *) platform)
{
    if ([self shouldWorkOnIosWithPlatform: platform])
    {
        DTXAction *action = [self getAction:actionKey];
        if (action == nil) return;
        [action reportEventWithName: name];
    }
}

RCT_EXPORT_METHOD(sendBizEvent:(NSString *)type withAttributes:(NSDictionary<NSString*, id>*) attributes platform: (NSString *) platform)
{
    if ([self shouldWorkOnIosWithPlatform: platform])
    {
        [Dynatrace sendBizEventWithType:type attributes:attributes];
    }
}

RCT_EXPORT_METHOD(forwardEvent:(NSDictionary<NSString*, id>*) attributes)
{
    [HybridBridge forwardEvent:attributes];
}

RCT_EXPORT_METHOD(forwardAppStartEvent:(NSDictionary<NSString*, id>*) attributes withAppStartKeys:(NSArray<NSString*>* _Nullable) appStartKeys)
{
    [HybridBridge forwardAppStartEvent:attributes keys:appStartKeys];
}

RCT_EXPORT_METHOD(setGPSLocation:(double)latitude andLongitude: (double)longitude platform: (NSString *) platform)
{
    if ([self shouldWorkOnIosWithPlatform: platform])
    {
        CLLocation *location = [[CLLocation alloc] initWithLatitude:latitude longitude:longitude];
        [Dynatrace setGpsLocation:location];
    }
}

RCT_EXPORT_METHOD(flushEvents:(NSString *) platform)
{
    if ([self shouldWorkOnIosWithPlatform: platform])
    {
        [Dynatrace flushEvents];
    }
}

RCT_EXPORT_METHOD(isCrashReportingOptedIn:(NSString *) platform findEventsWithResolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject)
{
    if ([self shouldWorkOnIosWithPlatform: platform])
    {
        if([Dynatrace crashReportingOptedIn]){
            resolve(@"true");
        }else{
            resolve(@"false");
        }
    }
}

RCT_EXPORT_METHOD(setCrashReportingOptedIn:(BOOL) crashReportingOptedIn platform: (NSString *) platform)
{
    if ([self shouldWorkOnIosWithPlatform: platform])
    {
        [Dynatrace setCrashReportingOptedIn:crashReportingOptedIn];
    }
}

RCT_EXPORT_METHOD(getDataCollectionLevel:(NSString *) platform findEventsWithResolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject)
{
    if ([self shouldWorkOnIosWithPlatform: platform])
    {
        int i = [Dynatrace dataCollectionLevel];

        if(i == DTX_DataCollectionUserBehavior){
            resolve(DataCollectionUserBehavior);
        }else if(i == DTX_DataCollectionPerformance){
            resolve(DataCollectionPerformance);
        }else{
            resolve(DataCollectionOff);
        }
    }
}

RCT_EXPORT_METHOD(setDataCollectionLevel:(nonnull NSString *) dataCollectionLevel platform: (NSString *) platform)
{
    if ([self shouldWorkOnIosWithPlatform: platform])
    {
        if([dataCollectionLevel isEqualToString: DataCollectionPerformance]){
            [Dynatrace setDataCollectionLevel:DTX_DataCollectionPerformance completion:^(BOOL successful) {}];
        }else if([dataCollectionLevel isEqualToString: DataCollectionUserBehavior]){
            [Dynatrace setDataCollectionLevel:DTX_DataCollectionUserBehavior completion:^(BOOL successful) {}];
        }else{
            [Dynatrace setDataCollectionLevel:DTX_DataCollectionOff completion:^(BOOL successful) {}];
        }
    }
}

RCT_EXPORT_METHOD(setBeaconHeaders:(NSDictionary *) headers platform: (NSString *) platform)
{
    if ([self shouldWorkOnIosWithPlatform: platform])
    {
        if (!headers) {
            [Dynatrace setBeaconHeaders:nil];
        } else {
            [Dynatrace setBeaconHeaders:headers];
        }
    }
}

RCT_EXPORT_METHOD(getUserPrivacyOptions:(NSString *) platform findEventsWithResolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject)
{
    if ([self shouldWorkOnIosWithPlatform: platform])
    {
        id<DTXUserPrivacyOptions> privacyConfig = [Dynatrace userPrivacyOptions];
        NSString * level;

        if(privacyConfig.dataCollectionLevel == DTX_DataCollectionUserBehavior){
            level = DataCollectionUserBehavior;
        }else if(privacyConfig.dataCollectionLevel == DTX_DataCollectionPerformance){
            level = DataCollectionPerformance;
        }else{
            level = DataCollectionOff;
        }

        NSDictionary *privacyDict = @{@"dataCollectionLevel": level, @"crashReportingOptedIn": [NSNumber numberWithBool: privacyConfig.crashReportingOptedIn], @"screenRecordOptedIn": [NSNumber numberWithBool: privacyConfig.screenRecordOptedIn]};

        resolve(privacyDict);
    }
}

RCT_EXPORT_METHOD(applyUserPrivacyOptions:(NSDictionary *) userPrivacyOptions platform: (NSString *) platform)
{
    if ([self shouldWorkOnIosWithPlatform: platform])
    {
        id<DTXUserPrivacyOptions> privacyConfig = [Dynatrace userPrivacyOptions];

        if([[userPrivacyOptions valueForKey:@"_dataCollectionLevel"] isEqualToString: DataCollectionPerformance]){
            privacyConfig.dataCollectionLevel = DTX_DataCollectionPerformance;
        }else if([[userPrivacyOptions valueForKey:@"_dataCollectionLevel"] isEqualToString: DataCollectionUserBehavior]){
            privacyConfig.dataCollectionLevel = DTX_DataCollectionUserBehavior;
        }else if([[userPrivacyOptions valueForKey:@"_dataCollectionLevel"] isEqualToString: DataCollectionOff]){
            privacyConfig.dataCollectionLevel = DTX_DataCollectionOff;
        } else {
            // do nothing and keep current value
        }

        if ([[userPrivacyOptions valueForKey:@"_crashReportingOptedIn"] isEqual: @(YES)]) {
            privacyConfig.crashReportingOptedIn = YES;
        } else if ([[userPrivacyOptions valueForKey:@"_crashReportingOptedIn"]  isEqual: @(NO)]) {
            privacyConfig.crashReportingOptedIn = NO;
        } else {
            // do nothing and keep current value
        }

        if ([[userPrivacyOptions valueForKey:@"_screenRecordOptedIn"] isEqual: @(YES)]) {
            privacyConfig.screenRecordOptedIn = @(YES);
        } else if ([[userPrivacyOptions valueForKey:@"_screenRecordOptedIn"] isEqual: @(NO)]) {
            privacyConfig.screenRecordOptedIn = @(NO);
        }

        [Dynatrace applyUserPrivacyOptions:privacyConfig completion:^(BOOL successful) {
            // do nothing with callback
        }];
    }
}

RCT_EXPORT_METHOD(getCurrentConfiguration:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject)
{
    if (lastRuntimeConfiguration != nil) {
        resolve(lastRuntimeConfiguration);
    } else {
        resolve([NSNull null]);
    }
}

RCT_EXPORT_METHOD(startViewInternal:(NSDictionary<NSString*, id>*) fields)
{
    [HybridBridge startView:fields];
}

RCT_EXPORT_METHOD(stopViewInternal)
{
    [HybridBridge stopView];
}

- (void)newAction:(NSString *)name key:(nonnull NSString *)key parentAction:(DTXAction *)parentAction
{
    DTXAction *action = [DTXAction integrateActionWithName:name];

    if (action)
    {
        [actionDict setObject:action forKey:key];
    }
}

- (DTXAction *)getAction:(nonnull NSString *)key
{
  return [actionDict objectForKey:key];
}

+ (BOOL)requiresMainQueueSetup
{
  return YES;
}

- (BOOL)shouldWorkOnIosWithPlatform: (NSString *) platform
{
    return platform == nil || [platform isEqualToString:PlatformiOS] || [platform isEqualToString:@""];
}

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

@end