#import "Knotapi.h"
#import <React/RCTConvert.h>
#import "KnotAPI/KnotAPI-Swift.h"
#import <WebKit/WebKit.h>

/**
 * Knotapi - React Native bridge for Knot API integration
 *
 * This class provides a bridge between React Native JavaScript and the native Knot API.
 * It handles all communication with the Knot SDK and ensures safe data transmission
 * to prevent crashes from nil values or invalid data types.
 */
@interface Knotapi () <KnotEventDelegate>
/** Current product name being used (transaction_link, link, or card_switcher) */
@property (nonatomic, strong) NSString* productName;

/** Flag indicating if the React Native bridge is active and ready to send events */
@property (nonatomic, assign) BOOL isBridgeActive;

/** Flag indicating if the bridge is currently in the process of closing */
@property (nonatomic, assign) BOOL isClosing;
@end

/**
 * Private helper methods for safe data handling and event management
 *
 * These methods centralize common operations to avoid code duplication and ensure
 * consistent safety checks across all event-handling methods.
 */
@interface Knotapi (PrivateHelpers)
/** Check if the bridge is ready to send events (active and not closing) */
- (BOOL)isBridgeReadyForEvents;

/** Create a safe dictionary by filtering out nil and NSNull values */
- (NSDictionary *)createSafeDictionaryFromDictionary:(NSDictionary *)dictionary;

/** Safely add a string value to a dictionary if it's valid */
- (void)addSafeStringValue:(NSString *)value forKey:(NSString *)key toDictionary:(NSMutableDictionary *)dictionary;

/** Safely add metadata to a dictionary with nested safety checks */
- (void)addSafeMetaData:(NSDictionary *)metaData forKey:(NSString *)key toDictionary:(NSMutableDictionary *)dictionary;

/** Send an event with automatic safety validation of the body dictionary */
- (void)sendSafeEventWithName:(NSString *)eventName body:(NSDictionary *)body;
@end

@implementation Knotapi

RCT_EXPORT_MODULE()

/**
 * Initialize the Knotapi bridge
 *
 * Sets up the initial state with bridge active and not closing.
 * This ensures the bridge is ready to handle events immediately after initialization.
 */
- (instancetype)init {
    self = [super init];
    if (self) {
        // Initialize bridge state - ready to handle events
        _isBridgeActive = YES;
        _isClosing = NO;
    }
    return self;
}

/**
 * Get the current Knot SDK version
 *
 * @return NSString* The SDK version string, or NSNull if unavailable
 */
RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD(getSdkVersion) {
    NSString *version = [Knot SDKVersion];
    return version ?: @"Unknown";
}

/**
 * Close the Knot API and deactivate the bridge
 *
 * This method safely closes the Knot SDK and marks the bridge as inactive.
 * It prevents duplicate close calls and ensures proper cleanup.
 */
RCT_EXPORT_METHOD(close){
    // Prevent multiple close calls to avoid race conditions
    if (self.isClosing) {
        NSLog(@"KnotAPI close: Already closing, ignoring duplicate call");
        return;
    }

    // Mark as closing to prevent new events
    self.isClosing = YES;

    // Perform close operation on main queue to ensure thread safety
    dispatch_async(dispatch_get_main_queue(), ^{
        @try {
            [Knot close];
        } @catch (NSException *exception) {
            NSLog(@"KnotAPI close error: %@", exception.reason);
        } @finally {
            // Always mark bridge as inactive after close attempt
            self.isBridgeActive = NO;
            // Reset closing flag to allow future operations
            self.isClosing = NO;
        }
    });
}

/**
 * Open the Knot API with configuration parameters
 *
 * @param params NSDictionary containing configuration parameters:
 *   - sessionId: Required string for API session
 *   - clientId: Required string for client identification
 *   - environment: Optional string ("sandbox", "development", or "production")
 *   - merchantIds: Optional array of merchant IDs
 *   - customerConfig: Optional dictionary with customer configuration
 *   - useCategories: Optional boolean for category usage
 *   - useSearch: Optional boolean for search functionality
 */
