//
// Copyright © 2024 MediaMall Technologies, Inc.  All rights reserved.
//

#import "ConnectSdk.h"
#import "YCConnect+Additions.h"

#import <YounifyConnectSDK/YounifyConnectSDK.h>

#define NameOf(arg) (@""#arg)

#pragma mark JS Synchronized Constants

//XXX: Move the JS synchronized strings into a seperate file - most of it is already in the Additions files.
//XXX: Consider exporting these strings as constants

static NSString *const EventLogLogName = @"Log_onLog";
static NSString *const EventLogLogParam_Level = @"level";
static NSString *const EventLogLogParam_Message = @"message";

static NSString *const EventTokenRenewTokensName = @"Token_onRenewTokens";
static NSString *const EventTokenRenewTokensParam_RenewTokenID = @"renewalId";
static NSString *const EventTokenRenewTokensParam_ExpiredAccessToken = @"expiredAccessToken";
static NSString *const EventTokenRenewTokensParam_RefreshToken = @"refreshToken";

static NSString *const EventTokenRenewedTokensName = @"Token_onRenewedTokens";
static NSString *const EventTokenRenewedTokensParam_NewAccessToken = @"newAccessToken";
static NSString *const EventTokenRenewedTokensParam_NewRefreshToken = @"newRefreshToken";

static NSString *const EventFetchContentServiceErrorName = @"FetchContent_onServiceError";
static NSString *const EventFetchContentServiceErrorParam_RequestID = @"requestId";
static NSString *const EventFetchContentServiceErrorParam_Service = @"service";
static NSString *const EventFetchContentServiceErrorParam_Error = @"error";

static NSString *const EventFetchContentCategoryServiceCompleteName = @"FetchContent_onCategoryServiceComplete";
static NSString *const EventFetchContentCategoryServiceCompleteParam_RequestID = @"requestId";
static NSString *const EventFetchContentCategoryServiceCompleteParam_Category = @"category";
static NSString *const EventFetchContentCategoryServiceCompleteParam_Service = @"service";
static NSString *const EventFetchContentCategoryServiceCompleteParam_Content = @"content";
static NSString *const EventFetchContentCategoryServiceCompleteParam_Error = @"error";

static NSString *const EventSearchContentServiceCompleteName = @"SearchContent_onServiceComplete";
static NSString *const EventSearchContentServiceCompleteParam_RequestID = @"requestId";
static NSString *const EventSearchContentServiceCompleteParam_Service = @"service";
static NSString *const EventSearchContentServiceCompleteParam_Content = @"content";
static NSString *const EventSearchContentServiceCompleteParam_Error = @"error";

static NSString *const StreamingServiceLinkResultKey_Service = @"service";
static NSString *const StreamingServiceLinkResultKey_Success = @"success";

#pragma mark Objects

typedef void (^TokenRenewCompletionBlock)(NSString *accessToken, NSString *refreshToken);


@interface YCSRCancellationState : NSObject

@property (nonatomic, strong) NSString *ticketId;
@property (nonatomic, strong) YCSCancellationTicket *ticket;
@property (nonatomic, assign) long count;

@end

@implementation YCSRCancellationState

- (instancetype)initWithTicketId:(NSString *)ticketId
{
    if (self = [super init])
    {
        self.ticketId = ticketId;
        self.ticket = [[YCSCancellationTicket alloc] init];
        self.count = 0;
    }

    return self;
}

@end


@interface YCSRModule ()

@property (nonatomic, strong) YCSConnect *ycsConnect;
@property (nonatomic, strong) NSMutableDictionary<NSNumber *, TokenRenewCompletionBlock> *pendingRenewal;
@property (nonatomic, strong) NSMutableDictionary<NSString *, YCSRCancellationState *> *cancellationMap;
@property (nonatomic, strong) NSNumber *renewTokenId;
@property (nonatomic, assign) BOOL hasListeners;

@end


@implementation YCSRModule
{
    dispatch_queue_t _queueMain;
    dispatch_queue_t _queueWorker;
}

