//!important: Intentionally leave out #import "ZSMNativeModule.h", note that it uses externs and should have no problem with the missing import
#import <React/RCTBridgeModule.h>
#import <ZSM/ZSM.h>
#import "generated.h"

@interface ZSMNativeModule : NSObject <RCTBridgeModule>
@property (nonatomic, strong) FIDO2Client *client;
@property (nonatomic, strong) UMFAClient *umfaClient;
- (NSString *)generateTraceId;
- (NSString *)formatTraceMessage:(NSString *)traceId message:(NSString *)message;
@end

@implementation ZSMNativeModule

- (NSString *)generateTraceId {
    return [[NSUUID UUID] UUIDString];
}

- (NSString *)formatTraceMessage:(NSString *)traceId message:(NSString *)message {
    return [NSString stringWithFormat:@"[TID:%@] %@", traceId, message];
}

// Safely serialize SDK data to JSON string, then parse back to get clean objects
// Returns nil if data is corrupted or cannot be serialized
- (NSDictionary *)safeCopyFromSDK:(NSDictionary *)dict {
    if (!dict) return nil;
    
    // Immediately try to serialize to JSON - this is the safest way to detect corruption
    @try {
        NSError *serializeError = nil;
        NSData *jsonData = [NSJSONSerialization dataWithJSONObject:dict options:0 error:&serializeError];
        if (!jsonData || serializeError) {
            return nil; // Data cannot be serialized - treat as corrupted
        }
        
        NSError *parseError = nil;
        NSDictionary *result = [NSJSONSerialization JSONObjectWithData:jsonData options:NSJSONReadingMutableContainers error:&parseError];
        if (!result || parseError) {
            return nil; // Parse failed - treat as corrupted
        }
        
        return result;
    } @catch (NSException *exception) {
        return nil; // Any exception means corrupted data
    }
}

// Store pending promise resolvers and rejecters to avoid block capture corruption
static NSMutableDictionary<NSString *, RCTPromiseResolveBlock> *_pendingResolvers;
static NSMutableDictionary<NSString *, RCTPromiseRejectBlock> *_pendingRejecters;
static NSMutableDictionary<NSString *, NSDictionary *> *_pendingResults;
static dispatch_queue_t _resolverQueue;
// Flag to track if we're currently inside an SDK callback
static BOOL _insideSDKCallback;

+ (void)initialize {
    if (self == [ZSMNativeModule class]) {
        _pendingResolvers = [NSMutableDictionary new];
        _pendingRejecters = [NSMutableDictionary new];
        _pendingResults = [NSMutableDictionary new];
        _resolverQueue = dispatch_queue_create("com.zsm.resolver", DISPATCH_QUEUE_SERIAL);
        _insideSDKCallback = NO;
    }
}

// Generate a unique promise ID
- (NSString *)generatePromiseId {
    return [[NSUUID UUID] UUIDString];
}

// Store resolver AND rejecter before SDK call (BEFORE any SDK operations)
- (NSString *)storeResolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject {
    NSString *promiseId = [self generatePromiseId];
    RCTPromiseResolveBlock copiedResolver = [resolve copy];
    RCTPromiseRejectBlock copiedRejecter = [reject copy];
    
    dispatch_sync(_resolverQueue, ^{
        _pendingResolvers[promiseId] = copiedResolver;
        if (copiedRejecter) {
            _pendingRejecters[promiseId] = copiedRejecter;
        }
    });
    
    return promiseId;
}

// Resolve the stored promise later (called from main queue, after SDK context exits)
// CRITICAL FIX: Call resolver IMMEDIATELY but with CFRetain to prevent premature deallocation
- (void)resolvePromise:(NSString *)promiseId withResult:(NSDictionary *)result {
    // Get resolver SYNCHRONOUSLY and remove from storage IMMEDIATELY
    // This prevents any race conditions with the resolver being freed
    NSString *safePromiseId = [promiseId copy];
    
    __block RCTPromiseResolveBlock resolver = nil;
    dispatch_sync(_resolverQueue, ^{
        resolver = _pendingResolvers[safePromiseId];
        // DON'T remove yet - keep it retained in the dictionary until after we call it
    });
    
    if (!resolver) {
        NSLog(@"[ZSM] No resolver found for promiseId: %@", safePromiseId);
        return;
    }
    
    // Create a completely fresh copy of the result using JSON round-trip
    NSDictionary *freshResult = @{@"result": @{}, @"metadata": @{}};
    @try {
        NSData *resultData = [NSJSONSerialization dataWithJSONObject:result options:0 error:nil];
        if (resultData) {
            NSDictionary *parsed = [NSJSONSerialization JSONObjectWithData:resultData options:NSJSONReadingMutableContainers error:nil];
            if (parsed) {
                freshResult = [parsed copy]; // Immutable copy
            }
        }
    } @catch (NSException *e) {
        NSLog(@"[ZSM] Exception serializing result: %@", e);
    }
    
    // Explicitly retain the resolver block to prevent deallocation during call
    RCTPromiseResolveBlock strongResolver = [resolver copy];
    
    // NOW remove from storage (after we have our strong reference)
    dispatch_sync(_resolverQueue, ^{
        [_pendingResolvers removeObjectForKey:safePromiseId];
        [_pendingRejecters removeObjectForKey:safePromiseId];
    });
    
    // Use performSelector to schedule on the NEXT run loop iteration
    // This is different from dispatch_async - it goes through NSRunLoop which is what React Native uses
    NSDictionary *callInfo = @{
        @"resolver": strongResolver,
        @"result": freshResult
    };
    [self performSelectorOnMainThread:@selector(invokeResolverWithInfo:) withObject:callInfo waitUntilDone:NO];
}

// Helper method to invoke resolver - called via performSelector for run loop isolation
- (void)invokeResolverWithInfo:(NSDictionary *)info {
    @autoreleasepool {
        RCTPromiseResolveBlock resolver = info[@"resolver"];
        NSDictionary *result = info[@"result"];
        
        if (!resolver) {
            NSLog(@"[ZSM] Resolver was nil in invokeResolverWithInfo");
            return;
        }
        
        @try {
            resolver(result);
        } @catch (NSException *exception) {
            NSLog(@"[ZSM] Exception calling resolver: %@ - %@", exception.name, exception.reason);
        }
    }
}

