#import "RNSentry.h"
#import "RNSentryTimeToDisplay.h"
#import <dlfcn.h>

#if __has_include(<React/RCTConvert.h>)
#    import <React/RCTConvert.h>
#else
#    import "RCTConvert.h"
#endif

#if __has_include(<hermes/hermes.h>) && SENTRY_PROFILING_SUPPORTED
#    define SENTRY_PROFILING_ENABLED 1
#    import <Sentry/SentryProfilingConditionals.h>
#else
#    define SENTRY_PROFILING_ENABLED 0
#    define SENTRY_TARGET_PROFILING_SUPPORTED 0
#endif

#import "RNSentryBreadcrumb.h"
#import "RNSentryId.h"
#import <Sentry/PrivateSentrySDKOnly.h>
#import <Sentry/SentryAppStartMeasurement.h>
#import <Sentry/SentryBreadcrumb.h>
#import <Sentry/SentryDebugMeta.h>
#import <Sentry/SentryEvent.h>
#import <Sentry/SentryException.h>
#import <Sentry/SentryUser.h>

@interface SentryUser ()
- (instancetype)initWithDictionary:(NSDictionary *)dictionary;
@end

// This guard prevents importing Hermes in JSC apps
#if SENTRY_PROFILING_ENABLED
#    import <hermes/hermes.h>
#endif

// Thanks to this guard, we won't import this header when we build for the old architecture.
#ifdef RCT_NEW_ARCH_ENABLED
#    import "RNSentrySpec.h"
#endif

#import "RNSentryDependencyContainer.h"
#import "RNSentryEvents.h"
#import "RNSentryNativeLogsForwarder.h"

#if SENTRY_TARGET_REPLAY_SUPPORTED
#    import "RNSentryReplay.h"
#endif

#if SENTRY_HAS_UIKIT
#    import "RNSentryEmitNewFrameEvent.h"
#    import "RNSentryRNSScreen.h"
#endif

#import "RNSentryExperimentalOptions.h"
#import "RNSentryStart.h"
#import "RNSentryVersion.h"
#import "SentrySDKWrapper.h"

// TurboModule perf logger — only available on New Architecture, but we always
// include the header so the `Sentry_SetTurboModuleTrackingEnabled` toggle
// compiles on Old Arch too (it's a no-op there).
#import "../cpp/SentryTurboModulePerfLogger.h"
#import "SentryScreenFramesWrapper.h"

static bool hasFetchedAppStart;

@implementation RNSentry {
    bool hasListeners;
    bool _shakeDetectionEnabled;
    RNSentryTimeToDisplay *_timeToDisplay;
    NSArray<NSString *> *_ignoreErrorPatternsStr;
    NSArray<NSRegularExpression *> *_ignoreErrorPatternsRegex;
}

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

+ (BOOL)requiresMainQueueSetup
{
    return NO;
}

// Strict type match for the JS `enableTurboModuleTracking` option — we only
// honour a real boolean. Android requires `ReadableType.Boolean`, and this
// helper enforces the same cross-platform contract: a JS numeric `1` must
// NOT enable tracking.
//
// `CFBooleanGetTypeID` is the canonical, toll-free-bridged way to
// distinguish `@YES` from `@1`. Comparing `[num objCType]` against
// `@encode(BOOL)` does NOT work on 64-bit iOS: `BOOL` is `typedef bool`
// there, so `@encode(BOOL)` is `"B"`, but every `NSNumber` created from a
// boolean (including everything crossing the RN bridge) always reports
// `objCType == "c"` for historical compatibility. An earlier revision had
// that bug — the `strcmp` check never matched and tracking was a no-op on
// every modern iOS device. Centralising the check behind one method keeps
// the trap from coming back.
+ (BOOL)turboModuleTrackingEnabledFromOptions:(NSDictionary *)options
{
    id value = [options objectForKey:@"enableTurboModuleTracking"];
    if (![value isKindOfClass:[NSNumber class]]) {
        return NO;
    }
    if (CFGetTypeID((__bridge CFTypeRef)value) != CFBooleanGetTypeID()) {
        return NO;
    }
    return [(NSNumber *)value boolValue];
}

- (instancetype)init
{
    if (self = [super init]) {
        _timeToDisplay = [[RNSentryTimeToDisplay alloc] init];
    }
    return self;
}

- (NSMutableDictionary *)prepareOptions:(NSDictionary *)options
{
    SentryBeforeSendEventCallback beforeSend = ^SentryEvent *(SentryEvent *event) {
        // We don't want to send an event after startup that came from a Unhandled JS Exception of
        // React Native because we sent it already before the app crashed.
        if (nil != event.exceptions.firstObject.type &&
            [event.exceptions.firstObject.type rangeOfString:@"Unhandled JS Exception"].location
                != NSNotFound) {
            return nil;
        }

        // With New Architecture, React Native wraps JS errors in C++ exceptions.
        // These exceptions are caught by the native crash handler and should be filtered out
        // since the JS error is already reported by the JS error handler.
        // The key indicator is "ExceptionsManager.reportException" in the exception value,
        // which is React Native's mechanism for reporting JS errors to the native layer.
        for (SentryException *exception in event.exceptions) {
            if (nil != exception.value &&
                [exception.value rangeOfString:@"ExceptionsManager.reportException"].location
                    != NSNotFound) {
                return nil;
            }
        }

        // Regex and Str are set when one of them has value so we only need to check one of them.
        if (self->_ignoreErrorPatternsStr || self->_ignoreErrorPatternsRegex) {
            for (SentryException *exception in event.exceptions) {
                if ([self shouldIgnoreError:exception.value]) {
                    return nil;
                }
            }
            if ([self shouldIgnoreError:event.message.message]) {
                return nil;
            }
        }

        [self setEventOriginTag:event];
        return event;
    };

    NSMutableDictionary *mutableOptions = [options mutableCopy];
    [mutableOptions setValue:beforeSend forKey:@"beforeSend"];

    // remove performance traces sample rate and traces sampler since we don't want to synchronize
    // these configurations to the Native SDKs. The user could tho initialize the SDK manually and
    // set themselves.
    [mutableOptions removeObjectForKey:@"tracesSampleRate"];
    [mutableOptions removeObjectForKey:@"tracesSampler"];
    [mutableOptions removeObjectForKey:@"enableTracing"];

    // `enableTurboModuleTracking` is consumed by `initNativeSdk` before this
    // dict reaches sentry-cocoa; strip so it does not leak into
    // SentryOptions (which would not know what to do with it).
    [mutableOptions removeObjectForKey:@"enableTurboModuleTracking"];

    [self trySetIgnoreErrors:mutableOptions];

    return mutableOptions;
}

