#import "DapiConnectManager.h"
#import <React/RCTConvert.h>
#import <React/RCTUtils.h>
#import <DapiConnect/DapiConnect.h>
#import <WebKit/WebKit.h>

@interface DapiConnectManager () <DPCConnectDelegate>

@property (nonatomic, assign) BOOL hasListeners;
@property (nonatomic, assign) BOOL isStarted;

@end

@implementation DapiConnectManager

// MARK: - Native Module Setup
RCT_EXPORT_MODULE();

+ (BOOL)requiresMainQueueSetup {
    return YES;
}

- (NSArray<NSString *> *)supportedEvents {
    return @[
        // connect
        @"EventConnectSuccessful",
        @"EventConnectFailure",
        @"EventDapiUIWillTransfer",
        @"EventConnectDismissed",
        @"EventConnectBankRequest",
        @"EventDapiTransferUIDismissed",
    ];
}

- (void)startObserving {
    self.hasListeners = YES;
}

- (void)stopObserving {
    self.hasListeners = NO;
}

- (DPCBankConnection *)bankConnectionForUserID:(NSString *)userID {
    __block DPCBankConnection *bankConnection;
    [Dapi.sharedInstance.getConnections enumerateObjectsUsingBlock:^(DPCBankConnection * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
        if ([obj.userID isEqualToString:userID]) {
            *stop = YES;
            bankConnection = obj;
        }
    }];
    
    return bankConnection;
}

RCT_EXPORT_METHOD(start:(NSString *)appKey clientUserID:(NSString *)clientUserID configurations:(NSDictionary<NSString *, id> *)configs resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) {
    
    DPCConfigurations *configurations = [self nativeConfigurations:configs];

    [Dapi startWithAppKey:appKey clientUserID:clientUserID configuration:configurations completion:^(Dapi * _Nullable dapi, NSError * _Nullable error) {
        if (dapi) {
            self.isStarted = YES;
            resolve(nil);
        } else {
            reject([NSNumber numberWithInteger:error.code].stringValue, error.localizedDescription, error);
        }
    }];
}

RCT_EXPORT_METHOD(presentConnect:(NSString *)bankID) {
    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0ul);
    dispatch_async(queue, ^{
        dispatch_async(dispatch_get_main_queue(), ^{
            [Dapi.sharedInstance presentConnect:self bankID:bankID];
        });
    });
}

RCT_EXPORT_METHOD(dismissConnect) {
    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0ul);
    dispatch_async(queue, ^{
        dispatch_async(dispatch_get_main_queue(), ^{
            [Dapi.sharedInstance dismissConnect];
        });
    });
}

RCT_EXPORT_METHOD(setClientUserID:(NSString *)clientUserID) {
    Dapi.sharedInstance.clientUserID = clientUserID;
}

RCT_EXPORT_METHOD(clientUserID:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) {
    resolve(Dapi.sharedInstance.clientUserID);
}

RCT_EXPORT_METHOD(setConfigurations:(NSDictionary<NSString *, id> *)configs) {
    DPCConfigurations *configurations = [self nativeConfigurations:configs];
    
    Dapi.sharedInstance.configurations = configurations;
}

RCT_EXPORT_METHOD(configurations:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) {
    NSDictionary<NSString *, id> *jsConfigurations = [self jsConfigurations:Dapi.sharedInstance.configurations];
    resolve(jsConfigurations);
}

RCT_EXPORT_METHOD(isStarted:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) {
    resolve([NSNumber numberWithBool:self.isStarted]);
}

RCT_EXPORT_METHOD(getConnections:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) {
    
    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0ul);
    dispatch_async(queue, ^{
        NSMutableArray *arrayObjects = [NSMutableArray array];
        dispatch_async(dispatch_get_main_queue(), ^{
            [Dapi.sharedInstance.getConnections enumerateObjectsUsingBlock:^(DPCBankConnection * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
                if ([obj respondsToSelector:@selector(dictionaryRepresentation)]) {
                    NSDictionary<NSString *, id> *dictionaryResponse = [obj valueForKey:@"dictionaryRepresentation"];
                    [arrayObjects addObject:dictionaryResponse];
                }
            }];
            
            resolve(arrayObjects);
        });
    });
}