RCT_EXPORT_MODULE(YounifyConnectSDK)

- (instancetype)init
{
    if (self = [super init])
    {
        self.ycsConnect = [YCSConnect shared];
        self.pendingRenewal = [[NSMutableDictionary<NSNumber *, TokenRenewCompletionBlock> alloc] init];
        self.renewTokenId = [NSNumber numberWithInt:0];
        self.cancellationMap = [[NSMutableDictionary<NSString *, YCSRCancellationState *> alloc] init];
        self.hasListeners = NO;

        _queueMain = dispatch_get_main_queue();
        _queueWorker = dispatch_queue_create(NULL, DISPATCH_QUEUE_SERIAL);
    }

    return self;
}

+ (BOOL)requiresMainQueueSetup
{
    return YES;
}

- (NSArray<NSString *> *)supportedEvents
{
    return @[EventLogLogName, EventTokenRenewTokensName, EventTokenRenewedTokensName, EventFetchContentServiceErrorName, EventFetchContentCategoryServiceCompleteName, EventSearchContentServiceCompleteName];
}

- (void)startObserving
{
    dispatch_async(_queueWorker,
    ^{
        self.hasListeners = YES;
    });
}

- (void)stopObserving
{
    dispatch_async(_queueWorker,
    ^{
        self.hasListeners = NO;
    });
}

- (void)sendEventName:(NSString *)eventName body:(id)body
{
    dispatch_async(_queueWorker,
    ^{
        if (self.hasListeners)
            [self sendEventWithName:eventName body:body];
    });
}

- (void)resolveOrReject:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject data:(id)data error:(NSError *)error
{
    if (error)
    {
        NSMutableDictionary *userInfo = error.userInfo ? [error.userInfo mutableCopy] : [[NSMutableDictionary alloc] init];
        userInfo[YCSErrorUserInfoPartialResultKey] = data ? data : [NSNull null];

        NSError *errorWithData = [NSError errorWithDomain:error.domain code:error.code userInfo:userInfo];
        dispatch_async(_queueWorker, ^{ reject(errorWithData.codeString, errorWithData.localizedDescription, errorWithData); });
        return;
    }

    dispatch_async(_queueWorker, ^{ resolve(data); });
}

- (void)rejectWithInvalidArgument:(RCTPromiseRejectBlock)reject paramName:(NSString *)paramName
{
    NSError *error = [YCSErrors createError:YCSErrorInvalidArgument userInfo:@{ YCSErrorUserInfoParamNameKey: paramName }];
    dispatch_async(_queueWorker, ^{ reject(error.codeString, error.localizedDescription, error); });
}

- (YCSRCancellationState *)registerCancellation:(NSString *)cancellationTicketId
{
    YCSRCancellationState *cancellationState = nil;

    if (cancellationTicketId)
    {
        cancellationState = self.cancellationMap[cancellationTicketId];
        if (!cancellationState)
        {
            cancellationState = [[YCSRCancellationState alloc] initWithTicketId:cancellationTicketId];
            [self.cancellationMap setValue:cancellationState forKey:cancellationTicketId];
        }

        cancellationState.count++;
    }

    return cancellationState;
}

- (void)unregisterCancellation:(YCSRCancellationState *)cancellationState
{
    if (cancellationState && (--cancellationState.count == 0))
        [self.cancellationMap removeObjectForKey:cancellationState.ticketId];
}

#pragma mark - YCSLogDelegate

- (void)log:(YCSLogLevel)level message:(NSString *)message
{
    [self sendEventName:EventLogLogName body:@{ EventLogLogParam_Level: [YCSRLogLevelEnum toString:level], EventLogLogParam_Message: message }];
}

#pragma mark - YCSTokenDelegate

