#import "WalletCore.h"
#import "installer.hpp"
#import "logs.h"
#import "JSIUtils.hpp"
#import <Security/Security.h>
#import <CoreFoundation/CFBundle.h>
#import <LocalAuthentication/LocalAuthentication.h>
#import <React/RCTBridge+Private.h>
#import <jsi/jsi.h>
#import <React/RCTUtils.h>
#import <ReactCommon/CallInvoker.h>

@implementation WalletCore

@synthesize bridge = _bridge;
@synthesize methodQueue = _methodQueue;

BiometricsState getBiometricsState()
{
  LAContext *laContext = [[LAContext alloc] init];
  NSError *authError = nil;

  if ([laContext canEvaluatePolicy:LAPolicyDeviceOwnerAuthenticationWithBiometrics error:&authError]) {
    return biometricsStateAvailable;
  } else {
    if (authError.code == LAErrorBiometryLockout) {
      return biometricsStateLocked;
    } else {
      return biometricsStateNotAvailable;
    }
  }
}

SecAccessControlRef getBioSecAccessControl()
{

  return SecAccessControlCreateWithFlags(nil, // default allocator
                                         kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly, // Require passcode to be set on device
                                         kSecAccessControlUserPresence, // checks for user presence with touchID, faceID or passcode :)
                                         nil); // Error pointer
}

bool commitSeed(std::string seed, bool useBiometrics, std::string title, std::string subtitle)
{
  CFBundleRef bundle = CFBundleGetMainBundle();
  CFStringRef bundleIdentifier = CFBundleGetIdentifier(bundle);
  std::vector<uint8_t> vec(seed.begin(), seed.end());

  int size = useBiometrics ? 5 : 4;

  CFStringRef keys[size];
  keys[0] = kSecClass;
  keys[1] = kSecAttrAccount;
  keys[2] = kSecAttrService;
  keys[3] = kSecValueData;
  if(useBiometrics) {
    keys[4] = kSecAttrAccessControl;
  }


  CFTypeRef values[size];
  values[0] = kSecClassGenericPassword;
  values[1] = CFSTR("seed");
  values[2] = bundleIdentifier;
  values[3] = CFDataCreate(kCFAllocatorDefault, &vec[0], vec.size());
  if(useBiometrics) {
    values[4] = getBioSecAccessControl();
  }

  CFDictionaryRef query = CFDictionaryCreate(kCFAllocatorDefault,
                                             (const void **)keys,
                                             (const void **)values,
                                             size,
                                             &kCFTypeDictionaryKeyCallBacks,
                                             &kCFTypeDictionaryValueCallBacks);

  clearSeed(useBiometrics, title, subtitle);

  OSStatus status = SecItemAdd(query, NULL);

  if (status != errSecSuccess) {
    LOGW("Did not save entropy seed %d", status);
    return false;
  }

  return true;
}


StringOptional getSeed(bool useBiometrics, std::string title, std::string subtitle)
{
  if(useBiometrics) {
    BiometricsState biometricsState = getBiometricsState();
    LAContext *authContext = [[LAContext alloc] init];

    // If device has no passcode/faceID/touchID then wallet-core cannot read the value from memory
    if(biometricsState == biometricsStateNotAvailable) {
      return StringOptional {
        .hasValue = false
      };
    }

    if(biometricsState == biometricsStateLocked) {

      // TODO receiving a localized string might be necessary if this is happening on production
      [authContext evaluatePolicy:LAPolicyDeviceOwnerAuthenticationWithBiometrics
                  localizedReason:@"Please unlock the device to access saved wallet"
                            reply:^(BOOL success, NSError *error) {
        if (!success) {
          // Edge case when device might locked out after too many password attempts
          // User has failed to authenticate, but due to this being a callback cannot interrupt upper lexical scope
          // We should somehow prompt/tell the user that it has failed to authenticate
          // and wallet could not be loaded
        }
      }];
    }
  }

  CFBundleRef bundle = CFBundleGetMainBundle();
  CFStringRef bundleIdentifier = CFBundleGetIdentifier(bundle);

  int size = useBiometrics ? 6 : 5;

  CFStringRef keys[size];
  keys[0] = kSecClass;
  keys[1] = kSecAttrAccount;
  keys[2] = kSecAttrService;
  keys[3] = kSecReturnAttributes;
  keys[4] = kSecReturnData;
  if(useBiometrics) {
    keys[5] = kSecAttrAccessControl;
  }

  CFTypeRef values[size];
  values[0] = kSecClassGenericPassword;
  values[1] = CFSTR("seed");
  values[2] = bundleIdentifier;
  values[3] = kCFBooleanFalse;
  values[4] = kCFBooleanTrue;
  if(useBiometrics) {
    values[5] = getBioSecAccessControl();  
  }

  CFDictionaryRef query = CFDictionaryCreate(kCFAllocatorDefault,
                                             (const void **)keys,
                                             (const void **)values,
                                             size,
                                             &kCFTypeDictionaryKeyCallBacks,
                                             &kCFTypeDictionaryValueCallBacks);

  CFTypeRef foundTypeRef = NULL;
  OSStatus status = SecItemCopyMatching(query, &foundTypeRef);

  if (status != errSecSuccess) {
    return StringOptional {
      .hasValue = false
    };
  }

  CFDataRef found = (CFDataRef)(foundTypeRef);

  CFStringRef stringRef = CFStringCreateFromExternalRepresentation(kCFAllocatorDefault, found, kCFStringEncodingUTF8);
  const char *value = CFStringGetCStringPtr(stringRef, kCFStringEncodingUTF8);

  return StringOptional {
    .hasValue = true,
    .value = string(value)
  };
}

// on iOS biometrics check is not needed to delete data
// but the signature is shared with the android implementation, so the param needs to be here
bool clearSeed(bool useBiometrics, std::string title, std::string subtitle)
{
  CFBundleRef bundle = CFBundleGetMainBundle();
  CFStringRef bundleIdentifier = CFBundleGetIdentifier(bundle);

  CFStringRef keys[5];
  keys[0] = kSecClass;
  keys[1] = kSecAttrService;
  keys[2] = kSecAttrAccount;
  keys[3] = kSecReturnAttributes;
  keys[4] = kSecReturnData;

  CFTypeRef values[5];
  values[0] = kSecClassGenericPassword;
  values[1] = bundleIdentifier;
  values[2] = CFSTR("seed");
  values[3] = kCFBooleanFalse;
  values[4] = kCFBooleanFalse;

  CFDictionaryRef query = CFDictionaryCreate(kCFAllocatorDefault,
                                             (const void **)keys,
                                             (const void **)values,
                                             5,
                                             &kCFTypeDictionaryKeyCallBacks,
                                             &kCFTypeDictionaryValueCallBacks);

  OSStatus status = SecItemDelete(query);

  if (status != errSecSuccess) {
    return false;
  }

  return true;
}

RCT_EXPORT_MODULE()

+ (BOOL)requiresMainQueueSetup {
  return YES;
}

- (void)setBridge:(RCTBridge *)bridge {
  _bridge = bridge;
  _setBridgeOnMainQueue = RCTIsMainQueue();

  RCTCxxBridge *cxxBridge = (RCTCxxBridge *)self.bridge;

  if (!cxxBridge.runtime) {
    return;
  }

  walletcore::install(*(facebook::jsi::Runtime *)cxxBridge.runtime,
                      &getSeed,
                      &clearSeed,
                      &commitSeed,
                      &getBiometricsState);
}

@end