RCT_EXPORT_METHOD(getConnectionParameters:(NSString *)userID resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) {
    DPCBankConnection *bankConnection = [self bankConnectionForUserID:userID];
    NSDictionary<NSString *, id> *connectionParams = bankConnection.getConnectionParameters;
    
    
    NSError *error;
    NSData *jsonData = [NSJSONSerialization dataWithJSONObject:connectionParams options:NSJSONWritingPrettyPrinted error:&error];
    
    if (!jsonData) {
        reject([NSNumber numberWithInteger:error.code].stringValue, error.localizedDescription, error);
    } else {
        NSString *json = [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding];
        resolve(json);
    }
}

RCT_EXPORT_METHOD(createConnection:(NSString *)jsonConnectionParameters resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) {
    NSData *jsonData = [jsonConnectionParameters dataUsingEncoding:NSUTF8StringEncoding];
    NSError *err;
    NSDictionary<NSString *, id> *connectionParams = [NSJSONSerialization JSONObjectWithData:jsonData options:0 error:&err];
    
    if (err) {
        reject([NSNumber numberWithInteger:err.code].stringValue, err.localizedDescription, err);
    }
    
    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0ul);
    dispatch_async(queue, ^{
        dispatch_async(dispatch_get_main_queue(), ^{
            [DPCBankConnection create:connectionParams completion:^(DPCBankConnection * _Nullable connection, NSError * _Nullable error) {
                if (error != nil) {
                    reject([NSNumber numberWithInteger:error.code].stringValue, error.localizedDescription, error);
                } else {
                    NSDictionary<NSString *, id> *dictionaryResponse = [connection valueForKey:@"dictionaryRepresentation"];
                    resolve(dictionaryResponse);
                }
            }];
        });
    });
}

RCT_EXPORT_METHOD(getIdentity:(NSString *)userID resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) {
    DPCBankConnection *bankConnection = [self bankConnectionForUserID:userID];
    [bankConnection getIdentity:^(DPCIdentity * _Nullable identity, NSError * _Nullable error, NSString * _Nullable jobID) {
        [self respondForDictionaryRepresentableObject:identity error:error resolver:resolve rejecter:reject];
    }];
}

RCT_EXPORT_METHOD(getAccounts:(NSString *)userID resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) {
    DPCBankConnection *bankConnection = [self bankConnectionForUserID:userID];
    [bankConnection getAccounts:^(NSArray<DPCAccount *> * _Nullable accounts, NSError * _Nullable error, NSString * _Nullable operationID) {
        [self respondForDictionaryRepresentableObject:accounts error:error resolver:resolve rejecter:reject];
    }];
}

RCT_EXPORT_METHOD(getCards:(NSString *)userID resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) {
    DPCBankConnection *bankConnection = [self bankConnectionForUserID:userID];
    [bankConnection getCards:^(NSArray<DPCCard *> * _Nullable result, NSError * _Nullable error, NSString * _Nullable operationID) {
        [self respondForDictionaryRepresentableObject:result error:error resolver:resolve rejecter:reject];
    }];
}

RCT_EXPORT_METHOD(getTransactionsForAccount:(NSString *)userID accountID:(NSString *)accountID startDate:(NSDate *)startDate endDate:(NSDate *)endDate resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) {
    DPCBankConnection *bankConnection = [self bankConnectionForUserID:userID];
    __block DPCAccount *account;
    [bankConnection.accounts enumerateObjectsUsingBlock:^(DPCAccount * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
        if ([obj.accountID isEqualToString:accountID]) {
            *stop = YES;
            account = obj;
        }
    }];
    [bankConnection getTransactionsForAccount:account fromDate:startDate toDate:endDate completion:^(NSArray<DPCTransaction *> * _Nullable transactions, NSError * _Nullable error, NSString * _Nullable operationID) {
        [self respondForDictionaryRepresentableObject:transactions error:error resolver:resolve rejecter:reject];
    }];
}