- (void)renewTokens:(NSString *)expiredAccessToken refreshToken:(NSString *)refreshToken completionHandler:(void (^)(NSString *, NSString *))completionHandler
{
    NSNumber *renewTokenId;
    NSMutableDictionary<NSNumber *, TokenRenewCompletionBlock> *pendingRenewal = self.pendingRenewal;

    @synchronized (pendingRenewal)
    {
        renewTokenId = @(self.renewTokenId.intValue + 1);
        pendingRenewal[renewTokenId] = completionHandler;
        self.renewTokenId = renewTokenId;
    }

    [self sendEventName:EventTokenRenewTokensName body:@{ EventTokenRenewTokensParam_RenewTokenID: renewTokenId, EventTokenRenewTokensParam_ExpiredAccessToken: expiredAccessToken, EventTokenRenewTokensParam_RefreshToken: refreshToken }];
}

- (void)renewedTokens:(NSString *)newAccessToken refreshToken:(NSString *)newRefreshToken
{
    [self sendEventName:EventTokenRenewedTokensName body:@{ EventTokenRenewedTokensParam_NewAccessToken: newAccessToken, EventTokenRenewedTokensParam_NewRefreshToken: newRefreshToken }];
}

#pragma mark - Public methods

RCT_EXPORT_METHOD(configure:(NSDictionary *)options resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject)
{
    dispatch_async(_queueMain,
    ^{
        YCSConnectOptions *opt = [[YCSConnectOptions alloc] initWithJSONDictionary:options];
        opt.tokenDelegate = self;
        opt.logDelegate = self;

        [self.ycsConnect configure:opt];
        [self resolveOrReject:resolve reject:reject data:nil error:nil];
    });
}

RCT_EXPORT_METHOD(setUserTokens:(NSString *)accessToken refreshToken:(NSString *)refreshToken resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject)
{
    dispatch_async(_queueMain,
    ^{
        [self.ycsConnect setUserTokens:accessToken refreshToken:refreshToken];
        [self resolveOrReject:resolve reject:reject data:nil error:nil];
    });
}

RCT_EXPORT_METHOD(completeTokenRenewal:(NSNumber * _Nonnull)renewalId accessToken:(NSString *)accessToken refreshToken:(NSString *)refreshToken resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject)
{
    dispatch_async(_queueMain,
    ^{
        TokenRenewCompletionBlock completionBlock = nil;
        NSMutableDictionary<NSNumber *, TokenRenewCompletionBlock> *pendingRenewal = self.pendingRenewal;

        @synchronized (pendingRenewal)
        {
            completionBlock = pendingRenewal[renewalId];
            if (completionBlock)
                [pendingRenewal removeObjectForKey:renewalId];
        }

        if (!completionBlock)
        {
            [self rejectWithInvalidArgument:reject paramName:NameOf(renewalId)];
            return;
        }

        completionBlock(accessToken, refreshToken);
        [self resolveOrReject:resolve reject:reject data:nil error:nil];
    });
}

RCT_EXPORT_METHOD(requestUserConsent:(NSString *)cancellationTicketId resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject)
{
    dispatch_async(_queueMain,
    ^{
        YCSRCancellationState *cancellationState = [self registerCancellation:cancellationTicketId];

        [self.ycsConnect requestUserConsent:cancellationState.ticket completionHandler:^(BOOL consented, NSError *error)
        {
            [self unregisterCancellation:cancellationState];
            [self resolveOrReject:resolve reject:reject data:@(consented) error:error];
        }];
    });
}

RCT_EXPORT_METHOD(linkService:(NSDictionary *)service cancellationTicket:(NSString *)cancellationTicketId resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject)
{
    dispatch_async(_queueMain,
    ^{
        if (!service)
        {
            [self rejectWithInvalidArgument:reject paramName:NameOf(service)];
            return;
        }

        YCSRCancellationState *cancellationState = [self registerCancellation:cancellationTicketId];

        YCSStreamingService *streamingService = [[YCSStreamingService alloc] initWithJSONDictionary:service];
        [self.ycsConnect linkService:streamingService cancellationTicket:cancellationState.ticket completionHandler:^(YCSStreamingService *service, BOOL success, NSError *error)
        {
            [self unregisterCancellation:cancellationState];

            NSDictionary *serviceJsonDict = [service JSONDictionary];
            NSDictionary *resultJsonDict = @
            {
                StreamingServiceLinkResultKey_Success: @(success),
                StreamingServiceLinkResultKey_Service: serviceJsonDict
            };

            [self resolveOrReject:resolve reject:reject data:resultJsonDict error:error];
        }];
    });
}