RCT_EXPORT_MODULE()
RCT_EXPORT_METHOD(initNativeSdk : (NSDictionary *_Nonnull)options resolve : (
    RCTPromiseResolveBlock)resolve rejecter : (RCTPromiseRejectBlock)reject)
{
    NSMutableDictionary *mutableOptions = [self prepareOptions:options];

    NSError *error = nil;
    [RNSentryStart startWithOptions:mutableOptions error:&error];
    if (error != nil) {
        reject(@"SentryReactNative", error.localizedDescription, error);
        return;
    }

    // Toggle the TurboModule perf-logger sink based on the JS option. Only
    // do this after the native SDK has started successfully — otherwise a
    // rejected `initNativeSdk` would still leave tracking on (and would
    // claim the perf-logger slot via lazy install) while no SDK is around to
    // receive the data.
    //
    // Always reconcile to a concrete boolean (defaulting to `0`) so a
    // re-init that omits the key cannot leave a previous opt-in latched on:
    // the native controller is process-wide and not reset by closeNativeSdk.
    // `setEnabled(false)` is cheap and never triggers the lazy install, so
    // the RN perf-logger slot stays untouched while the option is off.
    Sentry_SetTurboModuleTrackingEnabled(
        [RNSentry turboModuleTrackingEnabledFromOptions:options] ? 1 : 0);

    // RNSentryStart.startWithOptions already handles:
    // - Session tracking notification (SentryHybridSdkDidBecomeActive)
    // - Replay postInit
    // - SDK initialization
    resolve(@YES);
}

- (void)trySetIgnoreErrors:(NSDictionary *)options
{
    NSArray *ignoreErrorsStr = nil;
    NSArray *ignoreErrorsRegex = nil;

    id strArr = [options objectForKey:@"ignoreErrorsStr"];
    id regexArr = [options objectForKey:@"ignoreErrorsRegex"];
    if ([strArr isKindOfClass:[NSArray class]]) {
        ignoreErrorsStr = (NSArray *)strArr;
    }
    if ([regexArr isKindOfClass:[NSArray class]]) {
        ignoreErrorsRegex = (NSArray *)regexArr;
    }

    NSMutableArray<NSString *> *strs = [NSMutableArray array];
    NSMutableArray<NSRegularExpression *> *regexes = [NSMutableArray array];

    if (ignoreErrorsStr != nil) {
        for (id str in ignoreErrorsStr) {
            if ([str isKindOfClass:[NSString class]]) {
                [strs addObject:str];
            }
        }
    }

    if (ignoreErrorsRegex != nil) {
        for (id pattern in ignoreErrorsRegex) {
            if ([pattern isKindOfClass:[NSString class]]) {
                NSError *error = nil;
                NSRegularExpression *regex =
                    [NSRegularExpression regularExpressionWithPattern:pattern
                                                              options:0
                                                                error:&error];
                if (regex && error == nil) {
                    [regexes addObject:regex];
                }
            }
        }
    }

    _ignoreErrorPatternsStr = [strs count] > 0 ? [strs copy] : nil;
    _ignoreErrorPatternsRegex = [regexes count] > 0 ? [regexes copy] : nil;
}

- (BOOL)shouldIgnoreError:(NSString *)message
{
    if ((!_ignoreErrorPatternsStr && !_ignoreErrorPatternsRegex) || !message) {
        return NO;
    }

    for (NSString *str in _ignoreErrorPatternsStr) {
        if ([message containsString:str]) {
            return YES;
        }
    }

    for (NSRegularExpression *regex in _ignoreErrorPatternsRegex) {
        NSRange range = NSMakeRange(0, message.length);
        if ([regex firstMatchInString:message options:0 range:range]) {
            return YES;
        }
    }

    return NO;
}

- (void)setEventOriginTag:(SentryEvent *)event
{
    if (event.sdk != nil) {
        NSString *sdkName = event.sdk[@"name"];

        // If the event is from react native, it gets set
        // there and we do not handle it here.
        if ([sdkName isEqual:NATIVE_SDK_NAME]) {
            [self setEventEnvironmentTag:event origin:@"ios" environment:@"native"];
        }
    }
}

- (void)setEventEnvironmentTag:(SentryEvent *)event
                        origin:(NSString *)origin
                   environment:(NSString *)environment
{
    NSMutableDictionary *newTags = [NSMutableDictionary new];

    if (nil != event.tags && [event.tags count] > 0) {
        [newTags addEntriesFromDictionary:event.tags];
    }
    if (nil != origin) {
        [newTags setValue:origin forKey:@"event.origin"];
    }
    if (nil != environment) {
        [newTags setValue:environment forKey:@"event.environment"];
    }

    event.tags = newTags;
}