RCT_EXPORT_METHOD(getTransactionsForCard:(NSString *)userID cardID:(NSString *)cardID startDate:(NSDate *)startDate endDate:(NSDate *)endDate resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) {
    DPCBankConnection *bankConnection = [self bankConnectionForUserID:userID];
    
    __block DPCCard *card;
    [bankConnection.cards enumerateObjectsUsingBlock:^(DPCCard * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
        if ([obj.cardID isEqualToString:cardID]) {
            *stop = YES;
            card = obj;
        }
    }];
    
    [bankConnection getTransactionsForCard:card fromDate:startDate toDate:endDate completion:^(NSArray<DPCTransaction *> * _Nullable transactions, NSError * _Nullable error, NSString * _Nullable operationID) {
        [self respondForDictionaryRepresentableObject:transactions error:error resolver:resolve rejecter:reject];
    }];
}

RCT_EXPORT_METHOD(delete:(NSString *)userID resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) {
    DPCBankConnection *bankConnection = [self bankConnectionForUserID:userID];
    [bankConnection delete:^(DPCResult * _Nullable result, NSError * _Nullable error) {
        if (error) {
            reject([NSNumber numberWithInteger:error.code].stringValue, error.localizedDescription, error);
        } else {
            resolve(nil);
        }
    }];
}

RCT_EXPORT_METHOD(getBeneficiaries:(NSString *)userID resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) {
    DPCBankConnection *bankConnection = [self bankConnectionForUserID:userID];
    [bankConnection getBeneficiaries:^(NSArray<DPCBankBeneficiary *> * _Nullable beneficiaries, NSError * _Nullable error, NSString * _Nullable operationID) {
        [self respondForDictionaryRepresentableObject:beneficiaries error:error resolver:resolve rejecter:reject];
    }];
}

RCT_EXPORT_METHOD(createBeneficiary:(NSString *)userID beneficiary:(NSDictionary<NSString *, id> *)beneficiary resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) {
    dispatch_async(dispatch_get_main_queue(), ^{
        DPCBankConnection *bankConnection = [self bankConnectionForUserID:userID];
        DPCBeneficiary *nativeBeneficiary = [self nativeBeneficiaryInfoFromDictionary:beneficiary];
        [bankConnection createBeneficiary:nativeBeneficiary completion:^(DPCResult * _Nullable result, NSError * _Nullable error, NSString * _Nullable operationID) {
            [self respondForDictionaryRepresentableObject:result error:error resolver:resolve rejecter:reject];
        }];
    });
}

RCT_EXPORT_METHOD(createWireBeneficiary:(NSString *)userID beneficiary:(NSDictionary<NSString *, id> *)beneficiary resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) {
    dispatch_async(dispatch_get_main_queue(), ^{
        DPCBankConnection *bankConnection = [self bankConnectionForUserID:userID];
        DPCWireBeneficiary *nativeBeneficiary = [self nativeWireBeneficiaryInfoFromDictionary:beneficiary];
        [bankConnection createWireBeneficiary:nativeBeneficiary completion:^(DPCResult * _Nullable result, NSError * _Nullable error, NSString * _Nullable operationID) {
            [self respondForDictionaryRepresentableObject:result error:error resolver:resolve rejecter:reject];
        }];
    });
}

RCT_EXPORT_METHOD(getWireBeneficiaries:(NSString *)userID resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) {
    DPCBankConnection *bankConnection = [self bankConnectionForUserID:userID];
    [bankConnection getWireBeneficiaries:^(NSArray<DPCBankWireBeneficiary *> * _Nullable beneficiaries, NSError * _Nullable error, NSString * _Nullable operationID) {
        [self respondForDictionaryRepresentableObject:beneficiaries error:error resolver:resolve rejecter:reject];
    }];
}