RCT_EXPORT_METHOD(manageLinkedService:(NSDictionary *)service cancellationTicket:(NSString *)cancellationTicketId resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject)
{
    dispatch_async(_queueMain,
    ^{
        if (!service)
        {
            [self rejectWithInvalidArgument:reject paramName:NameOf(service)];
            return;
        }

        YCSRCancellationState *cancellationState = [self registerCancellation:cancellationTicketId];

        YCSStreamingService *streamingService = [[YCSStreamingService alloc] initWithJSONDictionary:service];
        [self.ycsConnect manageLinkedService:streamingService cancellationTicket:cancellationState.ticket completionHandler:^(YCSStreamingService *service, NSError *error)
        {
            [self unregisterCancellation:cancellationState];

            NSDictionary *serviceJsonDict = [service JSONDictionary];
            [self resolveOrReject:resolve reject:reject data:serviceJsonDict error:error];
        }];
    });
}

RCT_EXPORT_METHOD(unlinkService:(NSDictionary *)service cancellationTicket:(NSString *)cancellationTicketId resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject)
{
    dispatch_async(_queueMain,
    ^{
        if (!service)
        {
            [self rejectWithInvalidArgument:reject paramName:NameOf(service)];
            return;
        }

        YCSRCancellationState *cancellationState = [self registerCancellation:cancellationTicketId];

        YCSStreamingService *streamingService = [[YCSStreamingService alloc] initWithJSONDictionary:service];
        [self.ycsConnect unlinkService:streamingService cancellationTicket:cancellationState.ticket completionHandler:^(YCSStreamingService *service, NSError *error)
        {
            [self unregisterCancellation:cancellationState];

            NSDictionary *serviceJsonDict = [service JSONDictionary];
            [self resolveOrReject:resolve reject:reject data:serviceJsonDict error:error];
        }];
    });
}

RCT_EXPORT_METHOD(fetchServices:(NSString *)cancellationTicketId resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject)
{
    dispatch_async(_queueMain,
    ^{
        YCSRCancellationState *cancellationState = [self registerCancellation:cancellationTicketId];

        [self.ycsConnect fetchServices:cancellationState.ticket completionHandler:^(NSArray<YCSStreamingService *> *services, NSError *error)
        {
            [self unregisterCancellation:cancellationState];

            NSMutableArray *resultJsonArray = [[NSMutableArray alloc] initWithCapacity:services.count];
            for (YCSStreamingService *service in services)
            {
                NSMutableDictionary *serviceJsonDict = [service JSONDictionary];
                [resultJsonArray addObject:serviceJsonDict];
            }

            [self resolveOrReject:resolve reject:reject data:resultJsonArray error:error];
        }];
    });
}

RCT_EXPORT_METHOD(fetchLinkedServices:(NSString *)cancellationTicketId resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject)
{
    dispatch_async(_queueMain,
    ^{
        YCSRCancellationState *cancellationState = [self registerCancellation:cancellationTicketId];

        [self.ycsConnect fetchLinkedServices:cancellationState.ticket completionHandler:^(NSArray<YCSStreamingService *> *services, NSError *error)
        {
            [self unregisterCancellation:cancellationState];

            NSMutableArray *resultJsonArray = [[NSMutableArray alloc] initWithCapacity:services.count];
            for (YCSStreamingService *service in services)
            {
                NSMutableDictionary *serviceJsonDict = [service JSONDictionary];
                [resultJsonArray addObject:serviceJsonDict];
            }

            [self resolveOrReject:resolve reject:reject data:resultJsonArray error:error];
        }];
    });
}

