/**
 * Copyright (c) 2016-present Invertase Limited & Contributors
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this library except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 */

#import "NotifeeApiModule.h"
#import <React/RCTUtils.h>
#import <UIKit/UIKit.h>
#import "../NotifeeCore/NotifeeCore+UNUserNotificationCenter.h"
#import "../NotifeeCore/NotifeeCore.h"

@interface NotifeeApiModule () <NotifeeCoreDelegate>
@end

@interface NotifeeCoreUNUserNotificationCenter (Rechain)
- (void)rechainUserNotificationCenterDelegate;
@end

static NSString *kReactNativeNotifeeNotificationEvent = @"app.notifee.notification-event";
static NSString *kReactNativeNotifeeNotificationBackgroundEvent =
    @"app.notifee.notification-event-background";

static NSInteger kReactNativeNotifeeNotificationTypeDisplayed = 1;
static NSInteger kReactNativeNotifeeNotificationTypeTrigger = 2;
static NSInteger kReactNativeNotifeeNotificationTypeAll = 0;

@implementation NotifeeApiModule {
  // Guarded by @synchronized(self): didReceiveNotifeeCoreEvent: may be called
  // from arbitrary threads (UNUserNotificationCenter callbacks), while
  // startObserving/stopObserving/sendNotifeeCoreEvent: run on the main thread.
  bool hasListeners;
  NSMutableArray *pendingCoreEvents;
}

#pragma mark - Module Setup

RCT_EXPORT_MODULE();

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

- (id)init {
  if (self = [super init]) {
    pendingCoreEvents = [[NSMutableArray alloc] init];
    [NotifeeCore setCoreDelegate:self];
  }
  return self;
}

- (NSArray<NSString *> *)supportedEvents {
  return @[ kReactNativeNotifeeNotificationEvent, kReactNativeNotifeeNotificationBackgroundEvent ];
}

- (void)startObserving {
  [[NotifeeCoreUNUserNotificationCenter instance] rechainUserNotificationCenterDelegate];

  NSArray *eventsToFlush;
  @synchronized(self) {
    hasListeners = YES;
    eventsToFlush = [pendingCoreEvents copy];
    [pendingCoreEvents removeAllObjects];
  }
  for (NSDictionary *eventBody in eventsToFlush) {
    [self sendNotifeeCoreEvent:eventBody];
  }
}

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

+ (BOOL)requiresMainQueueSetup {
  return YES;
}

- (std::shared_ptr<facebook::react::TurboModule>)getTurboModule:
    (const facebook::react::ObjCTurboModule::InitParams &)params {
  return std::make_shared<facebook::react::NativeNotifeeModuleSpecJSI>(params);
}

- (NSDictionary *)getConstants {
  return @{@"ANDROID_API_LEVEL" : @0};
}

#pragma mark - Events

- (void)didReceiveNotifeeCoreEvent:(NSDictionary *_Nonnull)event {
  BOOL shouldSend;
  @synchronized(self) {
    shouldSend = hasListeners;
    if (!shouldSend) {
      [pendingCoreEvents addObject:event];
    }
  }
  if (shouldSend) {
    [self sendNotifeeCoreEvent:event];
  }
}

- (void)sendNotifeeCoreEvent:(NSDictionary *_Nonnull)eventBody {
  dispatch_async(dispatch_get_main_queue(), ^{
    @synchronized(self) {
      if (!self->hasListeners) {
        [self->pendingCoreEvents addObject:eventBody];
        return;
      }
    }
    // Routing foreground vs background is based on UIApplication state.
    // iOS delivers didReceiveNotificationResponse: while the app is in
    // Inactive state during a background tap transition, not Background —
    // so we check != Active rather than == Background.
    //
    // Known limitation: this also routes non-tap events (DELIVERED,
    // TRIGGER_NOTIFICATION_CREATED, DISMISSED) to the background channel
    // if they happen to be emitted while the app is in Inactive state for
    // unrelated reasons (Control Center open, incoming call, etc.). In
    // practice this is rare because DELIVERED originates from
    // willPresentNotification: (only called when app is Active) and the
    // API-triggered events require an interactive JS context. If this
    // becomes a real-world problem, the routing should be split by event
    // type rather than by state alone.
    BOOL isBackground =
        RCTRunningInAppExtension() ||
        [UIApplication sharedApplication].applicationState != UIApplicationStateActive;
    if (isBackground) {
      [self sendEventWithName:kReactNativeNotifeeNotificationBackgroundEvent body:eventBody];
    } else {
      [self sendEventWithName:kReactNativeNotifeeNotificationEvent body:eventBody];
    }
  });
}

// clang-format off

#pragma mark - Shared Methods