RCT_EXPORT_METHOD(getAccountsMetadata:(NSString *)userID resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) {
    DPCBankConnection *bankConnection = [self bankConnectionForUserID:userID];
    [bankConnection getAccountMetadata:^(DPCBankMetadata * _Nullable accounts, NSError * _Nullable error, NSString * _Nullable operationID) {
        [self respondForDictionaryRepresentableObject:accounts error:error resolver:resolve rejecter:reject];
    }];
}

RCT_EXPORT_METHOD(createTransferToExistingBeneficiary:(NSString *)userID accountID:(NSString *)accountID receiverID:(NSString *)receiverID amount:(double)amount remark:(NSString *)remark resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) {
    dispatch_async(dispatch_get_main_queue(), ^{
        DPCBankConnection *bankConnection = [self bankConnectionForUserID:userID];
        __block DPCAccount *account;
        [bankConnection.accounts enumerateObjectsUsingBlock:^(DPCAccount * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
            if ([obj.accountID isEqualToString:accountID]) {
                *stop = YES;
                account = obj;
            }
        }];
        
        [bankConnection createTransferToExistingBeneficiaryFromAccount:account beneficiaryID:receiverID amount:amount remark:remark completion:^(DPCAccount * _Nullable account, double amount, NSError * _Nullable error, NSString * _Nullable operationID) {
            if (error) {
                if (@available(iOS 8.0, *)) {
                    if ([error.description containsString:@"User canceled account selection"]) {
                        [self emitAccountSelectionCanceledEvent];
                    }
                }
                NSString *jsonError = [self jsonRepresentationOfError:error account:account operationID:operationID];
                reject([NSNumber numberWithInteger:error.code].stringValue, jsonError, error);
            } else {
                resolve(@{
                    @"account": account.accountID ?: [NSNull null],
                    @"amount": [NSNumber numberWithDouble:amount],
                    @"operationID": operationID,
                        });
            }

        }];
    });
}

RCT_EXPORT_METHOD(createWireTransferToExistingBeneficiary:(NSString *)userID accountID:(NSString *)accountID receiverID:(NSString *)receiverID amount:(double)amount remark:(NSString *)remark resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) {
    dispatch_async(dispatch_get_main_queue(), ^{
        DPCBankConnection *bankConnection = [self bankConnectionForUserID:userID];
        __block DPCAccount *account;
        [bankConnection.accounts enumerateObjectsUsingBlock:^(DPCAccount * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
            if ([obj.accountID isEqualToString:accountID]) {
                *stop = YES;
                account = obj;
            }
        }];
        
        [bankConnection createWireTransferToExistingBeneficiaryFromAccount:account beneficiaryID:receiverID amount:amount remark:remark completion:^(DPCAccount * _Nullable account, double amount, NSError * _Nullable error, NSString * _Nullable operationID) {
            if (error) {
                if (@available(iOS 8.0, *)) {
                    if ([error.description containsString:@"User canceled account selection"]) {
                        [self emitAccountSelectionCanceledEvent];
                    }
                }
                NSString *jsonError = [self jsonRepresentationOfError:error account:account operationID:operationID];
                reject([NSNumber numberWithInteger:error.code].stringValue, jsonError, error);
            } else {
                resolve(@{
                    @"account": account.accountID ?: [NSNull null],
                    @"amount": [NSNumber numberWithDouble:amount],
                    @"operationID": operationID,
                        });
            }

        }];
    });
}