RCT_EXPORT_METHOD(fetchBrokenServices:(NSString *)cancellationTicketId resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject)
{
    dispatch_async(_queueMain,
    ^{
        YCSRCancellationState *cancellationState = [self registerCancellation:cancellationTicketId];

        [self.ycsConnect fetchBrokenServices:cancellationState.ticket completionHandler:^(NSArray<YCSStreamingService *> *services, NSError *error)
        {
            [self unregisterCancellation:cancellationState];

            NSMutableArray *resultJsonArray = [[NSMutableArray alloc] initWithCapacity:services.count];
            for (YCSStreamingService *service in services)
            {
                NSMutableDictionary *serviceJsonDict = [service JSONDictionary];
                [resultJsonArray addObject:serviceJsonDict];
            }

            [self resolveOrReject:resolve reject:reject data:resultJsonArray error:error];
        }];
    });
}

RCT_EXPORT_METHOD(fetchCategories:(NSString *)cancellationTicketId resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject)
{
    dispatch_async(_queueMain,
    ^{
        YCSRCancellationState *cancellationState = [self registerCancellation:cancellationTicketId];

        [self.ycsConnect fetchCategories:cancellationState.ticket completionHandler:^(NSArray<YCSStreamingCategory *> *categories, NSError *error)
        {
            [self unregisterCancellation:cancellationState];

            NSMutableArray *resultJsonArray = [[NSMutableArray alloc] initWithCapacity:categories.count];
            for (YCSStreamingCategory *category in categories)
            {
                NSMutableDictionary *categoryJsonDict = [category JSONDictionary];
                [resultJsonArray addObject:categoryJsonDict];
            }

            [self resolveOrReject:resolve reject:reject data:resultJsonArray error:error];
        }];
    });
}

RCT_EXPORT_METHOD(fetchContent:(NSString *)requestId categories:(NSArray<NSDictionary *> *)categories services:(NSArray<NSDictionary *> *)services cancellationTicket:(NSString *)cancellationTicketId serviceErrors:(BOOL)serviceErrors categoryServiceCompletions:(BOOL)categoryServiceCompletions allCompletions:(BOOL)allCompletions resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject)
{
    dispatch_async(_queueMain,
    ^{
        YCSRCancellationState *cancellationState = [self registerCancellation:cancellationTicketId];

        NSMutableArray<YCSStreamingCategory *> *streamingCategories = [[NSMutableArray<YCSStreamingCategory *> alloc] initWithCapacity:categories.count];
        for (id category in categories)
            [streamingCategories addObject:[[YCSStreamingCategory alloc] initWithJSONDictionary:category]];

        NSMutableArray<YCSStreamingService *> *streamingServices = [[NSMutableArray<YCSStreamingService *> alloc] initWithCapacity:services.count];
        for (id service in services)
            [streamingServices addObject:[[YCSStreamingService alloc] initWithJSONDictionary:service]];

        [self.ycsConnect
            fetchContent:streamingCategories
            services:streamingServices
            cancellationTicket:cancellationState.ticket
            serviceErrorHandler:^(YCSStreamingService *service, NSError *error)
            {
                if (serviceErrors)
                {
                    NSDictionary *errorJsonDict = [error JSONDictionary];
                    NSDictionary *serviceJsonDict = [service JSONDictionary];
                    NSDictionary *eventJsonDict =
                    @{
                        EventFetchContentServiceErrorParam_RequestID: requestId,
                        EventFetchContentServiceErrorParam_Error: errorJsonDict,
                        EventFetchContentServiceErrorParam_Service: serviceJsonDict
                    };

                    [self sendEventName:EventFetchContentServiceErrorName body:eventJsonDict];
                }
            }
            categoryServiceCompletionHandler:^(YCSStreamingCategory *category, YCSStreamingService *service, NSArray<YCSStreamingContent *> *content, NSError *error)
            {
                if (categoryServiceCompletions)
                {
                    NSDictionary *errorJsonDict = error ? [error JSONDictionary] : (NSDictionary *)[NSNull null];
                    NSDictionary *categoryJsonDict = [category JSONDictionary];
                    NSDictionary *serviceJsonDict = [service JSONDictionary];

                    NSMutableArray<NSDictionary *> *contentJsonArray = [[NSMutableArray<NSDictionary *> alloc] initWithCapacity:content.count];
                    for (YCSStreamingContent *streamingContent in content)
                    {
                        NSDictionary *streamingContentJsonDict = [streamingContent JSONDictionary];
                        [contentJsonArray addObject:streamingContentJsonDict];
                    }

                    NSDictionary *eventJsonDict =
                    @{
                        EventFetchContentCategoryServiceCompleteParam_RequestID: requestId,
                        EventFetchContentCategoryServiceCompleteParam_Error: errorJsonDict,
                        EventFetchContentCategoryServiceCompleteParam_Category: categoryJsonDict,
                        EventFetchContentCategoryServiceCompleteParam_Service: serviceJsonDict,
                        EventFetchContentCategoryServiceCompleteParam_Content: contentJsonArray
                    };

                    [self sendEventName:EventFetchContentCategoryServiceCompleteName body:eventJsonDict];
                }
            }
            allCompletionHandler:^(NSArray<YCSFetchContentCategoryResult *> *result, NSError *error)
            {
                [self unregisterCancellation:cancellationState];

                NSMutableArray *resultJsonArray;
                if (allCompletions)
                {
                    resultJsonArray = [[NSMutableArray alloc] initWithCapacity:result.count];
                    for (YCSFetchContentCategoryResult *categoryResult in result)
                    {
                        NSMutableDictionary *categoryResultJsonDict = [categoryResult JSONDictionary];
                        [resultJsonArray addObject:categoryResultJsonDict];
                    }
                }
                else
                    resultJsonArray = [[NSMutableArray alloc] initWithCapacity:0];

                [self resolveOrReject:resolve reject:reject data:resultJsonArray error:error];
            }
        ];
    });
}