// Reject the stored promise later (called from main queue, after SDK context exits)
// CRITICAL FIX: Call rejecter via performSelector for run loop isolation
- (void)rejectPromise:(NSString *)promiseId 
             withCode:(NSString *)code 
              message:(NSString *)message 
                error:(NSError *)error {
    NSString *safePromiseId = [promiseId copy];
    
    __block RCTPromiseRejectBlock rejecter = nil;
    dispatch_sync(_resolverQueue, ^{
        rejecter = _pendingRejecters[safePromiseId];
        // DON'T remove yet - keep it retained in the dictionary until after we call it
    });
    
    if (!rejecter) {
        NSLog(@"[ZSM] No rejecter found for promiseId: %@", safePromiseId);
        return;
    }
    
    // Copy all values
    NSString *safeCode = [code copy] ?: @"UNKNOWN_ERROR";
    NSString *safeMessage = [message copy] ?: @"Unknown error";
    
    // Create a fresh error with serialized userInfo
    NSError *freshError = nil;
    @try {
        NSDictionary *errorInfo = error.userInfo ?: @{};
        NSData *errorData = [NSJSONSerialization dataWithJSONObject:errorInfo options:0 error:nil];
        NSDictionary *freshErrorInfo = @{};
        if (errorData) {
            NSDictionary *parsed = [NSJSONSerialization JSONObjectWithData:errorData options:0 error:nil];
            if (parsed) {
                freshErrorInfo = [parsed copy];
            }
        }
        freshError = [NSError errorWithDomain:error.domain ?: @"ZSMError" code:error.code userInfo:freshErrorInfo];
    } @catch (NSException *e) {
        freshError = [NSError errorWithDomain:@"ZSMError" code:-1 userInfo:@{NSLocalizedDescriptionKey: safeMessage}];
    }
    
    // Explicitly retain the rejecter block
    RCTPromiseRejectBlock strongRejecter = [rejecter copy];
    
    // NOW remove from storage
    dispatch_sync(_resolverQueue, ^{
        [_pendingResolvers removeObjectForKey:safePromiseId];
        [_pendingRejecters removeObjectForKey:safePromiseId];
    });
    
    // Use performSelector to schedule on the NEXT run loop iteration
    NSDictionary *callInfo = @{
        @"rejecter": strongRejecter,
        @"code": safeCode,
        @"message": safeMessage,
        @"error": freshError
    };
    [self performSelectorOnMainThread:@selector(invokeRejecterWithInfo:) withObject:callInfo waitUntilDone:NO];
}

// Helper method to invoke rejecter - called via performSelector for run loop isolation
- (void)invokeRejecterWithInfo:(NSDictionary *)info {
    @autoreleasepool {
        RCTPromiseRejectBlock rejecter = info[@"rejecter"];
        NSString *code = info[@"code"];
        NSString *message = info[@"message"];
        NSError *error = info[@"error"];
        
        if (!rejecter) {
            NSLog(@"[ZSM] Rejecter was nil in invokeRejecterWithInfo");
            return;
        }
        
        @try {
            rejecter(code, message, error);
        } @catch (NSException *exception) {
            NSLog(@"[ZSM] Exception calling rejecter: %@ - %@", exception.name, exception.reason);
        }
    }
}

// Simple safe resolve - serialize final result through JSON to ensure React Native compatibility
// CRITICAL: Uses stored resolver pattern to avoid block capture in corrupted SDK context
- (void)safeResolve:(RCTPromiseResolveBlock)resolve credentials:(NSDictionary *)credentials metadata:(NSDictionary *)metadata promiseId:(NSString *)promiseId {
    // Convert to JSON string IMMEDIATELY while still in callback context
    // This is the safest way to extract data from potentially corrupted SDK objects
    NSString *credentialsJson = nil;
    NSString *metadataJson = nil;
    
    @try {
        if (credentials) {
            NSData *credData = [NSJSONSerialization dataWithJSONObject:credentials options:0 error:nil];
            if (credData) {
                credentialsJson = [[NSString alloc] initWithData:credData encoding:NSUTF8StringEncoding];
            }
        }
        if (metadata) {
            NSData *metaData = [NSJSONSerialization dataWithJSONObject:metadata options:0 error:nil];
            if (metaData) {
                metadataJson = [[NSString alloc] initWithData:metaData encoding:NSUTF8StringEncoding];
            }
        }
    } @catch (NSException *exception) {
        // If serialization crashes, we'll use nil values
    }
    
    // Copy strings to ensure they're fully owned and not referencing SDK memory
    NSString *safeCreds = credentialsJson ? [credentialsJson copy] : nil;
    NSString *safeMeta = metadataJson ? [metadataJson copy] : nil;
    NSString *safePromiseId = [promiseId copy];
    
    // Build result dictionary synchronously (no dispatch needed - resolvePromise handles that)
    NSMutableDictionary *result = [[NSMutableDictionary alloc] init];
    
    // Parse credentials back from JSON string
    if (safeCreds) {
        NSData *data = [safeCreds dataUsingEncoding:NSUTF8StringEncoding];
        if (data) {
            NSDictionary *parsed = [NSJSONSerialization JSONObjectWithData:data options:0 error:nil];
            result[@"result"] = parsed ?: @{};
        } else {
            result[@"result"] = @{};
        }
    } else {
        result[@"result"] = @{};
    }
    
    // Parse metadata back from JSON string  
    if (safeMeta) {
        NSData *data = [safeMeta dataUsingEncoding:NSUTF8StringEncoding];
        if (data) {
            NSDictionary *parsed = [NSJSONSerialization JSONObjectWithData:data options:0 error:nil];
            result[@"metadata"] = parsed ?: @{};
        } else {
            result[@"metadata"] = @{};
        }
    } else {
        result[@"metadata"] = @{};
    }
    
    // Resolve using stored resolver - this handles the delay and isolation
    [self resolvePromise:safePromiseId withResult:result];
}

RCT_EXPORT_MODULE(ZSM);

//TODO: use the same return types in the rust(wasm), kotlin, and ios here, rather than handling in the js client

// Method to return the version string
RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD(getVersionString) {
    return [NSString stringWithUTF8String:ZSM_VERSION];
}

RCT_EXPORT_METHOD(create:(NSDictionary *)config
                resolver:(RCTPromiseResolveBlock)resolve
                rejecter:(RCTPromiseRejectBlock)reject) {
    
    // Inject React Native SDK version into X-SDK-Version header via config headers
    NSMutableDictionary *enrichedConfig = [config mutableCopy];
    NSString *rnVersion = [NSString stringWithUTF8String:ZSM_VERSION];
    NSMutableDictionary *headers = [enrichedConfig[@"headers"] mutableCopy] ?: [NSMutableDictionary new];
    headers[@"X-SDK-Version"] = rnVersion;
    enrichedConfig[@"headers"] = headers;

    ZSMConfig *zsmConfig = [[ZSMConfig alloc] initWithJSON:enrichedConfig];
    if (!zsmConfig) {
        reject(@"ios::create", @"Invalid config", nil);
        return;
    }
    FIDO2Client *client = [[FIDO2Client alloc] initWithConfig:zsmConfig];
    if (!client) {
        reject(@"ios::create", @"Failed to create client", nil);

    } else {
        self.client = client;
        // Also initialize UMFAClient for listRegisteredUsers and getCredentialState
        self.umfaClient = [[UMFAClient alloc] initWithConfig:zsmConfig relyingParty:nil];
        resolve(@{@"success": @YES});
    }
}