RCT_EXPORT_METHOD(createWireTransfer:(NSString *)userID beneficiary:(NSDictionary<NSString *, id> *)beneficiary accountID:(NSString *)accountID amount:(double)amount remark:(NSString *)remark resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) {
    dispatch_async(dispatch_get_main_queue(), ^{
        DPCBankConnection *bankConnection = [self bankConnectionForUserID:userID];
        __block DPCAccount *account;
        [bankConnection.accounts enumerateObjectsUsingBlock:^(DPCAccount * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
            if ([obj.accountID isEqualToString:accountID]) {
                *stop = YES;
                account = obj;
            }
        }];
        DPCWireBeneficiary *nativeBeneficiary = [self nativeWireBeneficiaryInfoFromDictionary:beneficiary];
        [bankConnection createWireTransferFromAccount:account toWireBeneficiary:nativeBeneficiary amount:amount remark:remark completion:^(DPCAccount * _Nullable account, double amount, NSError * _Nullable error, NSString * _Nullable operationID) {
            if (error) {
                if (@available(iOS 8.0, *)) {
                    if ([error.description containsString:@"User canceled account selection"]) {
                        [self emitAccountSelectionCanceledEvent];
                    }
                }

                NSString *jsonError = [self jsonRepresentationOfError:error account:account operationID:operationID];
                reject([NSNumber numberWithInteger:error.code].stringValue, jsonError, error);
            } else {
                resolve(@{
                    @"account": account.accountID ?: [NSNull null],
                    @"amount": [NSNumber numberWithDouble:amount],
                    @"operationID": operationID
                        });
            }
        }];
    });
}

RCT_EXPORT_METHOD(createTransfer:(NSString *)userID accountID:(NSString *)accountID beneficiary:(NSDictionary<NSString *, id> *)beneficiary amount:(double)amount remark:(NSString *)remark resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) {
    dispatch_async(dispatch_get_main_queue(), ^{
        DPCBankConnection *bankConnection = [self bankConnectionForUserID:userID];
        __block DPCAccount *account;
        [bankConnection.accounts enumerateObjectsUsingBlock:^(DPCAccount * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
            if ([obj.accountID isEqualToString:accountID]) {
                *stop = YES;
                account = obj;
            }
        }];
        DPCBeneficiary *nativeBeneficiary = [self nativeBeneficiaryInfoFromDictionary:beneficiary];
        [bankConnection createTransferFromAccount:account toBeneficiary:nativeBeneficiary amount:amount remark:remark completion:^(DPCAccount * _Nullable account, double amount, NSError * _Nullable error, NSString * _Nullable operationID) {
            if (error) {
                if (@available(iOS 8.0, *)) {
                    if ([error.description containsString:@"User canceled account selection"]) {
                        [self emitAccountSelectionCanceledEvent];
                    }
                }

                NSString *jsonError = [self jsonRepresentationOfError:error account:account operationID:operationID];
                reject([NSNumber numberWithInteger:error.code].stringValue, jsonError, error);
            } else {
                resolve(@{
                    @"account": account.accountID ?: [NSNull null],
                    @"amount": [NSNumber numberWithDouble:amount],
                    @"operationID": operationID
                        });
            }
        }];
    });
}


- (void)connectDidFailConnectingToBankID:(nonnull NSString *)bankID withError:(nonnull NSString *)error {
    id body = @{
        @"bankID": bankID ?: [NSNull null],
        @"error": error ?: [NSNull null]
    };
    if (self.hasListeners)
        [self sendEventWithName:self.supportedEvents[1] body:body];
}

- (void)connectDidSuccessfullyConnectToBankID:(nonnull NSString *)bankID connection:(nonnull DPCBankConnection *)connection {
        
    NSDictionary<NSString *, id> *connectionDictionary;
    if ([connection respondsToSelector:@selector(dictionaryRepresentation)]) {
        connectionDictionary = [connection valueForKey:@"dictionaryRepresentation"];
    }

    id body = @{
        @"bankID": bankID,
        @"connection": connectionDictionary ?: [NSNull null]
    };

    if (self.hasListeners)
        [self sendEventWithName:self.supportedEvents[0] body:body];
}