RCT_EXPORT_METHOD(fetchContentChildren:(NSDictionary *)content cancellationTicket:(NSString *)cancellationTicketId resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject)
{
    dispatch_async(_queueMain,
    ^{
        if (!content)
        {
            [self rejectWithInvalidArgument:reject paramName:NameOf(content)];
            return;
        }

        YCSRCancellationState *cancellationState = [self registerCancellation:cancellationTicketId];

        YCSStreamingContent *streamingContent = [[YCSStreamingContent alloc] initWithJSONDictionary:content];
        [self.ycsConnect fetchContentChildren:streamingContent cancellationTicket:cancellationState.ticket completionHandler:^(YCSStreamingContent *content, NSArray<YCSStreamingContent *> *children, NSError *error)
        {
            [self unregisterCancellation:cancellationState];

            NSMutableArray *resultJsonArray = [[NSMutableArray alloc] initWithCapacity:children.count];
            for (YCSStreamingContent *child in children)
            {
                NSMutableDictionary *childJsonDict = [child JSONDictionary];
                [resultJsonArray addObject:childJsonDict];
            }

            [self resolveOrReject:resolve reject:reject data:resultJsonArray error:error];
        }];
    });
}

RCT_EXPORT_METHOD(fetchContentDetails:(NSDictionary *)content cancellationTicket:(NSString *)cancellationTicketId resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject)
{
    dispatch_async(_queueMain,
    ^{
        if (!content)
        {
            [self rejectWithInvalidArgument:reject paramName:NameOf(content)];
            return;
        }

        YCSRCancellationState *cancellationState = [self registerCancellation:cancellationTicketId];

        YCSStreamingContent *streamingContent = [[YCSStreamingContent alloc] initWithJSONDictionary:content];
        [self.ycsConnect fetchContentDetails:streamingContent cancellationTicket:cancellationState.ticket completionHandler:^(YCSStreamingContent *content, YCSStreamingContentDetails *details, NSError *error)
        {
            [self unregisterCancellation:cancellationState];

            NSDictionary *detailsJsonDict = [details JSONDictionary];
            [self resolveOrReject:resolve reject:reject data:detailsJsonDict error:error];
        }];
    });
}