// Method to configure the ZSM instance
RCT_EXPORT_METHOD(configure:(NSDictionary *)config
                   resolver:(RCTPromiseResolveBlock)resolve
                   rejecter:(RCTPromiseRejectBlock)reject) {
    if (!self.client) {
        reject(@"ios::configure", @"Client is not initialized", nil);
        return;
    }

    // Ensure X-SDK-Version header persists across reconfigures
    NSMutableDictionary *enrichedConfig = [config mutableCopy];
    NSMutableDictionary *headers = [enrichedConfig[@"headers"] mutableCopy] ?: [NSMutableDictionary new];
    if (!headers[@"X-SDK-Version"]) {
        headers[@"X-SDK-Version"] = [NSString stringWithUTF8String:ZSM_VERSION];
        enrichedConfig[@"headers"] = headers;
    }

    ZSMConfig *zsmConfig = [[ZSMConfig alloc] initWithJSON:enrichedConfig];
    if (!zsmConfig) {
        reject(@"ios::configure", @"Invalid config", nil);
        return;
    }
    // Configure the FIDO2Client
    [self.client configure:zsmConfig];
    // Also configure UMFAClient to ensure all client instances have the updated config
    // This is critical because UMFAClient creates its own internal FIDO2Client which
    // registers in the ZSMInstanceRegistry. If we only configure self.client, the
    // internal FIDO2Client in UMFAClient will have stale config (missing consumerId).
    if (self.umfaClient) {
        [self.umfaClient configure:zsmConfig];
    }
    resolve(@{@"success": @YES});
}

// WebAuthn Create
RCT_EXPORT_METHOD(webauthn_create:(NSDictionary *)options
                          traceId:(nullable NSString *)traceId
                         resolver:(RCTPromiseResolveBlock)resolve
                         rejecter:(RCTPromiseRejectBlock)reject) {
    if (!self.client) {
        reject(@"ios::webauthn_create", @"Client is not initialized", nil);
        return;
    }

    // Copy blocks to ensure they survive until the async callback fires
    RCTPromiseResolveBlock safeResolve = [resolve copy];
    RCTPromiseRejectBlock safeReject = [reject copy];

    NSString *actualTraceId = [traceId copy] ?: [self generateTraceId];
    [ZSMClient logWithLevel:LogLevelTrace message:[self formatTraceMessage:actualTraceId message:@"webauthn_create called"]];

    [self.client webauthnCreate:options completion:^(NSDictionary<NSString *, id> * _Nullable credentials, NSDictionary<NSString *, NSString *> * _Nullable metadata, ZSMError * _Nullable error) {
        if (error) {
            [ZSMClient logWithLevel:LogLevelError message:[self formatTraceMessage:actualTraceId message:[NSString stringWithFormat:@"webauthn_create error: %@", error.localizedDescription]]];
            
            // Create rich NSError with all ZSMError properties for JavaScript
            NSError *richError = [NSError errorWithDomain:@"ZSMError" 
                                                     code:error.code 
                                                 userInfo:@{
                                                     @"message": error.message ?: @"Unknown error",
                                                     @"code": @(error.code),
                                                     @"traceId": error.traceId ?: @"",
                                                     @"details": error.description ?: @""
                                                 }];
            
            // Use structured error code and message for consistency with Android
            safeReject([NSString stringWithFormat:@"ZSM_%ld", (long)error.code], 
                   [NSString stringWithFormat:@"[ZSM %ld] %@ (Trace: %@)", (long)error.code, error.message ?: @"Unknown error", error.traceId ?: @""], 
                   richError);
        } else {
            [ZSMClient logWithLevel:LogLevelTrace message:[self formatTraceMessage:actualTraceId message:@"webauthn_create completed successfully"]];
            safeResolve(@{
                @"result": credentials ?: [NSNull null],  // Use NSNull for nil credentials
                @"metadata": metadata ?: [NSNull null]   // Use NSNull for nil metadata
            });
        }
    }];
}

// WebAuthn Get
RCT_EXPORT_METHOD(webauthn_get:(NSDictionary *)options
                       traceId:(nullable NSString *)traceId
                      resolver:(RCTPromiseResolveBlock)resolve
                      rejecter:(RCTPromiseRejectBlock)reject) {
    if (!self.client) {
        reject(@"ios::webauthn_get", @"Client is not initialized", nil);
        return;
    }

    // Copy blocks to ensure they survive until the async callback fires
    RCTPromiseResolveBlock safeResolve = [resolve copy];
    RCTPromiseRejectBlock safeReject = [reject copy];

    NSString *actualTraceId = [traceId copy] ?: [self generateTraceId];
    [ZSMClient logWithLevel:LogLevelTrace message:[self formatTraceMessage:actualTraceId message:@"webauthn_get called"]];

    [self.client webauthnGet:options completion:^(NSDictionary<NSString *, id> * _Nullable credentials, NSDictionary<NSString *, NSString *> * _Nullable metadata, ZSMError * _Nullable error) {
        if (error) {
            [ZSMClient logWithLevel:LogLevelError message:[self formatTraceMessage:actualTraceId message:[NSString stringWithFormat:@"webauthn_get error: %@", error.localizedDescription]]];
            
            // Create rich NSError with all ZSMError properties for JavaScript
            NSError *richError = [NSError errorWithDomain:@"ZSMError" 
                                                     code:error.code 
                                                 userInfo:@{
                                                     @"message": error.message ?: @"Unknown error",
                                                     @"code": @(error.code),
                                                     @"traceId": error.traceId ?: @"",
                                                     @"details": error.description ?: @""
                                                 }];
            
            // Use structured error code and message for consistency with Android
            safeReject([NSString stringWithFormat:@"ZSM_%ld", (long)error.code], 
                   [NSString stringWithFormat:@"[ZSM %ld] %@ (Trace: %@)", (long)error.code, error.message ?: @"Unknown error", error.traceId ?: @""], 
                   richError);
        } else {
            [ZSMClient logWithLevel:LogLevelTrace message:[self formatTraceMessage:actualTraceId message:@"webauthn_get completed successfully"]];
            safeResolve(@{
                @"result": credentials ?: [NSNull null],  // Use NSNull for nil credentials
                @"metadata": metadata ?: [NSNull null]   // Use NSNull for nil metadata
            });
        }
    }];
}