RCT_EXPORT_METHOD(open:(NSDictionary *)params){
  dispatch_async(dispatch_get_main_queue(), ^{
      // Reset bridge state for new session
      self.isBridgeActive = YES;
      self.isClosing = NO;
      self.productName = nil;
      NSLog(@"Reset bridge state for new session");

      // Validate input parameters
      if (!params || ![params isKindOfClass:[NSDictionary class]]) {
          NSLog(@"KnotAPI open: Invalid or nil params dictionary");
          return;
      }
      // Extract and validate sessionId (required)
      id sessionIdObj = params[@"sessionId"];
      NSString *sessionId = nil;
      if (sessionIdObj && ![sessionIdObj isKindOfClass:[NSNull class]] && [sessionIdObj isKindOfClass:[NSString class]]) {
          sessionId = (NSString *)sessionIdObj;
      }

      // Extract and validate clientId (required)
      id clientIdObj = params[@"clientId"];
      NSString *clientId = nil;
      if (clientIdObj && ![clientIdObj isKindOfClass:[NSNull class]] && [clientIdObj isKindOfClass:[NSString class]]) {
          clientId = (NSString *)clientIdObj;
      }

      // Extract and validate merchantIds array (optional)
      id merchantIdsObj = params[@"merchantIds"];
      NSArray<NSNumber *> *merchantIds = nil;
      if ([merchantIdsObj isKindOfClass:[NSArray class]]) {
          NSMutableArray<NSNumber *> *tmp = [NSMutableArray array];
          for (id v in (NSArray *)merchantIdsObj) {
              if ([v isKindOfClass:[NSNumber class]]) { [tmp addObject:v]; }
          }
          merchantIds = tmp.count ? [tmp copy] : nil;
      }

      // Extract and validate environment string (optional)
      id environmentObj = params[@"environment"];
      NSString *environmentString = nil;
      if (environmentObj && ![environmentObj isKindOfClass:[NSNull class]] && [environmentObj isKindOfClass:[NSString class]]) {
          environmentString = (NSString *)environmentObj;
      }

      // Extract and validate entryPoint string (optional)
      id entryPointObj = params[@"entryPoint"];
      NSString *entryPoint = nil;
      if (entryPointObj && ![entryPointObj isKindOfClass:[NSNull class]] && [entryPointObj isKindOfClass:[NSString class]]) {
          entryPoint = (NSString *)entryPointObj;
      }

      // Validate required parameters before proceeding
      if (sessionId.length == 0 || clientId.length == 0) {
          NSLog(@"KnotAPI open: Missing required parameters (sessionId or clientId)");
          [self sendEventWithName:@"knot:onError"
                             body:@{ @"errorCode": @"INVALID_PARAMS",
                                     @"errorDescription": @"Missing required parameters (sessionId or clientId)" }];
          return;
      }

      // Extract boolean flags with default values
      BOOL useCategories = NO; // default value
      id useCategoriesValue = [params objectForKey:@"useCategories"];
      if (useCategoriesValue && ![useCategoriesValue isKindOfClass:[NSNull class]]) {
          useCategories = [useCategoriesValue boolValue];
      }
      BOOL useSearch = true; // default value is true
      id useSearchValue = [params objectForKey:@"useSearch"];
      if (useSearchValue && ![useSearchValue isKindOfClass:[NSNull class]]) {
          useSearch = [useSearchValue boolValue];
      }

    // Extract customer configuration (optional)
    NSDictionary *dict = [params objectForKey:@"customerConfig"];
    NSString *cardName = nil;
    NSString *customerName = nil;
    NSString *logoId = nil;

    if (dict && ![dict isKindOfClass:[NSNull class]] && [dict isKindOfClass:[NSDictionary class]]) {
        // Extract card name from customer config
        id cardNameObj = dict[@"cardName"];
        if (cardNameObj && ![cardNameObj isKindOfClass:[NSNull class]] && [cardNameObj isKindOfClass:[NSString class]]) {
            cardName = (NSString *)cardNameObj;
        }
        // Extract customer name from customer config
        id customerNameObj = dict[@"customerName"];
        if (customerNameObj && ![customerNameObj isKindOfClass:[NSNull class]] && [customerNameObj isKindOfClass:[NSString class]]) {
            customerName = (NSString *)customerNameObj;
        }
        // Extract logo ID from customer config
        id logoIdObj = dict[@"logoId"];
        if (logoIdObj && ![logoIdObj isKindOfClass:[NSNull class]] && [logoIdObj isKindOfClass:[NSString class]]) {
            logoId = (NSString *)logoIdObj;
        }
    }

    // Create customer configuration object if any customer data is provided
    CustomerConfiguration *customerConfig = nil;
    if (cardName || customerName || logoId) {
      customerConfig = [[CustomerConfiguration alloc] initWithCardName:cardName
                                                          customerName:customerName
                                                                logoId:logoId];
    }

    // Determine environment setting (defaults to production)
    Environment environment = EnvironmentProduction;
    if (environmentString && [environmentString isEqualToString:@"sandbox"]) {
      environment = EnvironmentSandbox;
    } else if (environmentString && [environmentString isEqualToString:@"development"]) {
      environment = EnvironmentDevelopment;
    }

    // Create Knot configuration with all extracted parameters
    KnotConfiguration *knotConfiguration = [[KnotConfiguration alloc]
        initWithSessionId:sessionId clientId:clientId environment:environment entryPoint:entryPoint useCategories:useCategories useSearch:useSearch merchantIds:merchantIds customerConfiguration: customerConfig
    ];

    // Open Knot API with the configuration and set this class as the delegate
    [Knot openWithConfiguration:knotConfiguration delegate:self];

  });
}