RCT_EXPORT_METHOD(searchContent:(NSString *)requestId services:(NSArray<NSDictionary *> *)services searchPhrase:(NSString *)searchPhrase cancellationTicket:(NSString *)cancellationTicketId serviceCompletions:(BOOL)serviceCompletions allCompletions:(BOOL)allCompletions resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject)
{
    dispatch_async(_queueMain,
    ^{
        YCSRCancellationState *cancellationState = [self registerCancellation:cancellationTicketId];

        NSMutableArray<YCSStreamingService *> *streamingServices = [[NSMutableArray<YCSStreamingService *> alloc] initWithCapacity:services.count];
        for (id service in services)
            [streamingServices addObject:[[YCSStreamingService alloc] initWithJSONDictionary:service]];

        [self.ycsConnect
            searchContent:streamingServices
            searchPhrase:searchPhrase
            cancellationTicket:cancellationState.ticket
            serviceCompletionHandler:^(YCSStreamingService *service, NSArray<YCSStreamingContent *> *results, NSError *error)
            {
                if (serviceCompletions)
                {
                    NSDictionary *errorJsonDict = error ? [error JSONDictionary] : (NSDictionary *)[NSNull null];
                    NSDictionary *serviceJsonDict = [service JSONDictionary];

                    NSMutableArray *resultsJsonArray = [[NSMutableArray alloc] initWithCapacity:results.count];
                    for (YCSStreamingContent *result in results)
                    {
                        NSMutableDictionary *resultJsonDict = [result JSONDictionary];
                        [resultsJsonArray addObject:resultJsonDict];
                    }

                    NSDictionary *eventJsonDict = @
                    {
                        EventSearchContentServiceCompleteParam_RequestID: requestId,
                        EventSearchContentServiceCompleteParam_Error: errorJsonDict,
                        EventSearchContentServiceCompleteParam_Service: serviceJsonDict,
                        EventSearchContentServiceCompleteParam_Content: resultsJsonArray
                    };

                    [self sendEventName:EventSearchContentServiceCompleteName body:eventJsonDict];
                }
            }
            allCompletionHandler:^(NSArray<YCSSearchContentServiceResult *> *results, NSError *error)
            {
                [self unregisterCancellation:cancellationState];

                NSMutableArray *resultsJsonArray;
                if (allCompletions)
                {
                    resultsJsonArray = [[NSMutableArray alloc] initWithCapacity:results.count];
                    for (YCSSearchContentServiceResult *serviceResult in results)
                    {
                        NSMutableDictionary *serviceResultJsonDict = [serviceResult JSONDictionary];
                        [resultsJsonArray addObject:serviceResultJsonDict];
                    }
                }
                else
                    resultsJsonArray = [[NSMutableArray alloc] initWithCapacity:0];

                [self resolveOrReject:resolve reject:reject data:resultsJsonArray error:error];
            }
        ];
    });
}

RCT_EXPORT_METHOD(fetchImage:(NSString *)url cancellationTicket:(NSString *)cancellationTicketId resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject)
{
    dispatch_async(_queueMain,
    ^{
        YCSRCancellationState *cancellationState = [self registerCancellation:cancellationTicketId];

        [self.ycsConnect fetchImage:url cancellationTicket:cancellationState.ticket completionHandler:^(NSData *image, NSError *error)
        {
            [self unregisterCancellation:cancellationState];

            dispatch_async(_queueWorker,
            ^{
                NSString *base64 = [image base64EncodedStringWithOptions:NSDataBase64EncodingEndLineWithLineFeed];
                [self resolveOrReject:resolve reject:reject data:base64 error:error];
            });
        }];
    });
}

RCT_EXPORT_METHOD(cancel:(NSString *)ticketId resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject)
{
    dispatch_async(_queueMain,
    ^{
        YCSRCancellationState *cancellationState = self.cancellationMap[ticketId];
        if (cancellationState)
            [cancellationState.ticket cancel];

        [self resolveOrReject:resolve reject:reject data:nil error:nil];
    });
}

@end