// WebAuthn Retrieve
RCT_EXPORT_METHOD(webauthn_retrieve:(NSString *)identityId
                           traceId:(nullable NSString *)traceId
                           resolver:(RCTPromiseResolveBlock)resolve
                           rejecter:(RCTPromiseRejectBlock)reject) {

    if (!self.client) {
        reject(@"ios::webauthn_retrieve", @"Client is not initialized", nil);
        return;
    }

    // CRITICAL: Store the resolver AND rejecter BEFORE any SDK operations
    // This ensures they are captured in a clean memory context
    NSString *promiseId = [self storeResolver:resolve rejecter:reject];

    // Make strong copies of all strings that will be captured by blocks to prevent use-after-free
    NSString *actualTraceId = [traceId copy] ?: [self generateTraceId];
    NSString *capturedIdentityId = [identityId copy];
    [ZSMClient logWithLevel:LogLevelTrace message:[self formatTraceMessage:actualTraceId message:[NSString stringWithFormat:@"webauthn_retrieve called with identityId: %@", capturedIdentityId]]];

    // Store the identity mapping for future use (implements the missing lookup functionality)
    NSString *currentConsumerId = [self.client.config.consumerId copy];
    [ZSMClient logWithLevel:LogLevelTrace message:[self formatTraceMessage:actualTraceId message:[NSString stringWithFormat:@"[IDENTITY-MAPPING] Current consumerId: %@", currentConsumerId]]];
    [ZSMClient logWithLevel:LogLevelTrace message:[self formatTraceMessage:actualTraceId message:[NSString stringWithFormat:@"[IDENTITY-MAPPING] identityId parameter: %@", capturedIdentityId]]];
    
    if (capturedIdentityId && ![capturedIdentityId isEqualToString:currentConsumerId]) {
        [ZSMClient logWithLevel:LogLevelTrace message:[self formatTraceMessage:actualTraceId message:[NSString stringWithFormat:@"[IDENTITY-MAPPING] About to store mapping: userId='%@' -> identityId='%@'", currentConsumerId, capturedIdentityId]]];
        [self storeIdentityMapping:capturedIdentityId identityId:currentConsumerId];
        [ZSMClient logWithLevel:LogLevelTrace message:[self formatTraceMessage:actualTraceId message:[NSString stringWithFormat:@"[IDENTITY-MAPPING] ✅ Stored identity mapping: userId='%@' -> identityId='%@'", currentConsumerId, capturedIdentityId]]];
        
        // Verify the storage immediately
        NSString *verifyLookup = [self lookupIdentityId:currentConsumerId];
        [ZSMClient logWithLevel:LogLevelTrace message:[self formatTraceMessage:actualTraceId message:[NSString stringWithFormat:@"[IDENTITY-MAPPING] ✓ Verification lookup for userId='%@' returned: '%@'", currentConsumerId, verifyLookup]]];
    } else {
        [ZSMClient logWithLevel:LogLevelTrace message:[self formatTraceMessage:actualTraceId message:[NSString stringWithFormat:@"[IDENTITY-MAPPING] ⚠️ NOT storing mapping - identityId:'%@' same as consumerId:'%@' or identityId is nil", capturedIdentityId, currentConsumerId]]];
    }

    // Try to retrieve using the current consumerId first (original userId for storage access)
    [ZSMClient logWithLevel:LogLevelTrace message:[self formatTraceMessage:actualTraceId message:@"Trying webauthnRetrieve with original consumerId for storage access"]];
    [ZSMClient logWithLevel:LogLevelTrace message:[self formatTraceMessage:actualTraceId message:[NSString stringWithFormat:@"Current consumerId (for storage): %@", currentConsumerId]]];
    [ZSMClient logWithLevel:LogLevelTrace message:[self formatTraceMessage:actualTraceId message:[NSString stringWithFormat:@"identityId (for future MPC): %@", capturedIdentityId]]];
    
    [self.client webauthnRetrieve:^(NSDictionary<NSString *, id> * _Nullable credentials, NSDictionary<NSString *, NSString *> * _Nullable metadata, ZSMError * _Nullable error) {
        // IMMEDIATELY serialize SDK data via JSON to create completely isolated copies
        // This happens before ANY other operations to prevent accessing corrupted memory
        NSDictionary *safeCredentials = [self safeCopyFromSDK:credentials];
        NSDictionary *safeMetadata = [self safeCopyFromSDK:metadata];
        
        // Capture promiseId for use throughout this callback
        NSString *capturedPromiseId = [promiseId copy];
        
        [ZSMClient logWithLevel:LogLevelTrace message:[self formatTraceMessage:actualTraceId message:[NSString stringWithFormat:@"webauthnRetrieve callback - hasCredentials: %@, error: %@", safeCredentials ? @"YES" : @"NO", error]]];
        
        if (safeCredentials) {
            [ZSMClient logWithLevel:LogLevelTrace message:[self formatTraceMessage:actualTraceId message:[NSString stringWithFormat:@"Found credential in storage for consumerId: %@", currentConsumerId]]];
        } else if (!error) {
            // No credentials found with current consumerId, try looking up the identityId as a userId
            NSString *lookupIdentityId = [self lookupIdentityId:capturedIdentityId];
            if (lookupIdentityId && ![lookupIdentityId isEqualToString:capturedIdentityId] && ![lookupIdentityId isEqualToString:currentConsumerId]) {
                [ZSMClient logWithLevel:LogLevelTrace message:[self formatTraceMessage:actualTraceId message:[NSString stringWithFormat:@"No credential found for consumerId: %@, trying with mapped identityId: %@", currentConsumerId, lookupIdentityId]]];
                
                // Copy for inner block capture
                NSString *capturedLookupId = [lookupIdentityId copy];
                
                // Try with the mapped identity ID
                [self.client webauthnRetrieveForUser:capturedLookupId withCompletion:^(NSDictionary<NSString *, id> * _Nullable altCredentials, NSDictionary<NSString *, NSString *> * _Nullable altMetadata, ZSMError * _Nullable altError) {
                    // IMMEDIATELY serialize SDK data via JSON
                    NSDictionary *safeAltCredentials = [self safeCopyFromSDK:altCredentials];
                    NSDictionary *safeAltMetadata = [self safeCopyFromSDK:altMetadata];
                    
                    if (safeAltCredentials) {
                        [ZSMClient logWithLevel:LogLevelTrace message:[self formatTraceMessage:actualTraceId message:[NSString stringWithFormat:@"Found credential using mapped identityId: %@", capturedLookupId]]];
                    } else {
                        [ZSMClient logWithLevel:LogLevelTrace message:[self formatTraceMessage:actualTraceId message:[NSString stringWithFormat:@"No credential found for mapped identityId: %@", capturedLookupId]]];
                    }
                    [self safeResolve:nil credentials:safeAltCredentials metadata:safeAltMetadata promiseId:capturedPromiseId];
                }];
                return;
            } else {
                [ZSMClient logWithLevel:LogLevelTrace message:[self formatTraceMessage:actualTraceId message:[NSString stringWithFormat:@"No credential found in storage for consumerId: %@", currentConsumerId]]];
            }
        }
        
        if (error) {
            // Wrap all error property access in try-catch since error object could be corrupted
            // Extract all values IMMEDIATELY and copy to local strings
            NSString *errorMessage = nil;
            NSInteger errorCode = 0;
            NSString *errorTraceId = nil;
            NSString *errorDescription = nil;
            
            @try {
                errorMessage = [error.message copy] ?: @"Unknown error";
                errorCode = error.code;
                errorTraceId = [error.traceId copy] ?: @"";
                errorDescription = [error.description copy] ?: @"";
                NSString *localizedDescription = error.localizedDescription ?: @"Unknown error";
                [ZSMClient logWithLevel:LogLevelError message:[self formatTraceMessage:actualTraceId message:[NSString stringWithFormat:@"webauthn_retrieve error: %@", localizedDescription]]];
            } @catch (NSException *exception) {
                errorMessage = @"Unknown error";
                errorCode = -1;
                errorTraceId = @"";
                errorDescription = @"Error details could not be retrieved";
            }
            
            // Build error details with safe copies
            NSString *capturedErrorMessage = [errorMessage copy];
            NSInteger capturedErrorCode = errorCode;
            NSString *capturedErrorTraceId = [errorTraceId copy];
            NSString *capturedErrorDescription = [errorDescription copy];
            
            // Create error code and message
            NSString *errorCodeStr = [NSString stringWithFormat:@"ZSM_%ld", (long)capturedErrorCode];
            NSString *errorFullMessage = [NSString stringWithFormat:@"[ZSM %ld] %@ (Trace: %@)", (long)capturedErrorCode, capturedErrorMessage, capturedErrorTraceId];
            
            // Create rich NSError
            NSError *richError = [NSError errorWithDomain:@"ZSMError" 
                                                     code:capturedErrorCode 
                                                 userInfo:@{
                                                     @"message": capturedErrorMessage,
                                                     @"code": @(capturedErrorCode),
                                                     @"traceId": capturedErrorTraceId,
                                                     @"details": capturedErrorDescription
                                                 }];
            
            // Use stored rejecter pattern
            [self rejectPromise:capturedPromiseId withCode:errorCodeStr message:errorFullMessage error:richError];
        } else {
            [ZSMClient logWithLevel:LogLevelTrace message:[self formatTraceMessage:actualTraceId message:@"webauthn_retrieve completed successfully"]];
            [self safeResolve:nil credentials:safeCredentials metadata:safeMetadata promiseId:capturedPromiseId];
        }
    }];
}