- (void)connectDidRequestBank:(NSString *)bankName iban:(NSString *)iban {
    id body = @{
        @"bankName": bankName ?: [NSNull null],
        @"iban": iban ?: [NSNull null]
    };
    if (self.hasListeners)
        [self sendEventWithName:self.supportedEvents[4] body:body];
}

- (void)connectDidDismiss {
    if (self.hasListeners)
        [self sendEventWithName:self.supportedEvents[3] body:nil];
}

// MARK: - Helper Methods
- (void)respondForDictionaryRepresentableObject:(NSObject *)object error:(NSError *)error resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject{
    
    if (error) {
        reject([NSNumber numberWithInteger:error.code].stringValue, error.localizedDescription, error);
    } else if ([object isKindOfClass:[NSArray class]]) {
        NSArray *arrayObjects = (NSArray *)object;
        
        [arrayObjects enumerateObjectsUsingBlock:^(id  _Nonnull element, NSUInteger idx, BOOL * _Nonnull stop) {
            if ([element respondsToSelector:@selector(dictionaryRepresentation)]) {
                NSDictionary<NSString *, id> *dictionaryResponse = [element valueForKey:@"dictionaryRepresentation"];
                resolve(dictionaryResponse);
                *stop = YES;
            } else {
                NSError *error = [NSError errorWithDomain:@"com.dapi.dapiconnect.reactnative" code:1012 userInfo:@{NSLocalizedDescriptionKey: @"Couldn't construct JSON representation of native array objects"}];
                reject([NSNumber numberWithInteger:error.code].stringValue, error.localizedDescription, error);
                *stop = YES;
            }
        }];
    } else {
        if ([object respondsToSelector:@selector(dictionaryRepresentation)]) {
            NSDictionary<NSString *, id> *dictionaryResponse = [object valueForKey:@"dictionaryRepresentation"];
            resolve(dictionaryResponse);
        } else {
            NSError *castingError = [NSError errorWithDomain:@"com.dapi.dapiconnect.reactnative" code:1012 userInfo:@{NSLocalizedDescriptionKey: @"Couldn't construct JSON representation of native array objects"}];
            reject([NSNumber numberWithInteger:error.code].stringValue, castingError.localizedDescription, castingError);
        }
    }
}

// MARK: - Helper Methods

- (void)emitAccountSelectionCanceledEvent {
    if (self.hasListeners)
        [self sendEventWithName:self.supportedEvents[5] body:nil];
}

- (DPCConfigurations *)nativeConfigurations:(NSDictionary<NSString *, id> *)configs {
    NSArray *countries = [configs objectForKey:@"countries"];
    NSString *environment = [configs objectForKey:@"environment"];
    
    DPCConfigurations *configurations = [[DPCConfigurations alloc] initWithCountries:countries environment:environment];
    
    NSDictionary<NSString *, NSDictionary<NSString *, NSString *> *> *endPointExtraHeaderFields = [configs objectForKey:@"endPointExtraHeaderFields"];
    NSDictionary<NSString *, NSDictionary<NSString *, id> *> *endPointExtraBody = [configs objectForKey:@"endPointExtraBody"];
    NSDictionary<NSString *, NSString *> *endpoints = [configs objectForKey:@"endpoints"];

    configurations.endPointExtraHeaderFields = endPointExtraHeaderFields;
    configurations.endPointExtraBody = endPointExtraBody;
    configurations.endpoints = endpoints;
    NSNumber *showCloseButton = [configs objectForKey:@"showCloseButton"];
    NSNumber *showLogos = [configs objectForKey:@"showLogos"];
    NSNumber *showAddButton = [configs objectForKey:@"showAddButton"];
    NSNumber *showTransferSuccessfulResult = [configs objectForKey:@"showTransferSuccessfulResult"];
    NSNumber *showTransferErrorResult = [configs objectForKey:@"showTransferErrorResult"];
    NSString *postSuccessfulConnectionLoadingText = [configs objectForKey:@"postSuccessfulConnectionLoadingText"];

    if (showCloseButton != nil) {
        configurations.showCloseButton = showCloseButton.boolValue;
    }
    
    if (showLogos != nil) {
        configurations.showLogos = showLogos.boolValue;
    }
    
    if (showAddButton != nil) {
        configurations.showAddAccountButton = showAddButton.boolValue;
    }
    
    if (showTransferSuccessfulResult != nil) {
        configurations.showTransferSuccessfulResult = showTransferSuccessfulResult.boolValue;
    }

    if (showTransferErrorResult != nil) {
        configurations.showTransferErrorResult = showTransferErrorResult.boolValue;
    }
    
    if (postSuccessfulConnectionLoadingText != nil) {
        configurations.postSuccessfulConnectionLoadingText = postSuccessfulConnectionLoadingText;
    }

    return configurations;
}