- (void)cancelAllNotifications:(RCTPromiseResolveBlock)resolve
                        reject:(RCTPromiseRejectBlock)reject {
  [NotifeeCore cancelAllNotifications:kReactNativeNotifeeNotificationTypeAll withBlock:^(NSError *_Nullable error) {
    [self resolve:resolve orReject:reject promiseWithError:error orResult:nil];
  }];
}

- (void)cancelDisplayedNotifications:(RCTPromiseResolveBlock)resolve
                              reject:(RCTPromiseRejectBlock)reject {
  [NotifeeCore cancelAllNotifications:kReactNativeNotifeeNotificationTypeDisplayed withBlock:^(NSError *_Nullable error) {
    [self resolve:resolve orReject:reject promiseWithError:error orResult:nil];
  }];
}

- (void)cancelTriggerNotifications:(RCTPromiseResolveBlock)resolve
                            reject:(RCTPromiseRejectBlock)reject {
  [NotifeeCore cancelAllNotifications:kReactNativeNotifeeNotificationTypeTrigger withBlock:^(NSError *_Nullable error) {
    [self resolve:resolve orReject:reject promiseWithError:error orResult:nil];
  }];
}

- (void)cancelAllNotificationsWithIds:(NSArray *)ids
                     notificationType:(double)notificationType
                                  tag:(NSString *_Nullable)tag
                              resolve:(RCTPromiseResolveBlock)resolve
                               reject:(RCTPromiseRejectBlock)reject {
  // tag is Android-only, ignored on iOS
  [NotifeeCore cancelAllNotificationsWithIds:(NSInteger)notificationType withIds:ids withBlock:^(NSError *_Nullable error) {
    [self resolve:resolve orReject:reject promiseWithError:error orResult:nil];
  }];
}

- (void)getDisplayedNotifications:(RCTPromiseResolveBlock)resolve
                           reject:(RCTPromiseRejectBlock)reject {
  [NotifeeCore getDisplayedNotifications:^(NSError *_Nullable error, NSArray<NSDictionary *> *notifications) {
    [self resolve:resolve orReject:reject promiseWithError:error orResult:notifications];
  }];
}

- (void)getTriggerNotifications:(RCTPromiseResolveBlock)resolve
                         reject:(RCTPromiseRejectBlock)reject {
  [NotifeeCore getTriggerNotifications:^(NSError *_Nullable error, NSArray<NSDictionary *> *notifications) {
    [self resolve:resolve orReject:reject promiseWithError:error orResult:notifications];
  }];
}

- (void)getTriggerNotificationIds:(RCTPromiseResolveBlock)resolve
                           reject:(RCTPromiseRejectBlock)reject {
  [NotifeeCore getTriggerNotificationIds:^(NSError *_Nullable error, NSArray<NSDictionary *> *notifications) {
    [self resolve:resolve orReject:reject promiseWithError:error orResult:notifications];
  }];
}

- (void)displayNotification:(NSDictionary *)notification
                    resolve:(RCTPromiseResolveBlock)resolve
                     reject:(RCTPromiseRejectBlock)reject {
  [NotifeeCore displayNotification:notification withBlock:^(NSError *_Nullable error) {
    [self resolve:resolve orReject:reject promiseWithError:error orResult:nil];
  }];
}

- (void)createTriggerNotification:(NSDictionary *)notification
                          trigger:(NSDictionary *)trigger
                          resolve:(RCTPromiseResolveBlock)resolve
                           reject:(RCTPromiseRejectBlock)reject {
  [NotifeeCore createTriggerNotification:notification withTrigger:trigger withBlock:^(NSError *_Nullable error) {
    [self resolve:resolve orReject:reject promiseWithError:error orResult:nil];
  }];
}

- (void)requestPermission:(NSDictionary *)permissions
                  resolve:(RCTPromiseResolveBlock)resolve
                   reject:(RCTPromiseRejectBlock)reject {
  [NotifeeCore requestPermission:permissions withBlock:^(NSError *_Nullable error, NSDictionary *settings) {
    [self resolve:resolve orReject:reject promiseWithError:error orResult:settings];
  }];
}

- (void)getNotificationSettings:(RCTPromiseResolveBlock)resolve
                         reject:(RCTPromiseRejectBlock)reject {
  [NotifeeCore getNotificationSettings:^(NSError *_Nullable error, NSDictionary *settings) {
    [self resolve:resolve orReject:reject promiseWithError:error orResult:settings];
  }];
}

- (void)getInitialNotification:(RCTPromiseResolveBlock)resolve
                        reject:(RCTPromiseRejectBlock)reject {
  [NotifeeCore getInitialNotification:^(NSError *_Nullable error, NSDictionary *notification) {
    [self resolve:resolve orReject:reject promiseWithError:error orResult:notification];
  }];
}

#pragma mark - iOS-only Methods