RCT_EXPORT_METHOD(unenroll:(NSString *)userId
                   traceId:(nullable NSString *)traceId
                   resolver:(RCTPromiseResolveBlock)resolve
                   rejecter:(RCTPromiseRejectBlock)reject) {

    if (!self.client) {
        reject(@"ios::unenroll", @"UMFA client is not initialized", nil);
        return;
    }

    // Copy blocks to ensure they survive until the async callback fires
    RCTPromiseResolveBlock safeResolve = [resolve copy];
    RCTPromiseRejectBlock safeReject = [reject copy];

    NSString *actualTraceId = [traceId copy] ?: [self generateTraceId];
    NSString *capturedUserId = [userId copy];
    [ZSMClient logWithLevel:LogLevelTrace message:[self formatTraceMessage:actualTraceId message:[NSString stringWithFormat:@"unenroll called with userId: %@", capturedUserId]]];

    [ZSMClient logWithLevel:LogLevelTrace message:[self formatTraceMessage:actualTraceId message:@"Calling UMFAClient.unenroll"]];

    // CALL unbind on fido2 client
    [self.client unbindForUser:capturedUserId withCompletion:^(BOOL success, NSDictionary<NSString *, NSString *> * _Nullable metadata, ZSMError * _Nullable error) {
        [ZSMClient logWithLevel:LogLevelTrace message:[self formatTraceMessage:actualTraceId message:[NSString stringWithFormat:@"unenroll callback - success: %@, error: %@", success ? @"YES" : @"NO", error]]];
        
        if (error) {
            [ZSMClient logWithLevel:LogLevelError message:[self formatTraceMessage:actualTraceId message:[NSString stringWithFormat:@"unenroll error: %@", error.localizedDescription]]];
            safeReject(@"unenroll_error", error.localizedDescription, nil);
        } else {
            [ZSMClient logWithLevel:LogLevelTrace message:[self formatTraceMessage:actualTraceId message:@"unenroll completed successfully"]];
            safeResolve(@{
                @"success": @(success),
                @"metadata": metadata ?: [NSNull null]
            });
        }
    }];
}

RCT_EXPORT_METHOD(listRegisteredUsers:(nullable NSString *)traceId
                             resolver:(RCTPromiseResolveBlock)resolve
                             rejecter:(RCTPromiseRejectBlock)reject) {
    if (!self.umfaClient) {
        reject(@"ios::listRegisteredUsers", @"UMFA client is not initialized", nil);
        return;
    }

    NSString *actualTraceId = traceId ?: [self generateTraceId];
    [ZSMClient logWithLevel:LogLevelTrace message:[self formatTraceMessage:actualTraceId message:@"listRegisteredUsers called"]];

    NSArray<NSString *> *users = [self.umfaClient listRegisteredUsers];
    [ZSMClient logWithLevel:LogLevelTrace message:[self formatTraceMessage:actualTraceId message:[NSString stringWithFormat:@"listRegisteredUsers returned %lu users", (unsigned long)users.count]]];
    
    resolve(users ?: @[]);
}

RCT_EXPORT_METHOD(getCredentialState:(NSString *)userId
                             traceId:(nullable NSString *)traceId
                            resolver:(RCTPromiseResolveBlock)resolve
                            rejecter:(RCTPromiseRejectBlock)reject) {
    if (!self.umfaClient) {
        reject(@"ios::getCredentialState", @"UMFA client is not initialized", nil);
        return;
    }

    NSString *actualTraceId = traceId ?: [self generateTraceId];
    [ZSMClient logWithLevel:LogLevelTrace message:[self formatTraceMessage:actualTraceId message:[NSString stringWithFormat:@"getCredentialState called with userId: %@", userId]]];

    NSInteger state = [self.umfaClient getCredentialState:userId];
    
    NSString *stateString;
    switch (state) {
        case 3: stateString = @"both"; break;
        case 2: stateString = @"passkey-only"; break;
        case 1: stateString = @"mpc-only"; break;
        default: stateString = @"none"; break;
    }
    
    [ZSMClient logWithLevel:LogLevelTrace message:[self formatTraceMessage:actualTraceId message:[NSString stringWithFormat:@"getCredentialState result: %@ (%ld)", stateString, (long)state]]];
    
    resolve(@{
        @"state": @(state),
        @"stateString": stateString
    });
}

RCT_EXPORT_METHOD(isPasskeySupported:(RCTPromiseResolveBlock)resolve
                            rejecter:(RCTPromiseRejectBlock)reject) {
    if (!self.umfaClient) {
        reject(@"ios::isPasskeySupported", @"UMFA client is not initialized", nil);
        return;
    }
    
    BOOL supported = [self.umfaClient isPasskeySupported];
    resolve(@(supported));
}

RCT_EXPORT_METHOD(passkeyNotSupportedReason:(RCTPromiseResolveBlock)resolve
                                   rejecter:(RCTPromiseRejectBlock)reject) {
    if (!self.umfaClient) {
        reject(@"ios::passkeyNotSupportedReason", @"UMFA client is not initialized", nil);
        return;
    }
    
    NSString *reason = [self.umfaClient passkeyNotSupportedReason];
    resolve(reason ?: [NSNull null]);
}

RCT_EXPORT_METHOD(isUserEnrolled:(NSString *)userId
                         traceId:(nullable NSString *)traceId
                        resolver:(RCTPromiseResolveBlock)resolve
                        rejecter:(RCTPromiseRejectBlock)reject) {
    if (!self.umfaClient) {
        reject(@"ios::isUserEnrolled", @"UMFA client is not initialized", nil);
        return;
    }

    NSString *actualTraceId = traceId ?: [self generateTraceId];
    [ZSMClient logWithLevel:LogLevelTrace message:[self formatTraceMessage:actualTraceId message:[NSString stringWithFormat:@"isUserEnrolled called with userId: %@", userId]]];

    BOOL enrolled = [self.umfaClient isUserEnrolled:userId];
    [ZSMClient logWithLevel:LogLevelTrace message:[self formatTraceMessage:actualTraceId message:[NSString stringWithFormat:@"isUserEnrolled result: %@", enrolled ? @"YES" : @"NO"]]];
    
    resolve(@(enrolled));
}