- (NSDictionary<NSString *, id> *)jsConfigurations:(DPCConfigurations *)configs {
    return @{
        @"countries": configs.countries ?: [NSNull null],
        @"environment": configs.environment ?: [NSNull null],
        @"endpoints": configs.endpoints ?: [NSNull null],
        @"endPointExtraQueryItems": configs.endPointExtraQueryItems ?: [NSNull null],
        @"endPointExtraHeaderFields": configs.endPointExtraHeaderFields ?: [NSNull null],
        @"endPointExtraBody": configs.endPointExtraBody ?: [NSNull null],
        @"showCloseButton": [NSNumber numberWithBool:configs.showCloseButton],
        @"showLogos": [NSNumber numberWithBool:configs.showCloseButton],
        @"showAddButton": [NSNumber numberWithBool:configs.showAddAccountButton],
        @"showTransferSuccessfulResult": [NSNumber numberWithBool:configs.showTransferSuccessfulResult],
        @"showTransferErrorResult": [NSNumber numberWithBool:configs.showTransferErrorResult]
    };
}

- (NSDictionary<DPCEndPoint, NSString *> *)parseEndpoints:(NSDictionary *)endpoints {
    NSMutableDictionary<DPCEndPoint, NSString *> *result = [NSMutableDictionary dictionary];
    
    [endpoints enumerateKeysAndObjectsUsingBlock:^(id  _Nonnull key, id  _Nonnull obj, BOOL * _Nonnull stop) {
        if ([key isKindOfClass:[NSString class]] && [obj isKindOfClass:[NSString class]]) {
            NSString *k = (NSString *)key;
            NSString *v = (NSString *)obj;
            
            if (![k isEqualToString:v]) {
                DPCEndPoint endpoint = [self nativeEndpointToJSEndpoint:k];
                if (endpoint) {
                    [result setObject:v forKey:endpoint];
                }
            }
        }
    }];
    
    return result;
}

- (DPCEndPoint)nativeEndpointToJSEndpoint:(NSString *)endpoint {
    
    if ([endpoint isEqualToString:@"exchangeToken"]) {
        return DPCEndPointExchangeToken;
    } else if ([endpoint isEqualToString:@"getIdentity"]) {
        return DPCEndPointGetIdentity;
    } else if ([endpoint isEqualToString:@"getAccounts"]) {
        return DPCEndPointGetAccounts;
    } else if ([endpoint isEqualToString:@"getAccountMetadata"]) {
        return DPCEndPointGetAccountMetadata;
    } else if ([endpoint isEqualToString:@"getTransactions"]) {
        return DPCEndPointGetTransactions;
    } else if ([endpoint isEqualToString:@"createTransfer"]) {
        return DPCEndPointCreateTransfer;
    } else if ([endpoint isEqualToString:@"resumeJob"]) {
        return DPCEndPointResumeJob;
    } else {
        return nil;
    }
}

