#import "RNInsiderAppFrameView.h"

// The codegen headers below exist only under the New Architecture. The legacy architecture is
// the default on React Native 0.74-0.75 and still selectable on 0.76-0.81 (removed in 0.82), all
// in the supported range, and this pod must still compile there.
#ifdef RCT_NEW_ARCH_ENABLED


#import <react/renderer/components/RNInsiderSpec/ComponentDescriptors.h>
#import <react/renderer/components/RNInsiderSpec/EventEmitters.h>
#import <react/renderer/components/RNInsiderSpec/Props.h>
#import <react/renderer/components/RNInsiderSpec/RCTComponentViewHelpers.h>

#import <React/RCTConversions.h>

// Diagnostics go through +[Insider sendError:desc:], like the rest of the bridge, rather than
// NSLog into a host app console the integrator cannot switch off.
#import <InsiderMobile/Insider.h>

// InsiderMobile releases before 16.0.0 have no App Frames headers. Codegen registers
// "InsiderAppFrameView" in every app regardless of the linked SDK, so InsiderAppFrameViewCls()
// must still resolve without them, or the app fails to link.
#if __has_include(<InsiderMobile/InsiderAppFramesView.h>)
#define INSIDER_APP_FRAMES_AVAILABLE 1
#import <InsiderMobile/InsiderAppFramesView.h>
#import <InsiderMobile/InsiderAppFramesError.h>
#else
#define INSIDER_APP_FRAMES_AVAILABLE 0
#endif

using namespace facebook::react;

#if INSIDER_APP_FRAMES_AVAILABLE

// Matches NO_DISMISS_CODE in src/InsiderAppFramesError.js.
static const NSInteger kInsiderAppFramesNoDismissCode = -1;

static NSString *RNInsiderAppFramesErrorCodeName(InsiderAppFramesErrorCode code) {
  switch (code) {
    case InsiderAppFramesErrorCodeResolutionFailed:
      return @"RESOLUTION_FAILED";
    case InsiderAppFramesErrorCodeResponseMalformed:
      return @"RESPONSE_MALFORMED";
    case InsiderAppFramesErrorCodeDownloadingFailed:
      return @"DOWNLOADING_FAILED";
    case InsiderAppFramesErrorCodePlacementUntrusted:
      return @"PLACEMENT_UNTRUSTED";
    case InsiderAppFramesErrorCodeContentDisplayFailed:
      return @"CONTENT_DISPLAY_FAILED";
    case InsiderAppFramesErrorCodeRenderingFailed:
      return @"RENDERING_FAILED";
    case InsiderAppFramesErrorCodeUnknown:
      return @"UNKNOWN";
  }

  // No `default:`, and the parameter is the enum type rather than NSInteger, so a code added
  // to the SDK trips -Wswitch instead of silently mapping to UNKNOWN.
  return @"UNKNOWN";
}

// JS compares against its own UPPER_SNAKE enum (src/InsiderAppFrameStatus.js), so the SDK's
// lowerCamelCase `stringValue` would match nothing, silently.
static NSString *RNInsiderAppFramesStatusName(InsiderAppFramesViewStatus status) {
  switch (status) {
    case InsiderAppFramesViewStatusDetached:
      return @"DETACHED";
    case InsiderAppFramesViewStatusNoPlacement:
      return @"NO_PLACEMENT";
    case InsiderAppFramesViewStatusResolving:
      return @"RESOLVING";
    case InsiderAppFramesViewStatusDownloading:
      return @"DOWNLOADING";
    case InsiderAppFramesViewStatusRendering:
      return @"RENDERING";
    case InsiderAppFramesViewStatusUnavailable:
      return @"UNAVAILABLE";
    case InsiderAppFramesViewStatusReady:
      return @"READY";
    case InsiderAppFramesViewStatusDismissed:
      return @"DISMISSED";
    case InsiderAppFramesViewStatusErrorResolving:
      return @"ERROR_RESOLVING";
    case InsiderAppFramesViewStatusErrorDownloading:
      return @"ERROR_DOWNLOADING";
    case InsiderAppFramesViewStatusErrorRendering:
      return @"ERROR_RENDERING";
    case InsiderAppFramesViewStatusDisabled:
      return @"DISABLED";
  }

  // Reachable only for an out-of-enum runtime value. UNRECOGNIZED is a declared member of the
  // JS status union; a real status such as DETACHED would tell JS something specific and wrong.
  [Insider sendError:[NSException exceptionWithName:@"InsiderAppFrameUnrecognisedStatus"
                                             reason:[NSString stringWithFormat:
                                                     @"Unrecognised InsiderAppFramesViewStatus: %ld",
                                                     (long)status]
                                           userInfo:nil]
                desc:[NSString stringWithFormat:@"%s:%d", __func__, __LINE__]];
  return @"UNRECOGNIZED";
}