RCT_EXPORT_METHOD(getServerCredentialState:(NSString *)userId
                                   traceId:(nullable NSString *)traceId
                                  resolver:(RCTPromiseResolveBlock)resolve
                                  rejecter:(RCTPromiseRejectBlock)reject) {
    if (!self.umfaClient) {
        reject(@"ios::getServerCredentialState", @"UMFA client is not initialized", nil);
        return;
    }

    // Copy blocks to ensure they survive until the async callback fires
    RCTPromiseResolveBlock safeResolve = [resolve copy];
    RCTPromiseRejectBlock safeReject = [reject copy];

    NSString *actualTraceId = [traceId copy] ?: [self generateTraceId];
    NSString *capturedUserId = [userId copy];
    [ZSMClient logWithLevel:LogLevelTrace message:[self formatTraceMessage:actualTraceId message:[NSString stringWithFormat:@"getServerCredentialState called with userId: %@", capturedUserId]]];

    [self.umfaClient getServerCredentialState:capturedUserId completion:^(NSInteger state, ZSMError * _Nullable error) {
        if (error) {
            [ZSMClient logWithLevel:LogLevelError message:[self formatTraceMessage:actualTraceId message:[NSString stringWithFormat:@"getServerCredentialState error: %@", error.localizedDescription]]];
            safeReject(@"server_credential_state_error", error.localizedDescription, nil);
        } else {
            NSString *stateString;
            switch (state) {
                case 3: stateString = @"both"; break;
                case 2: stateString = @"passkey-only"; break;
                case 1: stateString = @"mpc-only"; break;
                default: stateString = @"none"; break;
            }
            [ZSMClient logWithLevel:LogLevelTrace message:[self formatTraceMessage:actualTraceId message:[NSString stringWithFormat:@"getServerCredentialState result: %@ (%ld)", stateString, (long)state]]];
            safeResolve(@{
                @"state": @(state),
                @"stateString": stateString
            });
        }
    }];
}

RCT_EXPORT_METHOD(addPasskeyToExistingIdentity:(NSString *)userId
                              userVerification:(NSString *)userVerification
                                       traceId:(nullable NSString *)traceId
                                      resolver:(RCTPromiseResolveBlock)resolve
                                      rejecter:(RCTPromiseRejectBlock)reject) {
    if (!self.umfaClient) {
        reject(@"ios::addPasskeyToExistingIdentity", @"UMFA client is not initialized", nil);
        return;
    }

    // Copy blocks to ensure they survive until the async callback fires
    RCTPromiseResolveBlock safeResolve = [resolve copy];
    RCTPromiseRejectBlock safeReject = [reject copy];

    NSString *actualTraceId = [traceId copy] ?: [self generateTraceId];
    NSString *capturedUserId = [userId copy];
    NSString *capturedUserVerification = [userVerification copy];
    [ZSMClient logWithLevel:LogLevelTrace message:[self formatTraceMessage:actualTraceId message:[NSString stringWithFormat:@"addPasskeyToExistingIdentity called with userId: %@, userVerification: %@", capturedUserId, capturedUserVerification]]];

    // Use the public enroll:userVerification:completion: method which internally detects
    // MPC_ONLY state and adds passkey (same as native Swift UMFA app).
    // addPasskeyToExistingIdentity:userVerification:completion: is private in UMFAClient.
    [self.umfaClient enroll:capturedUserId userVerification:capturedUserVerification completion:^(NSDictionary<NSString *, id> * _Nullable credentials, NSDictionary<NSString *, NSString *> * _Nullable metadata, ZSMError * _Nullable error) {
        if (error) {
            [ZSMClient logWithLevel:LogLevelError message:[self formatTraceMessage:actualTraceId message:[NSString stringWithFormat:@"addPasskeyToExistingIdentity error: %@", error.localizedDescription]]];
            safeReject(@"add_passkey_error", error.localizedDescription, nil);
        } else {
            [ZSMClient logWithLevel:LogLevelTrace message:[self formatTraceMessage:actualTraceId message:@"addPasskeyToExistingIdentity success"]];
            safeResolve(@{
                @"result": credentials ?: [NSNull null],
                @"metadata": metadata ?: [NSNull null]
            });
        }
    }];
}

RCT_EXPORT_METHOD(addMpcToPasskeyUser:(NSString *)userId
                              traceId:(nullable NSString *)traceId
                             resolver:(RCTPromiseResolveBlock)resolve
                             rejecter:(RCTPromiseRejectBlock)reject) {
    if (!self.umfaClient) {
        reject(@"ios::addMpcToPasskeyUser", @"UMFA client is not initialized", nil);
        return;
    }

    // Copy blocks to ensure they survive until the async callback fires
    RCTPromiseResolveBlock safeResolve = [resolve copy];
    RCTPromiseRejectBlock safeReject = [reject copy];

    NSString *actualTraceId = [traceId copy] ?: [self generateTraceId];
    NSString *capturedUserId = [userId copy];
    [ZSMClient logWithLevel:LogLevelTrace message:[self formatTraceMessage:actualTraceId message:[NSString stringWithFormat:@"addMpcToPasskeyUser called with userId: %@", capturedUserId]]];

    [self.umfaClient addMpcToPasskeyUser:capturedUserId completion:^(NSDictionary<NSString *, id> * _Nullable credentials, NSDictionary<NSString *, NSString *> * _Nullable metadata, ZSMError * _Nullable error) {
        if (error) {
            [ZSMClient logWithLevel:LogLevelError message:[self formatTraceMessage:actualTraceId message:[NSString stringWithFormat:@"addMpcToPasskeyUser error: %@", error.localizedDescription]]];
            safeReject(@"add_mpc_error", error.localizedDescription, nil);
        } else {
            [ZSMClient logWithLevel:LogLevelTrace message:[self formatTraceMessage:actualTraceId message:@"addMpcToPasskeyUser success"]];
            safeResolve(@{
                @"result": credentials ?: [NSNull null],
                @"metadata": metadata ?: [NSNull null]
            });
        }
    }];
}

#pragma mark - UMFA High-Level Methods (matching native SDK)

// Enroll user with userVerification support (matches UMFAClient.enroll)
RCT_EXPORT_METHOD(umfaEnroll:(NSString *)userId
                userVerification:(NSString *)userVerification
                         traceId:(nullable NSString *)traceId
                        resolver:(RCTPromiseResolveBlock)resolve
                        rejecter:(RCTPromiseRejectBlock)reject) {
    if (!self.umfaClient) {
        reject(@"ios::umfaEnroll", @"UMFA client is not initialized", nil);
        return;
    }

    // Copy blocks to ensure they survive until the async callback fires
    RCTPromiseResolveBlock safeResolve = [resolve copy];
    RCTPromiseRejectBlock safeReject = [reject copy];

    NSString *actualTraceId = [traceId copy] ?: [self generateTraceId];
    NSString *capturedUserId = [userId copy];
    NSString *capturedUserVerification = [userVerification copy] ?: @"prevented";
    
    [ZSMClient logWithLevel:LogLevelTrace message:[self formatTraceMessage:actualTraceId message:[NSString stringWithFormat:@"umfaEnroll called with userId: %@, userVerification: %@", capturedUserId, capturedUserVerification]]];

    [self.umfaClient enroll:capturedUserId userVerification:capturedUserVerification completion:^(NSDictionary<NSString *, id> * _Nullable credentials, NSDictionary<NSString *, NSString *> * _Nullable metadata, ZSMError * _Nullable error) {
        if (error) {
            [ZSMClient logWithLevel:LogLevelError message:[self formatTraceMessage:actualTraceId message:[NSString stringWithFormat:@"umfaEnroll error: %@", error.localizedDescription]]];
            
            // Create rich NSError with all ZSMError properties
            NSError *richError = [NSError errorWithDomain:@"ZSMError" 
                                                     code:error.code 
                                                 userInfo:@{
                                                     @"message": error.message ?: @"Unknown error",
                                                     @"code": @(error.code),
                                                     @"traceId": error.traceId ?: @"",
                                                     @"details": error.description ?: @""
                                                 }];
            
            safeReject([NSString stringWithFormat:@"ZSM_%ld", (long)error.code], 
                   [NSString stringWithFormat:@"[ZSM %ld] %@ (Trace: %@)", (long)error.code, error.message ?: @"Unknown error", error.traceId ?: @""], 
                   richError);
        } else {
            [ZSMClient logWithLevel:LogLevelTrace message:[self formatTraceMessage:actualTraceId message:@"umfaEnroll completed successfully"]];
            safeResolve(@{
                @"result": credentials ?: [NSNull null],
                @"metadata": metadata ?: [NSNull null]
            });
        }
    }];
}