/**
 * Define the events that this module can send to React Native
 *
 * @return NSArray<NSString *> Array of supported event names
 */
- (NSArray<NSString *> *)supportedEvents
{
    // Define the original event names
    NSArray *originalEvents = @[@"onSuccess", @"onError", @"onEvent", @"onExit"];

    // Map original events to namespaced event names for better organization
    NSDictionary *eventMapping = @{
        @"onSuccess" : @"knot:onSuccess",
        @"onError" : @"knot:onError",
        @"onEvent" : @"knot:onEvent",
        @"onExit" : @"knot:onExit"
    };

    // Convert original events to namespaced events
    NSMutableArray *mappedEvents = [NSMutableArray array];
    for (NSString *event in originalEvents) {
        NSString *newEvent = eventMapping[event] ?: event; // Fallback to original if mapping not found
        [mappedEvents addObject:newEvent];
    }

    return [mappedEvents copy];
}

/**
 * Handle error events from the Knot API
 *
 * This delegate method is called when the Knot API encounters an error.
 * It converts the error enum to human-readable strings and sends them to React Native.
 *
 * @param error The error type from the Knot API
 */
- (void)onErrorWithError:(enum KnotError)error {
  // Check if bridge is ready before processing the error
  if (![self isBridgeReadyForEvents]) {
    NSLog(@"KnotAPI onError: Bridge inactive or closing, ignoring event");
    return;
  }

  // Convert error enum to human-readable strings
  NSString *errorDescription;
  NSString *errorCode;
  switch (error) {
      case KnotErrorInvalidSession:
        errorCode = @"Invalid_Session";
        errorDescription = @"The session is invalid.";
        break;
      case KnotErrorInvalidCardName:
        errorCode = @"INVALID_CARD_NAME";
        errorDescription = @"The card name is invalid.";
        break;
      case KnotErrorInvalidCustomerName:
        errorCode = @"INVALID_CUSTOMER_NAME";
        errorDescription = @"The customer name is invalid.";
        break;
      case KnotErrorInvalidLogoId:
        errorCode = @"INVALID_LOGO_ID";
        errorDescription = @"The logo ID is invalid.";
        break;
      case KnotErrorExpiredSession:
        errorCode = @"Expired_Session";
        errorDescription = @"The session has expired.";
        break;
      case KnotErrorInvalidClientId:
        errorCode = @"Invalid_Client_Id";
        errorDescription = @"The client ID is invalid.";
        break;
      case KnotErrorInternalError:
        errorCode = @"Internal_Error";
        errorDescription = @"An internal error occurred.";
        break;
    case KnotErrorMerchantIdNotFound:
      errorCode = @"Merchant_Id_Not_Found";
      errorDescription = @"The merchant ID is required.";
      break;
  }

  // Build error event body with safe string handling
  NSMutableDictionary *body = [NSMutableDictionary dictionaryWithCapacity:3];
  [self addSafeStringValue:errorCode forKey:@"errorCode" toDictionary:body];
  [self addSafeStringValue:errorDescription forKey:@"errorDescription" toDictionary:body];
  [self addSafeStringValue:self.productName forKey:@"product" toDictionary:body];

  // Send error event to React Native
  [self sendSafeEventWithName:@"knot:onError" body:body];
}

/**
 * Handle general events from the Knot API
 *
 * This delegate method is called for various events during the Knot API lifecycle.
 * It extracts event data and safely sends it to React Native.
 *
 * @param event The event object containing event details
 */