- (DPCBeneficiary *)nativeBeneficiaryInfoFromDictionary:(NSDictionary<NSString *,id> *)beneficiaryInfoDictionary {
    DPCBeneficiary *beneficiary = [[DPCBeneficiary alloc] init];
    NSDictionary<NSString *, id> *lineAddressDictionary = [beneficiaryInfoDictionary objectForKey:@"linesAddress"];
    beneficiary.linesAddress = [[DPCLinesAddress alloc] initWithDictionary:lineAddressDictionary];
    beneficiary.accountNumber = [beneficiaryInfoDictionary objectForKey:@"accountNumber"];
    beneficiary.name = [beneficiaryInfoDictionary objectForKey:@"name"];
    beneficiary.bankName = [beneficiaryInfoDictionary objectForKey:@"bankName"];
    beneficiary.swiftCode = [beneficiaryInfoDictionary objectForKey:@"swiftCode"];
    beneficiary.iban = [beneficiaryInfoDictionary objectForKey:@"iban"];
    beneficiary.phoneNumber = [beneficiaryInfoDictionary objectForKey:@"phoneNumber"];
    beneficiary.country = [beneficiaryInfoDictionary objectForKey:@"country"];
    beneficiary.branchAddress = [beneficiaryInfoDictionary objectForKey:@"branchAddress"];
    beneficiary.branchName = [beneficiaryInfoDictionary objectForKey:@"branchName"];
    return beneficiary;
}

- (DPCWireBeneficiary *)nativeWireBeneficiaryInfoFromDictionary:(NSDictionary<NSString *,id> *)beneficiaryInfoDictionary {
    DPCWireBeneficiary *beneficiary = [[DPCWireBeneficiary alloc] init];
    NSDictionary<NSString *, id> *lineAddressDictionary = [beneficiaryInfoDictionary objectForKey:@"linesAddress"];
    beneficiary.linesAddress = [[DPCLinesAddress alloc] initWithDictionary:lineAddressDictionary];
    beneficiary.accountNumber = [beneficiaryInfoDictionary objectForKey:@"accountNumber"];
    beneficiary.name = [beneficiaryInfoDictionary objectForKey:@"name"];
    beneficiary.country = [beneficiaryInfoDictionary objectForKey:@"country"];
    beneficiary.receiverType = [beneficiaryInfoDictionary objectForKey:@"receiverType"];
    beneficiary.routingNumber = [beneficiaryInfoDictionary objectForKey:@"routingNumber"];
    beneficiary.nickname = [beneficiaryInfoDictionary objectForKey:@"nickname"];
    beneficiary.receiverAccountType = [beneficiaryInfoDictionary objectForKey:@"receiverAccountType"];
    beneficiary.firstName = [beneficiaryInfoDictionary objectForKey:@"firstName"];
    beneficiary.lastName = [beneficiaryInfoDictionary objectForKey:@"lastName"];
    beneficiary.zipCode = [beneficiaryInfoDictionary objectForKey:@"zipCode"];
    beneficiary.state = [beneficiaryInfoDictionary objectForKey:@"state"];
    beneficiary.city = [beneficiaryInfoDictionary objectForKey:@"city"];
    return beneficiary;
}

- (NSString *)jsonRepresentationOfError:(NSError *)error account:(DPCAccount *)account operationID:(NSString *)operationID {
    
    NSDictionary *accountDictionaryRepresentation;
    if ([account respondsToSelector:@selector(dictionaryRepresentation)]) {
        accountDictionaryRepresentation = [account valueForKey:@"dictionaryRepresentation"];
    }
    
    NSDictionary *jsonError = @{
        @"error": error.localizedDescription ?: [NSNull null],
        @"account": accountDictionaryRepresentation ?: [NSNull null],
        @"operationID": operationID ?: [NSNull null],
    };
    
    NSError *convertingError;
    NSData *jsonData = [NSJSONSerialization dataWithJSONObject:jsonError
                                                       options:NSJSONWritingPrettyPrinted
                                                         error:&convertingError];
    if (convertingError) {
        return [NSString stringWithFormat:@"{\"error\": %@, \"account\": null}", error.localizedDescription];
    } else {
        NSString *jsonString = [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding];
        return jsonString;
    }
}

@end