- (void)cancelNotification:(NSString *)notificationId
                   resolve:(RCTPromiseResolveBlock)resolve
                    reject:(RCTPromiseRejectBlock)reject {
  [NotifeeCore cancelNotification:notificationId withNotificationType:kReactNativeNotifeeNotificationTypeAll withBlock:^(NSError *_Nullable error) {
    [self resolve:resolve orReject:reject promiseWithError:error orResult:nil];
  }];
}

- (void)cancelDisplayedNotification:(NSString *)notificationId
                            resolve:(RCTPromiseResolveBlock)resolve
                             reject:(RCTPromiseRejectBlock)reject {
  [NotifeeCore cancelNotification:notificationId withNotificationType:kReactNativeNotifeeNotificationTypeDisplayed withBlock:^(NSError *_Nullable error) {
    [self resolve:resolve orReject:reject promiseWithError:error orResult:nil];
  }];
}

- (void)cancelTriggerNotification:(NSString *)notificationId
                          resolve:(RCTPromiseResolveBlock)resolve
                           reject:(RCTPromiseRejectBlock)reject {
  [NotifeeCore cancelNotification:notificationId withNotificationType:kReactNativeNotifeeNotificationTypeTrigger withBlock:^(NSError *_Nullable error) {
    [self resolve:resolve orReject:reject promiseWithError:error orResult:nil];
  }];
}

- (void)cancelDisplayedNotificationsWithIds:(NSArray *)ids
                                    resolve:(RCTPromiseResolveBlock)resolve
                                     reject:(RCTPromiseRejectBlock)reject {
  [NotifeeCore cancelAllNotificationsWithIds:kReactNativeNotifeeNotificationTypeDisplayed withIds:ids withBlock:^(NSError *_Nullable error) {
    [self resolve:resolve orReject:reject promiseWithError:error orResult:nil];
  }];
}

- (void)cancelTriggerNotificationsWithIds:(NSArray *)ids
                                  resolve:(RCTPromiseResolveBlock)resolve
                                   reject:(RCTPromiseRejectBlock)reject {
  [NotifeeCore cancelAllNotificationsWithIds:kReactNativeNotifeeNotificationTypeTrigger withIds:ids withBlock:^(NSError *_Nullable error) {
    [self resolve:resolve orReject:reject promiseWithError:error orResult:nil];
  }];
}

- (void)getNotificationCategories:(RCTPromiseResolveBlock)resolve
                           reject:(RCTPromiseRejectBlock)reject {
  [NotifeeCore getNotificationCategories:^(NSError *_Nullable error, NSArray<NSDictionary *> *categories) {
    [self resolve:resolve orReject:reject promiseWithError:error orResult:categories];
  }];
}

- (void)setNotificationCategories:(NSArray *)categories
                          resolve:(RCTPromiseResolveBlock)resolve
                           reject:(RCTPromiseRejectBlock)reject {
  [NotifeeCore setNotificationCategories:categories withBlock:^(NSError *_Nullable error) {
    [self resolve:resolve orReject:reject promiseWithError:error orResult:nil];
  }];
}

- (void)setBadgeCount:(double)count
              resolve:(RCTPromiseResolveBlock)resolve
               reject:(RCTPromiseRejectBlock)reject {
  [NotifeeCore setBadgeCount:(NSInteger)count withBlock:^(NSError *_Nullable error) {
    [self resolve:resolve orReject:reject promiseWithError:error orResult:nil];
  }];
}

- (void)getBadgeCount:(RCTPromiseResolveBlock)resolve
               reject:(RCTPromiseRejectBlock)reject {
  [NotifeeCore getBadgeCount:^(NSError *_Nullable error, NSInteger count) {
    [self resolve:resolve orReject:reject promiseWithError:error orResult:@(count)];
  }];
}

- (void)incrementBadgeCount:(double)incrementBy
                    resolve:(RCTPromiseResolveBlock)resolve
                     reject:(RCTPromiseRejectBlock)reject {
  [NotifeeCore incrementBadgeCount:(NSInteger)incrementBy withBlock:^(NSError *_Nullable error) {
    [self resolve:resolve orReject:reject promiseWithError:error orResult:nil];
  }];
}

- (void)decrementBadgeCount:(double)decrementBy
                    resolve:(RCTPromiseResolveBlock)resolve
                     reject:(RCTPromiseRejectBlock)reject {
  [NotifeeCore decrementBadgeCount:(NSInteger)decrementBy withBlock:^(NSError *_Nullable error) {
    [self resolve:resolve orReject:reject promiseWithError:error orResult:nil];
  }];
}

- (void)setNotificationConfig:(NSDictionary *)config
                      resolve:(RCTPromiseResolveBlock)resolve
                       reject:(RCTPromiseRejectBlock)reject {
  [NotifeeCore setNotificationConfig:config withBlock:^(NSError *_Nullable error) {
    [self resolve:resolve orReject:reject promiseWithError:error orResult:nil];
  }];
}