- (void)onEventWithEvent:(KnotEvent * _Nonnull)event {
  // Check if bridge is ready before processing the event
  if (![self isBridgeReadyForEvents]) {
    NSLog(@"KnotAPI onEvent: Bridge inactive or closing, ignoring event");
    return;
  }

  // Additional nil check for safety
  if (!event) {
    NSLog(@"KnotAPI onEvent: Event is nil, ignoring");
    return;
  }

  // Convert environment enum to string representation
  Environment environment = event.environment;
  NSString *environmentString = @"";
  if (environment == EnvironmentSandbox) {
    environmentString = @"sandbox";
  } else if (environment == EnvironmentDevelopment) {
    environmentString = @"development";
  } else {
    environmentString = @"production";
  }

  // Build event body with safe data handling
  NSMutableDictionary *body = [NSMutableDictionary dictionaryWithCapacity:6];

  [self addSafeStringValue:event.event forKey:@"event" toDictionary:body];
  [self addSafeStringValue:environmentString forKey:@"environment" toDictionary:body];
  [self addSafeStringValue:event.merchant forKey:@"merchantName" toDictionary:body];
  [self addSafeStringValue:event.merchantId forKey:@"merchantId" toDictionary:body];

  // Map product enum to string and update instance variable
  // TODO: read product from ios native side, when we have access to it
  switch (event.product) {
    case 1:
      self.productName = @"transaction_link";
      break;
    case 2:
      self.productName = @"link";
      break;
    default:
      self.productName = @"card_switcher";
      break;
  }
  [self addSafeStringValue:self.productName forKey:@"product" toDictionary:body];
  [self addSafeStringValue:event.taskId forKey:@"taskId" toDictionary:body];
  [self addSafeMetaData:event.metaData forKey:@"metaData" toDictionary:body];

  // Send event to React Native with safety validation
  [self sendSafeEventWithName:@"knot:onEvent" body:body];
}

/**
 * Handle exit events from the Knot API
 *
 * This delegate method is called when the user exits the Knot API flow.
 * It sends a minimal event with just the product name to React Native.
 */
- (void)onExit {
  // Check if bridge is ready before processing the exit event
  if (![self isBridgeReadyForEvents]) {
    NSLog(@"KnotAPI onExit: Bridge inactive or closing, ignoring event");
    return;
  }

  // Build minimal exit event body
  NSMutableDictionary *body = [NSMutableDictionary dictionaryWithCapacity:1];
  [self addSafeStringValue:self.productName forKey:@"product" toDictionary:body];

  // Send exit event to React Native
  [self sendSafeEventWithName:@"knot:onExit" body:body];
}

/**
 * Handle success events from the Knot API
 *
 * This delegate method is called when the Knot API operation completes successfully.
 * It sends the merchant name and product information to React Native.
 *
 * @param merchant The merchant name from the successful operation
 */
- (void)onSuccessWithMerchant:(NSString * _Nonnull)merchant {
  // Check if bridge is ready before processing the success event
  if (![self isBridgeReadyForEvents]) {
    NSLog(@"KnotAPI onSuccess: Bridge inactive or closing, ignoring event");
    return;
  }

  // Build success event body with merchant and product info
  NSMutableDictionary *body = [NSMutableDictionary dictionaryWithCapacity:2];
  [self addSafeStringValue:merchant forKey:@"merchant" toDictionary:body];
  [self addSafeStringValue:self.productName forKey:@"product" toDictionary:body];

  // Send success event to React Native
  [self sendSafeEventWithName:@"knot:onSuccess" body:body];
}

/**
 * Clean up resources when the bridge is deallocated
 *
 * Ensures proper cleanup by marking the bridge as inactive and closing.
 * This prevents any potential issues with events being sent after deallocation.
 */
- (void)dealloc {
    // Ensure proper cleanup when bridge is deallocated
    self.isBridgeActive = NO;
    self.isClosing = YES;
}

#pragma mark - Private Helper Methods

/**
 * Check if the bridge is ready to send events
 *
 * @return BOOL YES if bridge is active and not closing, NO otherwise
 */
- (BOOL)isBridgeReadyForEvents {
    return self.isBridgeActive && !self.isClosing;
}

/**
 * Create a safe dictionary by filtering out nil and NSNull values
 *
 * This method recursively cleans a dictionary to prevent crashes when sending
 * data to React Native, which cannot handle nil or NSNull values.
 *
 * @param dictionary The dictionary to clean
 * @return NSDictionary A clean dictionary with only valid values
 */