// Authenticate user with userVerification support (matches UMFAClient.authenticate)
RCT_EXPORT_METHOD(umfaAuthenticate:(NSString *)userId
                  userVerification:(NSString *)userVerification
                           traceId:(nullable NSString *)traceId
                          resolver:(RCTPromiseResolveBlock)resolve
                          rejecter:(RCTPromiseRejectBlock)reject) {
    if (!self.umfaClient) {
        reject(@"ios::umfaAuthenticate", @"UMFA client is not initialized", nil);
        return;
    }

    // Copy blocks to ensure they survive until the async callback fires
    RCTPromiseResolveBlock safeResolve = [resolve copy];
    RCTPromiseRejectBlock safeReject = [reject copy];

    NSString *actualTraceId = [traceId copy] ?: [self generateTraceId];
    NSString *capturedUserId = [userId copy];
    NSString *capturedUserVerification = [userVerification copy] ?: @"preferred";
    
    [ZSMClient logWithLevel:LogLevelTrace message:[self formatTraceMessage:actualTraceId message:[NSString stringWithFormat:@"umfaAuthenticate called with userId: %@, userVerification: %@", capturedUserId, capturedUserVerification]]];

    [self.umfaClient authenticate:capturedUserId userVerification:capturedUserVerification completion:^(NSDictionary<NSString *, id> * _Nullable credentials, NSDictionary<NSString *, NSString *> * _Nullable metadata, ZSMError * _Nullable error) {
        if (error) {
            [ZSMClient logWithLevel:LogLevelError message:[self formatTraceMessage:actualTraceId message:[NSString stringWithFormat:@"umfaAuthenticate error: %@", error.localizedDescription]]];
            
            // Create rich NSError with all ZSMError properties
            NSError *richError = [NSError errorWithDomain:@"ZSMError" 
                                                     code:error.code 
                                                 userInfo:@{
                                                     @"message": error.message ?: @"Unknown error",
                                                     @"code": @(error.code),
                                                     @"traceId": error.traceId ?: @"",
                                                     @"details": error.description ?: @""
                                                 }];
            
            safeReject([NSString stringWithFormat:@"ZSM_%ld", (long)error.code], 
                   [NSString stringWithFormat:@"[ZSM %ld] %@ (Trace: %@)", (long)error.code, error.message ?: @"Unknown error", error.traceId ?: @""], 
                   richError);
        } else {
            [ZSMClient logWithLevel:LogLevelTrace message:[self formatTraceMessage:actualTraceId message:@"umfaAuthenticate completed successfully"]];
            safeResolve(@{
                @"result": credentials ?: [NSNull null],
                @"metadata": metadata ?: [NSNull null]
            });
        }
    }];
}

// Check enrollment status via server (matches UMFAClient.checkEnrollment)
RCT_EXPORT_METHOD(checkEnrollment:(NSString *)userId
                          traceId:(nullable NSString *)traceId
                         resolver:(RCTPromiseResolveBlock)resolve
                         rejecter:(RCTPromiseRejectBlock)reject) {
    if (!self.umfaClient) {
        reject(@"ios::checkEnrollment", @"UMFA client is not initialized", nil);
        return;
    }

    // Copy blocks to ensure they survive until the async callback fires
    RCTPromiseResolveBlock safeResolve = [resolve copy];
    RCTPromiseRejectBlock safeReject = [reject copy];

    NSString *actualTraceId = [traceId copy] ?: [self generateTraceId];
    NSString *capturedUserId = [userId copy];
    
    [ZSMClient logWithLevel:LogLevelTrace message:[self formatTraceMessage:actualTraceId message:[NSString stringWithFormat:@"checkEnrollment called with userId: %@", capturedUserId]]];

    [self.umfaClient checkEnrollment:capturedUserId completion:^(NSDictionary<NSString *, id> * _Nullable enrollmentInfo, NSDictionary<NSString *, NSString *> * _Nullable metadata, ZSMError * _Nullable error) {
        if (error) {
            [ZSMClient logWithLevel:LogLevelError message:[self formatTraceMessage:actualTraceId message:[NSString stringWithFormat:@"checkEnrollment error: %@", error.localizedDescription]]];
            // For checkEnrollment, return null instead of rejecting (like web SDK)
            safeResolve([NSNull null]);
        } else {
            [ZSMClient logWithLevel:LogLevelTrace message:[self formatTraceMessage:actualTraceId message:[NSString stringWithFormat:@"checkEnrollment result: %@", enrollmentInfo]]];
            safeResolve(enrollmentInfo ?: [NSNull null]);
        }
    }];
}

// Check if user has passkey credential (local check)
RCT_EXPORT_METHOD(hasPasskeyCredential:(NSString *)userId
                              resolver:(RCTPromiseResolveBlock)resolve
                              rejecter:(RCTPromiseRejectBlock)reject) {
    if (!self.umfaClient) {
        reject(@"ios::hasPasskeyCredential", @"UMFA client is not initialized", nil);
        return;
    }

    NSString *capturedUserId = [userId copy];
    [ZSMClient logWithLevel:LogLevelTrace message:[NSString stringWithFormat:@"hasPasskeyCredential called with userId: %@", capturedUserId]];

    // Check credential state - state 2 (passkey-only) or 3 (both) means has passkey
    NSInteger state = [self.umfaClient getCredentialState:capturedUserId];
    BOOL hasPasskey = (state == 2 || state == 3);
    
    [ZSMClient logWithLevel:LogLevelTrace message:[NSString stringWithFormat:@"hasPasskeyCredential result: %@ (state: %ld)", hasPasskey ? @"YES" : @"NO", (long)state]];
    
    resolve(@(hasPasskey));
}