RCT_EXPORT_METHOD(initNativeReactNavigationNewFrameTracking : (
    RCTPromiseResolveBlock)resolve rejecter : (RCTPromiseRejectBlock)reject)
{
#if SENTRY_HAS_UIKIT
    if ([[NSThread currentThread] isMainThread]) {
        [RNSentryRNSScreen swizzleViewDidAppear];
    } else {
        dispatch_async(dispatch_get_main_queue(), ^{ [RNSentryRNSScreen swizzleViewDidAppear]; });
    }

    [self initFramesTracking];
#endif
    resolve(nil);
}

- (void)initFramesTracking
{
#if SENTRY_HAS_UIKIT
    RNSentryEmitNewFrameEvent emitNewFrameEvent = ^(NSNumber *newFrameTimestampInSeconds) {
        [RNSentryTimeToDisplay putTimeToInitialDisplayForActiveSpan:newFrameTimestampInSeconds];
    };
    [[RNSentryDependencyContainer sharedInstance]
        initializeFramesTrackerListenerWith:emitNewFrameEvent];
#endif
}

// Will be called when this module's first listener is added.
- (void)startObserving
{
    hasListeners = YES;
    [[RNSentryNativeLogsForwarder shared] configureWithEventEmitter:self];
}

// Will be called when this module's last listener is removed, or on dealloc.
- (void)stopObserving
{
    hasListeners = NO;
    [[RNSentryNativeLogsForwarder shared] stopForwarding];
}

- (void)handleShakeDetected
{
    if (_shakeDetectionEnabled) {
        [self sendEventWithName:RNSentryOnShakeEvent body:@{ }];
    }
}

// SentryShakeDetector is a Swift class; its notification name and methods are accessed
// via the raw string / NSClassFromString to avoid requiring @import Sentry in this .mm file.
static NSNotificationName const RNSentryShakeNotification = @"SentryShakeDetected";

RCT_EXPORT_METHOD(enableShakeDetection)
{
    [[NSNotificationCenter defaultCenter] removeObserver:self
                                                    name:RNSentryShakeNotification
                                                  object:nil];
    [[NSNotificationCenter defaultCenter] addObserver:self
                                             selector:@selector(handleShakeDetected)
                                                 name:RNSentryShakeNotification
                                               object:nil];
    Class shakeDetector = NSClassFromString(@"SentryShakeDetector");
    if (shakeDetector) {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
        [shakeDetector performSelector:@selector(enable)];
#pragma clang diagnostic pop
    }
    _shakeDetectionEnabled = YES;
}

RCT_EXPORT_METHOD(disableShakeDetection)
{
    _shakeDetectionEnabled = NO;
    Class shakeDetector = NSClassFromString(@"SentryShakeDetector");
    if (shakeDetector) {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
        [shakeDetector performSelector:@selector(disable)];
#pragma clang diagnostic pop
    }
    [[NSNotificationCenter defaultCenter] removeObserver:self
                                                    name:RNSentryShakeNotification
                                                  object:nil];
}

- (NSArray<NSString *> *)supportedEvents
{
    return @[ RNSentryNewFrameEvent, RNSentryNativeLogEvent, RNSentryOnShakeEvent ];
}

RCT_EXPORT_METHOD(
    fetchNativeSdkInfo : (RCTPromiseResolveBlock)resolve rejecter : (RCTPromiseRejectBlock)reject)
{
    resolve(@ {
        @"name" : PrivateSentrySDKOnly.getSdkName,
        @"version" : PrivateSentrySDKOnly.getSdkVersionString
    });
}

RCT_EXPORT_METHOD(
    fetchModules : (RCTPromiseResolveBlock)resolve rejecter : (RCTPromiseRejectBlock)reject)
{
    NSString *filePath = [[NSBundle mainBundle] pathForResource:@"modules" ofType:@"json"];
    NSString *modulesString = [NSString stringWithContentsOfFile:filePath
                                                        encoding:NSUTF8StringEncoding
                                                           error:nil];
    resolve(modulesString);
}

RCT_EXPORT_SYNCHRONOUS_TYPED_METHOD(NSString *, fetchNativePackageName)
{
    NSString *packageName = [[NSBundle mainBundle] executablePath];
    return packageName;
}

RCT_EXPORT_SYNCHRONOUS_TYPED_METHOD(
    NSDictionary *, fetchNativeStackFramesBy : (NSArray *)instructionsAddr)
{
    return [self fetchNativeStackFramesBy:instructionsAddr symbolicate:dladdr];
}

RCT_EXPORT_METHOD(fetchNativeLogAttributes : (RCTPromiseResolveBlock)resolve rejecter : (
    RCTPromiseRejectBlock)reject)
{
    __block NSMutableDictionary<NSString *, id> *result = [NSMutableDictionary new];

    [SentrySDKWrapper configureScope:^(SentryScope *_Nonnull scope) {
        // Serialize to get contexts dictionary
        NSDictionary *serializedScope = [scope serialize];
        NSDictionary *allContexts = serializedScope[@"context"]; // It's singular here, annoyingly

        NSMutableDictionary *contexts = [NSMutableDictionary new];

        NSDictionary *device = allContexts[@"device"];
        if ([device isKindOfClass:[NSDictionary class]]) {
            contexts[@"device"] = device;
        }

        NSDictionary *os = allContexts[@"os"];
        if ([os isKindOfClass:[NSDictionary class]]) {
            contexts[@"os"] = os;
        }

        NSString *releaseName = [SentrySDKWrapper releaseName];
        if (releaseName) {
            contexts[@"release"] = releaseName;
        }
        // Merge extra context
        NSDictionary *extraContext = [PrivateSentrySDKOnly getExtraContext];

        if (extraContext) {
            NSDictionary *extraDevice = extraContext[@"device"];
            if ([extraDevice isKindOfClass:[NSDictionary class]]) {
                NSMutableDictionary *mergedDevice =
                    [contexts[@"device"] mutableCopy] ?: [NSMutableDictionary new];
                [mergedDevice addEntriesFromDictionary:extraDevice];
                contexts[@"device"] = mergedDevice;
            }

            NSDictionary *extraOS = extraContext[@"os"];
            if ([extraOS isKindOfClass:[NSDictionary class]]) {
                NSMutableDictionary *mergedOS =
                    [contexts[@"os"] mutableCopy] ?: [NSMutableDictionary new];
                [mergedOS addEntriesFromDictionary:extraOS];
                contexts[@"os"] = mergedOS;
            }
        }
        result[@"contexts"] = contexts;
    }];
    resolve(result);
}