- (NSDictionary *)createSafeDictionaryFromDictionary:(NSDictionary *)dictionary {
    if (!dictionary || ![dictionary isKindOfClass:[NSDictionary class]]) {
        return @{};
    }

    NSMutableDictionary *safeDictionary = [NSMutableDictionary dictionary];
    for (id key in dictionary) {
        id value = dictionary[key];
        // Only add valid key-value pairs (not nil or NSNull)
        if (key && value && ![key isKindOfClass:[NSNull class]] && ![value isKindOfClass:[NSNull class]]) {
            // If the value is a dictionary, recursively clean it
            if ([value isKindOfClass:[NSDictionary class]]) {
                NSDictionary *cleanedNestedDict = [self createSafeDictionaryFromDictionary:value];
                if (cleanedNestedDict.count > 0) {
                    safeDictionary[key] = cleanedNestedDict;
                }
            } else if ([value isKindOfClass:[NSArray class]]) {
                // If the value is an array, recursively clean it
                NSArray *cleanedArray = [self createSafeArrayFromArray:value];
                if (cleanedArray.count > 0) {
                    safeDictionary[key] = cleanedArray;
                }
            } else {
                safeDictionary[key] = value;
            }
        }
    }
    return [safeDictionary copy];
}

/**
 * Create a safe array by filtering out NSNull values
 *
 * This method recursively cleans an array to prevent crashes when sending
 * data to React Native, which cannot handle NSNull values in arrays.
 *
 * @param array The array to clean
 * @return NSArray A clean array with only valid values
 */
- (NSArray *)createSafeArrayFromArray:(NSArray *)array {
    if (!array || ![array isKindOfClass:[NSArray class]]) {
        return @[];
    }

    NSMutableArray *safeArray = [NSMutableArray array];
    for (id element in array) {
        // Skip NSNull values
        if ([element isKindOfClass:[NSNull class]]) {
            continue;
        }
        
        // If the element is a dictionary, recursively clean it
        if ([element isKindOfClass:[NSDictionary class]]) {
            NSDictionary *cleanedDict = [self createSafeDictionaryFromDictionary:element];
            if (cleanedDict.count > 0) {
                [safeArray addObject:cleanedDict];
            }
        } else if ([element isKindOfClass:[NSArray class]]) {
            // If the element is an array, recursively clean it
            NSArray *cleanedNestedArray = [self createSafeArrayFromArray:element];
            if (cleanedNestedArray.count > 0) {
                [safeArray addObject:cleanedNestedArray];
            }
        } else {
            // Add other valid elements
            [safeArray addObject:element];
        }
    }
    return [safeArray copy];
}

/**
 * Safely add a string value to a dictionary if it's valid
 *
 * @param value The string value to add
 * @param key The key for the value
 * @param dictionary The dictionary to add the value to
 */
- (void)addSafeStringValue:(NSString *)value forKey:(NSString *)key toDictionary:(NSMutableDictionary *)dictionary {
    if (value && [value isKindOfClass:[NSString class]] && key) {
        dictionary[key] = value;
    }
}

/**
 * Safely add metadata to a dictionary with nested safety checks
 *
 * This method ensures that metadata dictionaries are also cleaned of nil/NSNull values
 * before being added to the main dictionary.
 *
 * @param metaData The metadata dictionary to add
 * @param key The key for the metadata
 * @param dictionary The dictionary to add the metadata to
 */
- (void)addSafeMetaData:(NSDictionary *)metaData forKey:(NSString *)key toDictionary:(NSMutableDictionary *)dictionary {
    if (metaData && [metaData isKindOfClass:[NSDictionary class]] && key) {
        NSDictionary *safeMetaData = [self createSafeDictionaryFromDictionary:metaData];
        if (safeMetaData.count > 0) {
            dictionary[key] = safeMetaData;
        }
    }
}

/**
 * Send an event with automatic safety validation of the body dictionary
 *
 * This method ensures that all data sent to React Native is safe by cleaning
 * the body dictionary before sending the event.
 *
 * @param eventName The name of the event to send
 * @param body The body dictionary to send (will be cleaned automatically)
 */
- (void)sendSafeEventWithName:(NSString *)eventName body:(NSDictionary *)body {
    if (!eventName || !body) {
        return;
    }

    NSDictionary *safeBody = [self createSafeDictionaryFromDictionary:body];
    [self sendEventWithName:eventName body:safeBody];
}

@end