#pragma mark - Android-only stubs (required by NativeNotifeeModuleSpec)

- (void)createChannel:(NSDictionary *)channelMap
              resolve:(RCTPromiseResolveBlock)resolve
               reject:(RCTPromiseRejectBlock)reject {
  resolve(nil);
}

- (void)createChannels:(NSArray *)channelsArray
               resolve:(RCTPromiseResolveBlock)resolve
                reject:(RCTPromiseRejectBlock)reject {
  resolve(nil);
}

- (void)createChannelGroup:(NSDictionary *)channelGroupMap
                   resolve:(RCTPromiseResolveBlock)resolve
                    reject:(RCTPromiseRejectBlock)reject {
  resolve(nil);
}

- (void)createChannelGroups:(NSArray *)channelGroupsArray
                    resolve:(RCTPromiseResolveBlock)resolve
                     reject:(RCTPromiseRejectBlock)reject {
  resolve(nil);
}

- (void)deleteChannel:(NSString *)channelId
              resolve:(RCTPromiseResolveBlock)resolve
               reject:(RCTPromiseRejectBlock)reject {
  resolve(nil);
}

- (void)deleteChannelGroup:(NSString *)channelGroupId
                   resolve:(RCTPromiseResolveBlock)resolve
                    reject:(RCTPromiseRejectBlock)reject {
  resolve(nil);
}

- (void)getChannel:(NSString *)channelId
           resolve:(RCTPromiseResolveBlock)resolve
            reject:(RCTPromiseRejectBlock)reject {
  resolve(nil);
}

- (void)getChannels:(RCTPromiseResolveBlock)resolve
             reject:(RCTPromiseRejectBlock)reject {
  resolve(nil);
}

- (void)getChannelGroup:(NSString *)channelGroupId
                resolve:(RCTPromiseResolveBlock)resolve
                 reject:(RCTPromiseRejectBlock)reject {
  resolve(nil);
}

- (void)getChannelGroups:(RCTPromiseResolveBlock)resolve
                  reject:(RCTPromiseRejectBlock)reject {
  resolve(nil);
}

- (void)isChannelCreated:(NSString *)channelId
                 resolve:(RCTPromiseResolveBlock)resolve
                  reject:(RCTPromiseRejectBlock)reject {
  resolve(@(NO));
}

- (void)isChannelBlocked:(NSString *)channelId
                 resolve:(RCTPromiseResolveBlock)resolve
                  reject:(RCTPromiseRejectBlock)reject {
  resolve(@(NO));
}

- (void)openAlarmPermissionSettings:(RCTPromiseResolveBlock)resolve
                             reject:(RCTPromiseRejectBlock)reject {
  resolve(nil);
}

- (void)openNotificationSettings:(NSString *_Nullable)channelId
                         resolve:(RCTPromiseResolveBlock)resolve
                          reject:(RCTPromiseRejectBlock)reject {
  resolve(nil);
}

- (void)openBatteryOptimizationSettings:(RCTPromiseResolveBlock)resolve
                                 reject:(RCTPromiseRejectBlock)reject {
  resolve(nil);
}

- (void)isBatteryOptimizationEnabled:(RCTPromiseResolveBlock)resolve
                              reject:(RCTPromiseRejectBlock)reject {
  resolve(@(NO));
}

- (void)getPowerManagerInfo:(RCTPromiseResolveBlock)resolve
                     reject:(RCTPromiseRejectBlock)reject {
  resolve(@{});
}

- (void)openPowerManagerSettings:(RCTPromiseResolveBlock)resolve
                          reject:(RCTPromiseRejectBlock)reject {
  resolve(nil);
}

- (void)stopForegroundService:(RCTPromiseResolveBlock)resolve
                       reject:(RCTPromiseRejectBlock)reject {
  resolve(nil);
}

- (void)prewarmForegroundService:(RCTPromiseResolveBlock)resolve
                          reject:(RCTPromiseRejectBlock)reject {
  resolve(nil);
}

- (void)hideNotificationDrawer {
  // Android-only, no-op on iOS
}

- (void)addListener:(NSString *)eventName {
  [super addListener:eventName];
}

- (void)removeListeners:(double)count {
  [super removeListeners:count];
}

// clang-format on

#pragma mark - Internals

- (void)resolve:(RCTPromiseResolveBlock)resolve
            orReject:(RCTPromiseRejectBlock)reject
    promiseWithError:(NSError *_Nullable)error
            orResult:(id _Nullable)result {
  if (error != nil) {
    reject(@"unknown", error.localizedDescription, error);
  } else {
    resolve(result);
  }
}

@end