RCT_EXPORT_METHOD(fetchNativeDeviceContexts : (RCTPromiseResolveBlock)resolve rejecter : (
    RCTPromiseRejectBlock)reject)
{
    if ([SentrySDKWrapper debug]) {
        NSLog(@"Bridge call to: deviceContexts");
    }
    __block NSMutableDictionary<NSString *, id> *serializedScope;
    // Temp work around until sorted out this API in sentry-cocoa.
    // TODO: If the callback isnt' executed the promise wouldn't be resolved.
    [SentrySDKWrapper configureScope:^(SentryScope *_Nonnull scope) {
        serializedScope = [[scope serialize] mutableCopy];

        NSDictionary<NSString *, id> *user = [serializedScope valueForKey:@"user"];
        if (user == nil) {
            [serializedScope setValue:@ { @"id" : PrivateSentrySDKOnly.installationID }
                               forKey:@"user"];
        }

        if ([SentrySDKWrapper debug]) {
            NSData *data = [NSJSONSerialization dataWithJSONObject:serializedScope
                                                           options:0
                                                             error:nil];
            NSString *debugContext = [[NSString alloc] initWithData:data
                                                           encoding:NSUTF8StringEncoding];
            NSLog(@"Contexts: %@", debugContext);
        }
    }];

    NSDictionary<NSString *, id> *extraContext = [PrivateSentrySDKOnly getExtraContext];
    NSMutableDictionary<NSString *, NSDictionary<NSString *, id> *> *contexts =
        [serializedScope[@"context"] mutableCopy];

    if (extraContext && [extraContext[@"device"] isKindOfClass:[NSDictionary class]]) {
        NSMutableDictionary<NSString *, NSDictionary<NSString *, id> *> *deviceContext =
            [contexts[@"device"] mutableCopy];
        [deviceContext addEntriesFromDictionary:extraContext[@"device"]];
        [contexts setValue:deviceContext forKey:@"device"];
    }

    if (extraContext && [extraContext[@"app"] isKindOfClass:[NSDictionary class]]) {
        NSMutableDictionary<NSString *, NSDictionary<NSString *, id> *> *appContext =
            [contexts[@"app"] mutableCopy];
        [appContext addEntriesFromDictionary:extraContext[@"app"]];
        [contexts setValue:appContext forKey:@"app"];
    }

    [serializedScope setValue:contexts forKey:@"contexts"];
    [serializedScope removeObjectForKey:@"context"];

    // Remove react-native breadcrumbs
    NSPredicate *removeRNBreadcrumbsPredicate =
        [NSPredicate predicateWithBlock:^BOOL(NSDictionary *breadcrumb, NSDictionary *bindings) {
            return ![breadcrumb[@"origin"] isEqualToString:@"react-native"];
        }];
    NSArray *breadcrumbs = [[serializedScope[@"breadcrumbs"] mutableCopy]
        filteredArrayUsingPredicate:removeRNBreadcrumbsPredicate];
    [serializedScope setValue:breadcrumbs forKey:@"breadcrumbs"];

    resolve(serializedScope);
}

RCT_EXPORT_METHOD(
    fetchNativeAppStart : (RCTPromiseResolveBlock)resolve rejecter : (RCTPromiseRejectBlock)reject)
{
#if SENTRY_HAS_UIKIT
    NSDictionary<NSString *, id> *measurements =
        [PrivateSentrySDKOnly appStartMeasurementWithSpans];
    if (measurements == nil) {
        resolve(nil);
        return;
    }

    NSMutableDictionary<NSString *, id> *mutableMeasurements =
        [[NSMutableDictionary alloc] initWithDictionary:measurements];
    [mutableMeasurements setValue:[NSNumber numberWithBool:hasFetchedAppStart]
                           forKey:@"has_fetched"];

    // This is always set to true, as we would only allow an app start fetch to only happen once
    // in the case of a JS bundle reload, we do not want it to be instrumented again.
    hasFetchedAppStart = true;

    resolve(mutableMeasurements);
#else
    resolve(nil);
#endif
}

RCT_EXPORT_METHOD(
    fetchNativeFrames : (RCTPromiseResolveBlock)resolve rejecter : (RCTPromiseRejectBlock)reject)
{

#if TARGET_OS_IPHONE || TARGET_OS_MACCATALYST
    if (PrivateSentrySDKOnly.isFramesTrackingRunning) {
        if (![SentryScreenFramesWrapper canTrackFrames]) {
            resolve(nil);
            return;
        }

        resolve(@ {
            @"totalFrames" : [SentryScreenFramesWrapper totalFrames],
            @"frozenFrames" : [SentryScreenFramesWrapper frozenFrames],
            @"slowFrames" : [SentryScreenFramesWrapper slowFrames],
        });
    } else {
        resolve(nil);
    }
#else
    resolve(nil);
#endif
}