// Test seam; the delegate callbacks call the static mapper directly.
NSString *RNInsiderAppFramesStatusNameForRawStatus(NSInteger status) {
  return RNInsiderAppFramesStatusName((InsiderAppFramesViewStatus)status);
}

@interface RNInsiderAppFrameView () <InsiderAppFramesViewDelegate>
@end

static SharedViewProps RNInsiderAppFrameViewDefaultProps() {
  // SharedViewProps, not Props::Shared: `_props` is shared_ptr<const ViewProps>, and a
  // shared_ptr<const Props> will not implicitly convert down to it.
  static const auto defaultProps = std::make_shared<const InsiderAppFrameViewProps>();
  return defaultProps;
}

@implementation RNInsiderAppFrameView {
  InsiderAppFramesView *_frameView;
}

+ (ComponentDescriptorProvider)componentDescriptorProvider {
  return concreteComponentDescriptorProvider<InsiderAppFrameViewComponentDescriptor>();
}

- (instancetype)initWithFrame:(CGRect)frame {
  if (self = [super initWithFrame:frame]) {
    _props = RNInsiderAppFrameViewDefaultProps();

    _frameView = [[InsiderAppFramesView alloc] initWithFrame:self.bounds];
    _frameView.delegate = self;
    self.contentView = _frameView;
  }
  return self;
}

- (void)updateProps:(Props::Shared const &)props oldProps:(Props::Shared const &)oldProps {
  const auto &newProps = *std::static_pointer_cast<InsiderAppFrameViewProps const>(props);
  const auto &previousProps = *std::static_pointer_cast<InsiderAppFrameViewProps const>(_props);

  if (newProps.placementId != previousProps.placementId) {
    NSString *placementId = RCTNSStringFromStringNilIfEmpty(newProps.placementId);
    _frameView.placementId = placementId;
  }

  [super updateProps:props oldProps:oldProps];
}

// A recycled view's next updateProps: diffs against `_props`, which RCTViewComponentView does
// not reset; without resetting it here, a view reused for the same placementId never applies it.
- (void)prepareForRecycle {
  [super prepareForRecycle];

  // Replace the SDK view rather than reset it. A recycled view is already off-window, so it sits
  // in DETACHED, which keeps its reported height; native suppresses a repeat of that height, so
  // the next occupant would never receive one and render into a zero-height box.
  _frameView.delegate = nil;
  [_frameView removeFromSuperview];

  _frameView = [[InsiderAppFramesView alloc] initWithFrame:self.bounds];
  _frameView.delegate = self;
  self.contentView = _frameView;

  _props = RNInsiderAppFrameViewDefaultProps();
}

- (std::shared_ptr<const InsiderAppFrameViewEventEmitter>)frameEventEmitter {
  if (!_eventEmitter) {
    // Not expected: the emitter is set at mount, before the view reaches a window. Reported
    // because JS mirrors status and error from these events, so a dropped one leaves it stale.
    [Insider sendError:[NSException exceptionWithName:@"InsiderAppFrameDroppedEvent"
                                               reason:@"Dropped an event: no event emitter attached yet."
                                             userInfo:nil]
                  desc:[NSString stringWithFormat:@"%s:%d", __func__, __LINE__]];
    return nullptr;
  }
  return std::static_pointer_cast<const InsiderAppFrameViewEventEmitter>(_eventEmitter);
}

- (std::string)currentPlacementId {
  return RCTStringFromNSString(_frameView.placementId ?: @"");
}

#pragma mark - InsiderAppFramesViewDelegate

- (void)appFramesView:(InsiderAppFramesView *)view
    didChangeStatusTo:(InsiderAppFramesViewStatus)status
                 from:(InsiderAppFramesViewStatus)previousStatus {
  auto emitter = [self frameEventEmitter];
  if (emitter) {
    emitter->onStatusChange({
        .placementId = [self currentPlacementId],
        .status = RCTStringFromNSString(RNInsiderAppFramesStatusName(status)),
        .previousStatus = RCTStringFromNSString(RNInsiderAppFramesStatusName(previousStatus)),
    });
  }
}