// Check all enrollments info (matches UMFAClient.checkAllEnrollments)
RCT_EXPORT_METHOD(checkAllEnrollments:(NSString *)userId
                             resolver:(RCTPromiseResolveBlock)resolve
                             rejecter:(RCTPromiseRejectBlock)reject) {
    if (!self.umfaClient) {
        reject(@"ios::checkAllEnrollments", @"UMFA client is not initialized", nil);
        return;
    }

    NSString *capturedUserId = [userId copy];
    [ZSMClient logWithLevel:LogLevelTrace message:[NSString stringWithFormat:@"checkAllEnrollments called with userId: %@", capturedUserId]];

    NSDictionary *enrollmentInfo = [self.umfaClient checkAllEnrollments:capturedUserId];
    
    [ZSMClient logWithLevel:LogLevelTrace message:[NSString stringWithFormat:@"checkAllEnrollments localInfo: %@", enrollmentInfo]];
    
    resolve(enrollmentInfo ?: [NSNull null]);
}

// Browser-aligned enrollment lookup with optional remote enrichment.
RCT_EXPORT_METHOD(checkAllEnrollmentsWithRemoteCheck:(NSString *)userId
                  forceRemoteCheck:(BOOL)forceRemoteCheck
                  resolver:(RCTPromiseResolveBlock)resolve
                  rejecter:(RCTPromiseRejectBlock)reject) {
    if (!self.umfaClient) {
        reject(@"ios::checkAllEnrollmentsWithRemoteCheck", @"UMFA client is not initialized", nil);
        return;
    }

    NSString *capturedUserId = [userId copy];
    [ZSMClient logWithLevel:LogLevelTrace message:[NSString stringWithFormat:@"checkAllEnrollmentsWithRemoteCheck called with userId: %@, forceRemoteCheck: %@", capturedUserId, forceRemoteCheck ? @"YES" : @"NO"]];

    SEL remoteSelector = NSSelectorFromString(@"checkAllEnrollments:forceRemoteCheck:completion:");
    if ([self.umfaClient respondsToSelector:remoteSelector]) {
        typedef void (*CheckAllEnrollmentsWithRemoteCheckIMP)(id, SEL, NSString *, BOOL, ZSMJSONCompletion);
        CheckAllEnrollmentsWithRemoteCheckIMP remoteMethod = (CheckAllEnrollmentsWithRemoteCheckIMP)[self.umfaClient methodForSelector:remoteSelector];

        remoteMethod(self.umfaClient, remoteSelector, capturedUserId, forceRemoteCheck, ^(NSDictionary<NSString *,id> * _Nullable data, NSDictionary<NSString *,NSString *> * _Nullable metadata, ZSMError * _Nullable error) {
            if (error) {
                reject(@"ios::checkAllEnrollmentsWithRemoteCheck", error.message, error);
                return;
            }

            [ZSMClient logWithLevel:LogLevelTrace message:[NSString stringWithFormat:@"checkAllEnrollments localInfo: %@", data]];

            // Log server state from raw server fields if exposed by the framework,
            // otherwise derive from known server-specific fields in the merged result.
            NSString *identityId = data[@"identity_id"] ?: data[@"identityId"];
            id hasMpcRemote = data[@"hasMpcRemote"];
            id hasPasskeyRemote = data[@"hasPasskeyRemote"];
            id hasRcr = data[@"hasRcr"];
            if (hasMpcRemote || hasPasskeyRemote || identityId) {
                [ZSMClient logWithLevel:LogLevelTrace message:[NSString stringWithFormat:
                    @"checkAllEnrollments serverState={hasMpcCredential=%@, hasPasskeyCredential=%@, identityId=%@, hasRcr=%@}",
                    hasMpcRemote ?: @"<nil>", hasPasskeyRemote ?: @"<nil>",
                    identityId ?: @"<nil>", hasRcr ?: @"<nil>"]];
            } else if (metadata && metadata.count > 0) {
                [ZSMClient logWithLevel:LogLevelTrace message:[NSString stringWithFormat:@"checkAllEnrollments serverState (metadata): %@", metadata]];
            } else {
                [ZSMClient logWithLevel:LogLevelTrace message:@"checkAllEnrollments serverState: not exposed by iOS framework — see native UMFAClient logs"];
            }

            [ZSMClient logWithLevel:LogLevelTrace message:[NSString stringWithFormat:@"checkAllEnrollments mergedInfo: %@", data]];

            resolve(data ?: [NSNull null]);
        });
        return;
    }

    [ZSMClient logWithLevel:LogLevelWarn message:[NSString stringWithFormat:@"checkAllEnrollmentsWithRemoteCheck selector unavailable in bundled iOS SDK; falling back to local checkAllEnrollments for userId: %@", capturedUserId]];
    NSDictionary *enrollmentInfo = [self.umfaClient checkAllEnrollments:capturedUserId];
    [ZSMClient logWithLevel:LogLevelTrace message:[NSString stringWithFormat:@"checkAllEnrollments mergedInfo (fallback): %@", enrollmentInfo]];
    resolve(enrollmentInfo ?: [NSNull null]);
}

#pragma mark - Identity Mapping Storage

- (void)storeIdentityMapping:(NSString *)userId identityId:(NSString *)identityId {
    [ZSMClient logWithLevel:LogLevelTrace message:[NSString stringWithFormat:@"[STORE] storeIdentityMapping called with userId='%@', identityId='%@'", userId, identityId]];
    
    if (!userId || !identityId) {
        [ZSMClient logWithLevel:LogLevelError message:[NSString stringWithFormat:@"[STORE] ❌ Cannot store identity mapping with nil values: userId=%@, identityId=%@", userId, identityId]];
        return;
    }
    
    NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
    NSString *key = [NSString stringWithFormat:@"zsm_identity_mapping_%@", userId];
    [ZSMClient logWithLevel:LogLevelTrace message:[NSString stringWithFormat:@"[STORE] Using storage key: '%@'", key]];
    
    [defaults setObject:identityId forKey:key];
    [defaults synchronize];
    
    // Verify storage worked immediately
    NSString *storedValue = [defaults stringForKey:key];
    [ZSMClient logWithLevel:LogLevelTrace message:[NSString stringWithFormat:@"[STORE] ✅ Stored and verified identity mapping: userId='%@' -> identityId='%@' (verified: '%@')", userId, identityId, storedValue]];
}

- (NSString *)lookupIdentityId:(NSString *)userId {
    [ZSMClient logWithLevel:LogLevelTrace message:[NSString stringWithFormat:@"[LOOKUP] lookupIdentityId called with userId='%@'", userId]];
    
    if (!userId) {
        [ZSMClient logWithLevel:LogLevelError message:@"[LOOKUP] ❌ Cannot lookup identity mapping with nil userId"];
        return userId;
    }
    
    NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
    NSString *key = [NSString stringWithFormat:@"zsm_identity_mapping_%@", userId];
    [ZSMClient logWithLevel:LogLevelTrace message:[NSString stringWithFormat:@"[LOOKUP] Looking up storage key: '%@'", key]];
    
    NSString *identityId = [defaults stringForKey:key];
    
    if (identityId) {
        [ZSMClient logWithLevel:LogLevelTrace message:[NSString stringWithFormat:@"[LOOKUP] ✅ Found identity mapping: userId='%@' -> identityId='%@'", userId, identityId]];
        return identityId;
    } else {
        [ZSMClient logWithLevel:LogLevelTrace message:[NSString stringWithFormat:@"[LOOKUP] ⚠️ No identity mapping found for userId='%@', returning original userId", userId]];
        return userId;
    }
}


@end