RCT_EXPORT_METHOD(fetchNativeFramesDelay : (double)startTimestampSeconds endTimestampSeconds : (
    double)endTimestampSeconds resolve : (RCTPromiseResolveBlock)
        resolve rejecter : (RCTPromiseRejectBlock)reject)
{
#if TARGET_OS_IPHONE || TARGET_OS_MACCATALYST
    if (![SentryScreenFramesWrapper canTrackFrames]) {
        resolve(nil);
        return;
    }

    resolve([SentryScreenFramesWrapper framesDelayForStartTimestamp:startTimestampSeconds
                                                       endTimestamp:endTimestampSeconds]);
#else
    resolve(nil);
#endif
}

RCT_EXPORT_METHOD(
    fetchNativeRelease : (RCTPromiseResolveBlock)resolve rejecter : (RCTPromiseRejectBlock)reject)
{
    NSDictionary *infoDict = [[NSBundle mainBundle] infoDictionary];
    resolve(@ {
        @"id" : infoDict[@"CFBundleIdentifier"],
        @"version" : infoDict[@"CFBundleShortVersionString"],
        @"build" : infoDict[@"CFBundleVersion"],
    });
}

RCT_EXPORT_METHOD(captureEnvelope : (NSString *_Nonnull)rawBytes options : (NSDictionary *_Nonnull)
        options resolve : (RCTPromiseResolveBlock)resolve rejecter : (RCTPromiseRejectBlock)reject)
{
    NSData *data = [[NSData alloc] initWithBase64EncodedString:rawBytes options:0];

    SentryEnvelope *envelope = [PrivateSentrySDKOnly envelopeWithData:data];
    if (envelope == nil) {
        reject(@"SentryReactNative", @"Failed to parse envelope from byte array.", nil);
        return;
    }

#if DEBUG
    [PrivateSentrySDKOnly captureEnvelope:envelope];
#else
    if ([[options objectForKey:@"hardCrashed"] boolValue]) {
        // Storing to disk happens asynchronously with captureEnvelope
        [PrivateSentrySDKOnly storeEnvelope:envelope];
    } else {
        [PrivateSentrySDKOnly captureEnvelope:envelope];
    }
#endif
    resolve(@YES);
}

RCT_EXPORT_METHOD(
    captureScreenshot : (RCTPromiseResolveBlock)resolve rejecter : (RCTPromiseRejectBlock)reject)
{
#if TARGET_OS_IPHONE || TARGET_OS_MACCATALYST
    NSArray<NSData *> *rawScreenshots = [PrivateSentrySDKOnly captureScreenshots];
    NSMutableArray *screenshotsArray = [NSMutableArray arrayWithCapacity:[rawScreenshots count]];

    int counter = 1;
    for (NSData *raw in rawScreenshots) {
        NSMutableArray *screenshot = [NSMutableArray arrayWithCapacity:raw.length];
        const char *bytes = (char *)[raw bytes];
        for (int i = 0; i < [raw length]; i++) {
            [screenshot addObject:[[NSNumber alloc] initWithChar:bytes[i]]];
        }

        NSString *filename = @"screenshot.png";
        if (counter > 1) {
            filename = [NSString stringWithFormat:@"screenshot-%d.png", counter];
        }
        [screenshotsArray addObject:@ {
            @"data" : screenshot,
            @"contentType" : @"image/png",
            @"filename" : filename,
        }];
        counter++;
    }

    resolve(screenshotsArray);
#else
    resolve(nil);
#endif
}

RCT_EXPORT_METHOD(
    fetchViewHierarchy : (RCTPromiseResolveBlock)resolve rejecter : (RCTPromiseRejectBlock)reject)
{
#if TARGET_OS_IPHONE || TARGET_OS_MACCATALYST
    NSData *rawViewHierarchy = [PrivateSentrySDKOnly captureViewHierarchy];

    NSMutableArray *viewHierarchy = [NSMutableArray arrayWithCapacity:rawViewHierarchy.length];
    const char *bytes = (char *)[rawViewHierarchy bytes];
    for (int i = 0; i < [rawViewHierarchy length]; i++) {
        [viewHierarchy addObject:[[NSNumber alloc] initWithChar:bytes[i]]];
    }

    resolve(viewHierarchy);
#else
    resolve(nil);
#endif
}

RCT_EXPORT_METHOD(setUser : (NSDictionary *)userKeys otherUserKeys : (NSDictionary *)userDataKeys)
{
    [SentrySDKWrapper configureScope:^(SentryScope *_Nonnull scope) {
        [scope setUser:[RNSentry userFrom:userKeys otherUserKeys:userDataKeys]];
    }];
}

+ (SentryUser *_Nullable)userFrom:(NSDictionary *)userKeys
                    otherUserKeys:(NSDictionary *)userDataKeys
{
    // we can safely ignore userDataKeys since if original JS user was null userKeys will be null
    if ([userKeys isKindOfClass:NSDictionary.class]) {
        SentryUser *userInstance = [[SentryUser alloc] initWithDictionary:userKeys];

        if ([userDataKeys isKindOfClass:NSDictionary.class]) {
            [userInstance setData:userDataKeys];
        }

        return userInstance;
    }

    if (![[NSNull null] isEqual:userKeys] && nil != userKeys) {
        NSLog(@"[RNSentry] Method setUser received unexpected type of userKeys.");
    }

    return nil;
}

RCT_EXPORT_METHOD(addBreadcrumb : (NSDictionary *)breadcrumb)
{
    [SentrySDKWrapper configureScope:^(SentryScope *_Nonnull scope) {
        [scope addBreadcrumb:[RNSentryBreadcrumb from:breadcrumb]];
    }];

#if SENTRY_HAS_UIKIT
    NSString *_Nullable screen = [RNSentryBreadcrumb getCurrentScreenFrom:breadcrumb];
    if (screen != nil) {
        [PrivateSentrySDKOnly setCurrentScreen:screen];
    }
#endif // SENTRY_HAS_UIKIT
}