// The cast below would reinterpret a foreign-domain code as a real App Frames code.
NSString *RNInsiderAppFramesErrorCodeNameForError(NSError *error) {
  if (![error.domain isEqualToString:InsiderAppFramesErrorDomain]) {
    return @"UNKNOWN";
  }
  return RNInsiderAppFramesErrorCodeName((InsiderAppFramesErrorCode)error.code);
}

- (void)appFramesView:(InsiderAppFramesView *)view didFailLoadingWithError:(NSError *)error {
  auto emitter = [self frameEventEmitter];
  if (!emitter) {
    return;
  }

  // `message` is the canonical per-code text, as on Android; the underlying error goes in `cause`.
  NSError *underlying = error.userInfo[NSUnderlyingErrorKey];
  NSString *message = error.localizedDescription ?: @"";
  NSString *cause = underlying.localizedDescription ?: @"";

  // Set when a template dismisses with a non-default code. The SDK exports no constant for this
  // userInfo key.
  NSNumber *dismissCode = error.userInfo[@"dismiss_code"];

  NSString *codeName = RNInsiderAppFramesErrorCodeNameForError(error);

  emitter->onLoadError({
      .placementId = [self currentPlacementId],
      .code = RCTStringFromNSString(codeName),
      .message = RCTStringFromNSString(message),
      .cause = RCTStringFromNSString(cause),
      // The ternary's common type is NSInteger, which the Int32 braced initialiser rejects as
      // narrowing (-Wc++11-narrowing).
      .dismissCode = static_cast<int32_t>(
          dismissCode != nil ? dismissCode.integerValue : kInsiderAppFramesNoDismissCode),
  });
}

// The height is applied in JS: Fabric assigns frames without Auto Layout, so the native view's
// intrinsicContentSize is never consulted.
- (void)appFramesView:(InsiderAppFramesView *)view didRequestHeightChangeTo:(CGFloat)optimalHeight {
  auto emitter = [self frameEventEmitter];
  if (emitter) {
    emitter->onHeightChange({
        .placementId = [self currentPlacementId],
        .height = static_cast<double>(optimalHeight),
    });
  }
}

- (void)appFramesViewDidRequestDismiss:(InsiderAppFramesView *)view {
  auto emitter = [self frameEventEmitter];
  if (emitter) {
    emitter->onDismissRequest({.placementId = [self currentPlacementId]});
  }
}

- (void)appFramesView:(InsiderAppFramesView *)view
    didTriggerActionWithData:(NSDictionary<NSString *, id> *)actionData {
  auto emitter = [self frameEventEmitter];
  if (!emitter) {
    return;
  }

  NSString *json = nil;
  if ([NSJSONSerialization isValidJSONObject:actionData]) {
    NSData *data = [NSJSONSerialization dataWithJSONObject:actionData options:0 error:nil];
    if (data) {
      json = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
    }
  }
  if (!json) {
    // The payload is lost either way, since the codegen field is non-nullable; the report is what
    // makes the loss visible.
    [Insider sendError:[NSException exceptionWithName:@"InsiderAppFrameUnserializableAction"
                                               reason:@"Action data could not be serialized; forwarding {}."
                                             userInfo:nil]
                  desc:[NSString stringWithFormat:@"%s:%d", __func__, __LINE__]];
    json = @"{}";
  }

  emitter->onAction({
      .placementId = [self currentPlacementId],
      .data = RCTStringFromNSString(json),
  });
}

@end

#else // !INSIDER_APP_FRAMES_AVAILABLE

// The only signal for this configuration: the inert stub registers cleanly and nothing at runtime
// reports it. `#pragma message` rather than `#warning`, so a -Werror build does not break over it.
#pragma message("[Insider] App Frames is unavailable in the linked InsiderMobile, so InsiderAppFrameView compiles as an inert stub: it registers, renders nothing and emits no events. Update InsiderMobile to 16.0.0 or later.")

@implementation RNInsiderAppFrameView

+ (ComponentDescriptorProvider)componentDescriptorProvider {
  return concreteComponentDescriptorProvider<InsiderAppFrameViewComponentDescriptor>();
}

@end

// The header declares these unconditionally, so they are defined here to avoid a link error.
NSString *RNInsiderAppFramesErrorCodeNameForError(__unused NSError *error) {
  return @"UNKNOWN";
}

NSString *RNInsiderAppFramesStatusNameForRawStatus(__unused NSInteger status) {
  return @"UNRECOGNIZED";
}

#endif // INSIDER_APP_FRAMES_AVAILABLE

Class<RCTComponentViewProtocol> InsiderAppFrameViewCls(void) {
  return RNInsiderAppFrameView.class;
}

#endif // RCT_NEW_ARCH_ENABLED