RCT_EXPORT_METHOD(clearBreadcrumbs)
{
    [SentrySDKWrapper configureScope:^(SentryScope *_Nonnull scope) { [scope clearBreadcrumbs]; }];
}

RCT_EXPORT_METHOD(setExtra : (NSString *)key extra : (NSString *)extra)
{
    [SentrySDKWrapper
        configureScope:^(SentryScope *_Nonnull scope) { [scope setExtraValue:extra forKey:key]; }];
}

RCT_EXPORT_METHOD(setContext : (NSString *)key context : (NSDictionary *)context)
{
    if (key == nil) {
        return;
    }

    [SentrySDKWrapper configureScope:^(SentryScope *_Nonnull scope) {
        if (context == nil) {
            [scope removeContextForKey:key];
        } else {
            [scope setContextValue:context forKey:key];
        }
    }];
}

RCT_EXPORT_METHOD(setTag : (NSString *)key value : (NSString *)value)
{
    [SentrySDKWrapper
        configureScope:^(SentryScope *_Nonnull scope) { [scope setTagValue:value forKey:key]; }];
}

RCT_EXPORT_METHOD(setAttribute : (NSString *)key value : (NSString *)value)
{
    [SentrySDKWrapper configureScope:^(
        SentryScope *_Nonnull scope) { [scope setAttributeValue:value forKey:key]; }];
}

RCT_EXPORT_METHOD(setAttributes : (NSDictionary *)attributes)
{
    [SentrySDKWrapper configureScope:^(SentryScope *_Nonnull scope) {
        [attributes enumerateKeysAndObjectsUsingBlock:^(NSString *key, NSString *value,
            BOOL *stop) { [scope setAttributeValue:value forKey:key]; }];
    }];
}

RCT_EXPORT_METHOD(removeAttribute : (NSString *)key)
{
    [SentrySDKWrapper
        configureScope:^(SentryScope *_Nonnull scope) { [scope removeAttributeForKey:key]; }];
}

RCT_EXPORT_METHOD(crash) { [SentrySDKWrapper crash]; }

RCT_EXPORT_METHOD(
    closeNativeSdk : (RCTPromiseResolveBlock)resolve rejecter : (RCTPromiseRejectBlock)reject)
{
    [SentrySDKWrapper close];
    resolve(@YES);
}

RCT_EXPORT_METHOD(disableNativeFramesTracking)
{
    // Do nothing on iOS, this bridge method only has an effect on android.
}

RCT_EXPORT_METHOD(enableNativeFramesTracking)
{
    // Do nothing on iOS, this bridge method only has an effect on android.
    // If you're starting the Cocoa SDK manually,
    // you can set the 'enableAutoPerformanceTracing: true' option and
    // the 'tracesSampleRate' or 'tracesSampler' option.
}

RCT_EXPORT_METHOD(pauseAppHangTracking) { [SentrySDKWrapper pauseAppHangTracking]; }

RCT_EXPORT_METHOD(resumeAppHangTracking) { [SentrySDKWrapper resumeAppHangTracking]; }

/**
 * Calls captureReplay on the native replay integration and returns
 * the BOOL result indicating whether the capture succeeded.
 *
 * PrivateSentrySDKOnly.captureReplay is void and discards the result,
 * so we call the integration directly to get the success status.
 * This prevents returning a stale buffer-mode replay ID when the
 * capture actually failed (e.g., replay not running).
 *
 * Falls back to the old void captureReplay if the integration
 * cannot be accessed directly (e.g., future Cocoa SDK changes).
 *
 * See https://github.com/getsentry/sentry-react-native/issues/5074
 */
+ (BOOL)captureReplayWithReturnValue
{
#if SENTRY_TARGET_REPLAY_SUPPORTED
    @try {
        if ([PrivateSentrySDKOnly respondsToSelector:@selector(getReplayIntegration)]) {
#    pragma clang diagnostic push
#    pragma clang diagnostic ignored "-Warc-performSelector-leaks"
            id replayIntegration =
                [PrivateSentrySDKOnly performSelector:@selector(getReplayIntegration)];
#    pragma clang diagnostic pop
            if (replayIntegration &&
                [replayIntegration respondsToSelector:@selector(captureReplay)]) {
                typedef BOOL (*CaptureReplayIMP)(id, SEL);
                CaptureReplayIMP captureFunc = (CaptureReplayIMP)
                    [replayIntegration methodForSelector:@selector(captureReplay)];
                return captureFunc(replayIntegration, @selector(captureReplay));
            }
        }
    } @catch (NSException *exception) {
        NSLog(@"[RNSentry] Failed to call captureReplay on integration: %@", exception);
    }
    // Fallback: call the void method and assume success if a replay ID exists.
    // This preserves the old behavior when the integration isn't directly accessible.
    // clang-format off
    @try {
        [PrivateSentrySDKOnly captureReplay];
        return [PrivateSentrySDKOnly getReplayId] != nil;
    } @catch (NSException *exception) {
        NSLog(@"[RNSentry] Failed to call captureReplay fallback: %@", exception);
        return NO;
    }
    // clang-format on
#else
    return NO;
#endif
}

RCT_EXPORT_METHOD(captureReplay : (BOOL)isHardCrash resolver : (
    RCTPromiseResolveBlock)resolve rejecter : (RCTPromiseRejectBlock)reject)
{
#if SENTRY_TARGET_REPLAY_SUPPORTED
    BOOL captured = [RNSentry captureReplayWithReturnValue];
    if (captured) {
        resolve([PrivateSentrySDKOnly getReplayId]);
    } else {
        resolve(nil);
    }
#else
    resolve(nil);
#endif
}

#if TARGET_OS_IPHONE || TARGET_OS_MACCATALYST
static BOOL
RNSentryIsPathUnderAllowedRoots(NSString *path)
{
    if (path.length == 0) {
        return NO;
    }
    for (NSString *component in path.pathComponents) {
        if ([component isEqualToString:@".."]) {
            return NO;
        }
    }
    NSString *resolved = [path stringByResolvingSymlinksInPath];
    NSMutableArray<NSString *> *roots = [NSMutableArray arrayWithCapacity:3];
    NSString *tmp = NSTemporaryDirectory();
    if (tmp.length > 0) {
        [roots addObject:[tmp stringByResolvingSymlinksInPath]];
    }
    NSArray<NSString *> *caches
        = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES);
    if (caches.firstObject) {
        [roots addObject:[caches.firstObject stringByResolvingSymlinksInPath]];
    }
    NSArray<NSString *> *docs
        = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
    if (docs.firstObject) {
        [roots addObject:[docs.firstObject stringByResolvingSymlinksInPath]];
    }
    for (NSString *root in roots) {
        NSString *rootWithSlash =
            [root hasSuffix:@"/"] ? root : [root stringByAppendingString:@"/"];
        if ([resolved isEqualToString:root] || [resolved hasPrefix:rootWithSlash]) {
            return YES;
        }
    }
    return NO;
}

+ (BOOL)isPathUnderAllowedRootsForTesting:(NSString *)path
{
    return RNSentryIsPathUnderAllowedRoots(path);
}
#endif

RCT_EXPORT_METHOD(getDataFromUri : (NSString *_Nonnull)uri resolve : (
    RCTPromiseResolveBlock)resolve rejecter : (RCTPromiseRejectBlock)reject)
{
#if TARGET_OS_IPHONE || TARGET_OS_MACCATALYST
    NSURL *fileURL = [NSURL URLWithString:uri];
    if (![fileURL isFileURL]) {
        reject(@"SentryReactNative", @"The provided URI is not a valid file:// URL", nil);
        return;
    }
    if (!RNSentryIsPathUnderAllowedRoots(fileURL.path)) {
        reject(@"SentryReactNative",
            @"The provided URI points outside the app's temporary, caches, or documents "
            @"directories",
            nil);
        return;
    }
    NSError *error = nil;
    NSData *fileData = [NSData dataWithContentsOfURL:fileURL options:0 error:&error];
    if (error || !fileData) {
        reject(@"SentryReactNative", @"Failed to read file data", error);
        return;
    }
    NSMutableArray *byteArray = [NSMutableArray arrayWithCapacity:fileData.length];
    const unsigned char *bytes = (const unsigned char *)fileData.bytes;

    for (NSUInteger i = 0; i < fileData.length; i++) {
        [byteArray addObject:@(bytes[i])];
    }
    resolve(byteArray);
#else
    resolve(nil);
#endif
}

RCT_EXPORT_SYNCHRONOUS_TYPED_METHOD(NSString *, getCurrentReplayId)
{
#if SENTRY_TARGET_REPLAY_SUPPORTED
    return [PrivateSentrySDKOnly getReplayId];
#else
    return nil;
#endif
}

static NSString *const enabledProfilingMessage = @"Enable Hermes to use Sentry Profiling.";
static SentryId *nativeProfileTraceId = nil;
static uint64_t nativeProfileStartTime = 0;

RCT_EXPORT_SYNCHRONOUS_TYPED_METHOD(NSDictionary *, startProfiling : (BOOL)platformProfilers)
{
#if SENTRY_PROFILING_ENABLED
    try {
#    ifdef NEW_HERMES_RUNTIME
        auto *hermesAPI = facebook::jsi::castInterface<facebook::hermes::IHermesRootAPI>(
            facebook::hermes::makeHermesRootAPI());
        if (hermesAPI) {
            hermesAPI->enableSamplingProfiler();
        }
#    else
        facebook::hermes::HermesRuntime::enableSamplingProfiler();
#    endif
        if (nativeProfileTraceId == nil && nativeProfileStartTime == 0 && platformProfilers) {
#    if SENTRY_TARGET_PROFILING_SUPPORTED
            nativeProfileTraceId = [RNSentryId newId];
            nativeProfileStartTime =
                [PrivateSentrySDKOnly startProfilerForTrace:nativeProfileTraceId];
#    endif
        } else {
            if (!platformProfilers) {
                NSLog(@"Native profiling is disabled. Only starting Hermes profiling.");
            } else {
                NSLog(@"Native profiling already in progress. Currently existing trace: %@",
                    nativeProfileTraceId);
            }
        }
        return @{@"started" : @YES};
    } catch (const std::exception &ex) {
        if (nativeProfileTraceId != nil) {
#    if SENTRY_TARGET_PROFILING_SUPPORTED
            [PrivateSentrySDKOnly discardProfilerForTrace:nativeProfileTraceId];
#    endif
            nativeProfileTraceId = nil;
        }
        nativeProfileStartTime = 0;
        return @ {
            @"error" : [NSString stringWithCString:ex.what()
                                          encoding:[NSString defaultCStringEncoding]]
        };
    } catch (...) {
        if (nativeProfileTraceId != nil) {
#    if SENTRY_TARGET_PROFILING_SUPPORTED
            [PrivateSentrySDKOnly discardProfilerForTrace:nativeProfileTraceId];
#    endif
            nativeProfileTraceId = nil;
        }
        nativeProfileStartTime = 0;
        return @ { @"error" : @"Failed to start profiling" };
    }
#else
    return @ { @"error" : enabledProfilingMessage };
#endif
}

RCT_EXPORT_SYNCHRONOUS_TYPED_METHOD(NSDictionary *, stopProfiling)
{
#if SENTRY_PROFILING_ENABLED
    try {
        NSDictionary<NSString *, id> *nativeProfile = nil;
        if (nativeProfileTraceId != nil && nativeProfileStartTime != 0) {
#    if SENTRY_TARGET_PROFILING_SUPPORTED
            uint64_t nativeProfileStopTime = clock_gettime_nsec_np(CLOCK_UPTIME_RAW);
            nativeProfile = [PrivateSentrySDKOnly collectProfileBetween:nativeProfileStartTime
                                                                    and:nativeProfileStopTime
                                                               forTrace:nativeProfileTraceId];
#    endif
        }
        // Cleanup native profiles
        nativeProfileTraceId = nil;
        nativeProfileStartTime = 0;

        std::stringstream ss;
#    ifdef NEW_HERMES_RUNTIME
        auto *hermesAPI = facebook::jsi::castInterface<facebook::hermes::IHermesRootAPI>(
            facebook::hermes::makeHermesRootAPI());
        if (hermesAPI) {
            hermesAPI->disableSamplingProfiler();
            hermesAPI->dumpSampledTraceToStream(ss);
        }
#    else
        facebook::hermes::HermesRuntime::disableSamplingProfiler();
        // Before RN 0.69 Hermes used llvh::raw_ostream (profiling is supported for 0.69 and newer)
        facebook::hermes::HermesRuntime::dumpSampledTraceToStream(ss);
#    endif

        std::string s = ss.str();
        NSString *data = [NSString stringWithCString:s.c_str()
                                            encoding:[NSString defaultCStringEncoding]];

#    if SENTRY_PROFILING_DEBUG_ENABLED
        NSString *rawProfileFileName = @"hermes.profile";
        NSError *error = nil;
        NSString *rawProfileFilePath =
            [NSTemporaryDirectory() stringByAppendingPathComponent:rawProfileFileName];
        if (![data writeToFile:rawProfileFilePath
                    atomically:YES
                      encoding:NSUTF8StringEncoding
                         error:&error]) {
            NSLog(@"Error writing Raw Hermes Profile to %@: %@", rawProfileFilePath, error);
        } else {
            NSLog(@"Raw Hermes Profile saved to %@", rawProfileFilePath);
        }
#    endif

        if (data == nil) {
            return @ { @"error" : @"Failed to retrieve Hermes profile." };
        }

        if (nativeProfile == nil) {
            return @ { @"profile" : data };
        }

        return @ {
            @"profile" : data,
            @"nativeProfile" : nativeProfile,
        };
    } catch (const std::exception &ex) {
        if (nativeProfileTraceId != nil) {
#    if SENTRY_TARGET_PROFILING_SUPPORTED
            [PrivateSentrySDKOnly discardProfilerForTrace:nativeProfileTraceId];
#    endif
            nativeProfileTraceId = nil;
        }
        nativeProfileStartTime = 0;
        return @ {
            @"error" : [NSString stringWithCString:ex.what()
                                          encoding:[NSString defaultCStringEncoding]]
        };
    } catch (...) {
        if (nativeProfileTraceId != nil) {
#    if SENTRY_TARGET_PROFILING_SUPPORTED
            [PrivateSentrySDKOnly discardProfilerForTrace:nativeProfileTraceId];
#    endif
            nativeProfileTraceId = nil;
        }
        nativeProfileStartTime = 0;
        return @ { @"error" : @"Failed to stop profiling" };
    }
#else
    return @ { @"error" : enabledProfilingMessage };
#endif
}

RCT_EXPORT_METHOD(
    crashedLastRun : (RCTPromiseResolveBlock)resolve rejecter : (RCTPromiseRejectBlock)reject)
{
    resolve(@([SentrySDKWrapper crashedLastRun]));
}

// Thanks to this guard, we won't compile this code when we build for the old architecture.
#ifdef RCT_NEW_ARCH_ENABLED
- (std::shared_ptr<facebook::react::TurboModule>)getTurboModule:
    (const facebook::react::ObjCTurboModule::InitParams &)params
{
    return std::make_shared<facebook::react::NativeRNSentrySpecJSI>(params);
}
#endif

RCT_EXPORT_METHOD(getNewScreenTimeToDisplay : (RCTPromiseResolveBlock)resolve rejecter : (
    RCTPromiseRejectBlock)reject)
{
    [_timeToDisplay getTimeToDisplay:resolve];
}

RCT_EXPORT_METHOD(popTimeToDisplayFor : (NSString *)key resolver : (
    RCTPromiseResolveBlock)resolve rejecter : (RCTPromiseRejectBlock)reject)
{
    resolve([RNSentryTimeToDisplay popTimeToDisplayFor:key]);
}

RCT_EXPORT_SYNCHRONOUS_TYPED_METHOD(NSNumber *, setActiveSpanId : (NSString *)spanId)
{
    [RNSentryTimeToDisplay setActiveSpanId:spanId];
    return @YES; // The return ensures that the method is synchronous
}

RCT_EXPORT_METHOD(encodeToBase64 : (NSArray *)array resolver : (
    RCTPromiseResolveBlock)resolve rejecter : (RCTPromiseRejectBlock)reject)
{
    NSUInteger count = array.count;
    uint8_t *bytes = (uint8_t *)malloc(count);

    if (!bytes) {
        reject(@"encodeToBase64", @"Memory allocation failed", nil);
        return;
    }

    for (NSUInteger i = 0; i < count; i++) {
        bytes[i] = (uint8_t)[array[i] unsignedCharValue];
    }

    NSData *data = [NSData dataWithBytes:bytes length:count];
    free(bytes);

    NSString *base64String = [data base64EncodedStringWithOptions:0];
    resolve(base64String);
}

@end
