/*
 * Copyright 2019 Google
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file 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 <sqlite3.h>

#import "FirebaseRemoteConfig/Sources/RCNConfigDBManager.h"
#import "FirebaseRemoteConfig/Sources/RCNConfigDefines.h"
#import "FirebaseRemoteConfig/Sources/RCNConfigValue_Internal.h"

#import "FirebaseCore/Extension/FirebaseCoreInternal.h"

/// Using macro for securely preprocessing string concatenation in query before runtime.
#define RCNTableNameMain "main"
#define RCNTableNameMainActive "main_active"
#define RCNTableNameMainDefault "main_default"
#define RCNTableNameMetadataDeprecated "fetch_metadata"
#define RCNTableNameMetadata "fetch_metadata_v2"
#define RCNTableNameExperiment "experiment"
#define RCNTableNamePersonalization "personalization"
#define RCNTableNameRollout "rollout"

static BOOL gIsNewDatabase;
/// SQLite file name in versions 0, 1 and 2.
static NSString *const RCNDatabaseName = @"RemoteConfig.sqlite3";
/// The storage sub-directory that the Remote Config database resides in.
static NSString *const RCNRemoteConfigStorageSubDirectory = @"Google/RemoteConfig";

/// Remote Config database path for deprecated V0 version.
static NSString *RemoteConfigPathForOldDatabaseV0(void) {
  NSArray *dirPaths =
      NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
  NSString *docPath = dirPaths.firstObject;
  return [docPath stringByAppendingPathComponent:RCNDatabaseName];
}

/// Remote Config database path for current database.
static NSString *RemoteConfigPathForDatabase(void) {
#if TARGET_OS_TV
  NSArray *dirPaths = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES);
#else
  NSArray *dirPaths =
      NSSearchPathForDirectoriesInDomains(NSApplicationSupportDirectory, NSUserDomainMask, YES);
#endif
  NSString *storageDirPath = dirPaths.firstObject;
  NSArray *components = @[ storageDirPath, RCNRemoteConfigStorageSubDirectory, RCNDatabaseName ];
  return [NSString pathWithComponents:components];
}

static BOOL RemoteConfigAddSkipBackupAttributeToItemAtPath(NSString *filePathString) {
  NSURL *URL = [NSURL fileURLWithPath:filePathString];
  assert([[NSFileManager defaultManager] fileExistsAtPath:[URL path]]);

  NSError *error = nil;
  BOOL success = [URL setResourceValue:[NSNumber numberWithBool:YES]
                                forKey:NSURLIsExcludedFromBackupKey
                                 error:&error];
  if (!success) {
    FIRLogError(kFIRLoggerRemoteConfig, @"I-RCN000017", @"Error excluding %@ from backup %@.",
                [URL lastPathComponent], error);
  }
  return success;
}

static BOOL RemoteConfigCreateFilePathIfNotExist(NSString *filePath) {
  if (!filePath || !filePath.length) {
    FIRLogError(kFIRLoggerRemoteConfig, @"I-RCN000018",
                @"Failed to create subdirectory for an empty file path.");
    return NO;
  }
  NSFileManager *fileManager = [NSFileManager defaultManager];
  if (![fileManager fileExistsAtPath:filePath]) {
    gIsNewDatabase = YES;
    NSError *error;
    [fileManager createDirectoryAtPath:[filePath stringByDeletingLastPathComponent]
           withIntermediateDirectories:YES
                            attributes:nil
                                 error:&error];
    if (error) {
      FIRLogError(kFIRLoggerRemoteConfig, @"I-RCN000019",
                  @"Failed to create subdirectory for database file: %@.", error);
      return NO;
    }
  }
  return YES;
}

static NSArray *RemoteConfigMetadataTableColumnsInOrder(void) {
  return @[
    RCNKeyBundleIdentifier, RCNKeyNamespace, RCNKeyFetchTime, RCNKeyDigestPerNamespace,
    RCNKeyDeviceContext, RCNKeyAppContext, RCNKeySuccessFetchTime, RCNKeyFailureFetchTime,
    RCNKeyLastFetchStatus, RCNKeyLastFetchError, RCNKeyLastApplyTime, RCNKeyLastSetDefaultsTime
  ];
}

@interface RCNConfigDBManager () {
  /// Database storing all the config information.
  sqlite3 *_database;
  /// Serial queue for database read/write operations.
  dispatch_queue_t _databaseOperationQueue;
}
@end

@implementation RCNConfigDBManager

+ (instancetype)sharedInstance {
  static dispatch_once_t onceToken;
  static RCNConfigDBManager *sharedInstance;
  dispatch_once(&onceToken, ^{
    sharedInstance = [[RCNConfigDBManager alloc] init];
  });
  return sharedInstance;
}

/// Returns the current version of the Remote Config database.
+ (NSString *)remoteConfigPathForDatabase {
  return RemoteConfigPathForDatabase();
}

- (instancetype)init {
  self = [super init];
  if (self) {
    _databaseOperationQueue =
        dispatch_queue_create("com.google.GoogleConfigService.database", DISPATCH_QUEUE_SERIAL);
    [self createOrOpenDatabase];
  }
  return self;
}

#pragma mark - database
- (void)migrateV1NamespaceToV2Namespace {
  for (int table = 0; table < 3; table++) {
    NSString *tableName = @"" RCNTableNameMain;
    switch (table) {
      case 1:
        tableName = @"" RCNTableNameMainActive;
        break;
      case 2:
        tableName = @"" RCNTableNameMainDefault;
        break;
      default:
        break;
    }
    NSString *SQLString = [NSString
        stringWithFormat:@"SELECT namespace FROM %@ WHERE namespace NOT LIKE '%%:%%'", tableName];
    const char *SQL = [SQLString UTF8String];
    sqlite3_stmt *statement = [self prepareSQL:SQL];
    if (!statement) {
      return;
    }
    NSMutableArray<NSString *> *namespaceArray = [[NSMutableArray alloc] init];
    while (sqlite3_step(statement) == SQLITE_ROW) {
      NSString *configNamespace =
          [[NSString alloc] initWithUTF8String:(char *)sqlite3_column_text(statement, 0)];
      [namespaceArray addObject:configNamespace];
    }
    sqlite3_finalize(statement);

    // Update.
    for (NSString *namespaceToUpdate in namespaceArray) {
      NSString *newNamespace =
          [NSString stringWithFormat:@"%@:%@", namespaceToUpdate, kFIRDefaultAppName];
      NSString *updateSQLString =
          [NSString stringWithFormat:@"UPDATE %@ SET namespace = ? WHERE namespace = ?", tableName];
      const char *updateSQL = [updateSQLString UTF8String];
      sqlite3_stmt *updateStatement = [self prepareSQL:updateSQL];
      if (!updateStatement) {
        return;
      }
      NSArray<NSString *> *updateParams = @[ newNamespace, namespaceToUpdate ];
      [self bindStringsToStatement:updateStatement stringArray:updateParams];

      int result = sqlite3_step(updateStatement);
      if (result != SQLITE_DONE) {
        [self logErrorWithSQL:SQL finalizeStatement:updateStatement returnValue:NO];
        return;
      }
      sqlite3_finalize(updateStatement);
    }
  }
}

- (void)createOrOpenDatabase {
  __weak RCNConfigDBManager *weakSelf = self;
  dispatch_async(_databaseOperationQueue, ^{
    RCNConfigDBManager *strongSelf = weakSelf;
    if (!strongSelf) {
      return;
    }
    NSString *oldV0DBPath = RemoteConfigPathForOldDatabaseV0();
    // Backward Compatibility
    if ([[NSFileManager defaultManager] fileExistsAtPath:oldV0DBPath]) {
      FIRLogInfo(kFIRLoggerRemoteConfig, @"I-RCN000009",
                 @"Old database V0 exists, removed it and replace with the new one.");
      [strongSelf removeDatabase:oldV0DBPath];
    }
    NSString *dbPath = [RCNConfigDBManager remoteConfigPathForDatabase];
    FIRLogInfo(kFIRLoggerRemoteConfig, @"I-RCN000062", @"Loading database at path %@", dbPath);
    const char *databasePath = dbPath.UTF8String;

    // Create or open database path.
    if (!RemoteConfigCreateFilePathIfNotExist(dbPath)) {
      return;
    }

    int flags = SQLITE_OPEN_CREATE | SQLITE_OPEN_READWRITE | SQLITE_OPEN_FULLMUTEX;
#ifdef SQLITE_OPEN_FILEPROTECTION_COMPLETEUNTILFIRSTUSERAUTHENTICATION
    flags |= SQLITE_OPEN_FILEPROTECTION_COMPLETEUNTILFIRSTUSERAUTHENTICATION;
#endif

    if (sqlite3_open_v2(databasePath, &strongSelf->_database, flags, NULL) == SQLITE_OK) {
      // Always try to create table if not exists for backward compatibility.
      if (![strongSelf createTableSchema]) {
        // Remove database before fail.
        [strongSelf removeDatabase:dbPath];
        FIRLogError(kFIRLoggerRemoteConfig, @"I-RCN000010", @"Failed to create table.");
        // Create a new database if existing database file is corrupted.
        if (!RemoteConfigCreateFilePathIfNotExist(dbPath)) {
          return;
        }
        if (sqlite3_open_v2(databasePath, &strongSelf->_database, flags, NULL) == SQLITE_OK) {
          if (![strongSelf createTableSchema]) {
            // Remove database before fail.
            [strongSelf removeDatabase:dbPath];
            // If it failed again, there's nothing we can do here.
            FIRLogError(kFIRLoggerRemoteConfig, @"I-RCN000010", @"Failed to create table.");
          } else {
            // Exclude the app data used from iCloud backup.
            RemoteConfigAddSkipBackupAttributeToItemAtPath(dbPath);
          }
        } else {
          [strongSelf logDatabaseError];
        }
      } else {
        // DB file already exists. Migrate any V1 namespace column entries to V2 fully qualified
        // 'namespace:FIRApp' entries.
        [self migrateV1NamespaceToV2Namespace];
        // Exclude the app data used from iCloud backup.
        RemoteConfigAddSkipBackupAttributeToItemAtPath(dbPath);
      }
    } else {
      [strongSelf logDatabaseError];
    }
  });
}

- (BOOL)createTableSchema {
  RCN_MUST_NOT_BE_MAIN_THREAD();
  static const char *createTableMain =
      "create TABLE IF NOT EXISTS " RCNTableNameMain
      " (_id INTEGER PRIMARY KEY, bundle_identifier TEXT, namespace TEXT, key TEXT, value BLOB)";

  static const char *createTableMainActive =
      "create TABLE IF NOT EXISTS " RCNTableNameMainActive
      " (_id INTEGER PRIMARY KEY, bundle_identifier TEXT, namespace TEXT, key TEXT, value BLOB)";

  static const char *createTableMainDefault =
      "create TABLE IF NOT EXISTS " RCNTableNameMainDefault
      " (_id INTEGER PRIMARY KEY, bundle_identifier TEXT, namespace TEXT, key TEXT, value BLOB)";

  static const char *createTableMetadata =
      "create TABLE IF NOT EXISTS " RCNTableNameMetadata
      " (_id INTEGER PRIMARY KEY, bundle_identifier TEXT, namespace TEXT,"
      " fetch_time INTEGER, digest_per_ns BLOB, device_context BLOB, app_context BLOB, "
      "success_fetch_time BLOB, failure_fetch_time BLOB, last_fetch_status INTEGER, "
      "last_fetch_error INTEGER, last_apply_time INTEGER, last_set_defaults_time INTEGER)";

  static const char *createTableExperiment = "create TABLE IF NOT EXISTS " RCNTableNameExperiment
                                             " (_id INTEGER PRIMARY KEY, key TEXT, value BLOB)";
  static const char *createTablePersonalization =
      "create TABLE IF NOT EXISTS " RCNTableNamePersonalization
      " (_id INTEGER PRIMARY KEY, key INTEGER, value BLOB)";

  static const char *createTableRollout = "create TABLE IF NOT EXISTS " RCNTableNameRollout
                                          " (_id INTEGER PRIMARY KEY, key TEXT, value BLOB)";

  return [self executeQuery:createTableMain] && [self executeQuery:createTableMainActive] &&
         [self executeQuery:createTableMainDefault] && [self executeQuery:createTableMetadata] &&
         [self executeQuery:createTableExperiment] &&
         [self executeQuery:createTablePersonalization] && [self executeQuery:createTableRollout];
}

- (void)removeDatabaseOnDatabaseQueueAtPath:(NSString *)path {
  __weak RCNConfigDBManager *weakSelf = self;
  dispatch_sync(_databaseOperationQueue, ^{
    RCNConfigDBManager *strongSelf = weakSelf;
    if (!strongSelf) {
      return;
    }
    if (sqlite3_close(strongSelf->_database) != SQLITE_OK) {
      [self logDatabaseError];
    }
    strongSelf->_database = nil;

    NSFileManager *fileManager = [NSFileManager defaultManager];
    NSError *error;
    if (![fileManager removeItemAtPath:path error:&error]) {
      FIRLogError(kFIRLoggerRemoteConfig, @"I-RCN000011",
                  @"Failed to remove database at path %@ for error %@.", path, error);
    }
  });
}

- (void)removeDatabase:(NSString *)path {
  if (sqlite3_close(_database) != SQLITE_OK) {
    [self logDatabaseError];
  }
  _database = nil;

  NSFileManager *fileManager = [NSFileManager defaultManager];
  NSError *error;
  if (![fileManager removeItemAtPath:path error:&error]) {
    FIRLogError(kFIRLoggerRemoteConfig, @"I-RCN000011",
                @"Failed to remove database at path %@ for error %@.", path, error);
  }
}

#pragma mark - execute
- (BOOL)executeQuery:(const char *)SQL {
  RCN_MUST_NOT_BE_MAIN_THREAD();
  char *error;
  if (sqlite3_exec(_database, SQL, nil, nil, &error) != SQLITE_OK) {
    FIRLogError(kFIRLoggerRemoteConfig, @"I-RCN000012", @"Failed to execute query with error %s.",
                error);
    return NO;
  }
  return YES;
}

#pragma mark - insert
- (void)insertMetadataTableWithValues:(NSDictionary *)columnNameToValue
                    completionHandler:(RCNDBCompletion)handler {
  __weak RCNConfigDBManager *weakSelf = self;
  dispatch_async(_databaseOperationQueue, ^{
    BOOL success = [weakSelf insertMetadataTableWithValues:columnNameToValue];
    if (handler) {
      dispatch_async(dispatch_get_main_queue(), ^{
        handler(success, nil);
      });
    }
  });
}

- (BOOL)insertMetadataTableWithValues:(NSDictionary *)columnNameToValue {
  RCN_MUST_NOT_BE_MAIN_THREAD();
  static const char *SQL =
      "INSERT INTO " RCNTableNameMetadata
      " (bundle_identifier, namespace, fetch_time, digest_per_ns, device_context, "
      "app_context, success_fetch_time, failure_fetch_time, last_fetch_status, "
      "last_fetch_error, last_apply_time, last_set_defaults_time) values (?, ?, ?, ?, ?, ?, "
      "?, ?, ?, ?, ?, ?)";

  sqlite3_stmt *statement = [self prepareSQL:SQL];
  if (!statement) {
    [self logErrorWithSQL:SQL finalizeStatement:nil returnValue:NO];
    return NO;
  }

  NSArray *columns = RemoteConfigMetadataTableColumnsInOrder();
  int index = 0;
  for (NSString *columnName in columns) {
    if ([columnName isEqualToString:RCNKeyBundleIdentifier] ||
        [columnName isEqualToString:RCNKeyNamespace]) {
      NSString *value = columnNameToValue[columnName];
      if (![self bindStringToStatement:statement index:++index string:value]) {
        return [self logErrorWithSQL:SQL finalizeStatement:statement returnValue:NO];
      }
    } else if ([columnName isEqualToString:RCNKeyFetchTime] ||
               [columnName isEqualToString:RCNKeyLastApplyTime] ||
               [columnName isEqualToString:RCNKeyLastSetDefaultsTime]) {
      double value = [columnNameToValue[columnName] doubleValue];
      if (sqlite3_bind_double(statement, ++index, value) != SQLITE_OK) {
        return [self logErrorWithSQL:SQL finalizeStatement:statement returnValue:NO];
      }
    } else if ([columnName isEqualToString:RCNKeyLastFetchStatus] ||
               [columnName isEqualToString:RCNKeyLastFetchError]) {
      int value = [columnNameToValue[columnName] intValue];
      if (sqlite3_bind_int(statement, ++index, value) != SQLITE_OK) {
        return [self logErrorWithSQL:SQL finalizeStatement:statement returnValue:NO];
      }
    } else {
      NSData *data = columnNameToValue[columnName];
      if (sqlite3_bind_blob(statement, ++index, data.bytes, (int)data.length, NULL) != SQLITE_OK) {
        return [self logErrorWithSQL:SQL finalizeStatement:statement returnValue:NO];
      }
    }
  }
  if (sqlite3_step(statement) != SQLITE_DONE) {
    return [self logErrorWithSQL:SQL finalizeStatement:statement returnValue:NO];
  }
  sqlite3_finalize(statement);
  return YES;
}

- (void)insertMainTableWithValues:(NSArray *)values
                       fromSource:(RCNDBSource)source
                completionHandler:(RCNDBCompletion)handler {
  __weak RCNConfigDBManager *weakSelf = self;
  dispatch_async(_databaseOperationQueue, ^{
    BOOL success = [weakSelf insertMainTableWithValues:values fromSource:source];
    if (handler) {
      dispatch_async(dispatch_get_main_queue(), ^{
        handler(success, nil);
      });
    }
  });
}

- (BOOL)insertMainTableWithValues:(NSArray *)values fromSource:(RCNDBSource)source {
  RCN_MUST_NOT_BE_MAIN_THREAD();
  if (values.count != 4) {
    FIRLogError(kFIRLoggerRemoteConfig, @"I-RCN000013",
                @"Failed to insert config record. Wrong number of give parameters, current "
                @"number is %ld, correct number is 4.",
                (long)values.count);
    return NO;
  }
  const char *SQL = "INSERT INTO " RCNTableNameMain
                    " (bundle_identifier, namespace, key, value) values (?, ?, ?, ?)";
  if (source == RCNDBSourceDefault) {
    SQL = "INSERT INTO " RCNTableNameMainDefault
          " (bundle_identifier, namespace, key, value) values (?, ?, ?, ?)";
  } else if (source == RCNDBSourceActive) {
    SQL = "INSERT INTO " RCNTableNameMainActive
          " (bundle_identifier, namespace, key, value) values (?, ?, ?, ?)";
  }

  sqlite3_stmt *statement = [self prepareSQL:SQL];
  if (!statement) {
    return NO;
  }

  NSString *aString = values[0];
  if (![self bindStringToStatement:statement index:1 string:aString]) {
    return [self logErrorWithSQL:SQL finalizeStatement:statement returnValue:NO];
  }
  aString = values[1];
  if (![self bindStringToStatement:statement index:2 string:aString]) {
    return [self logErrorWithSQL:SQL finalizeStatement:statement returnValue:NO];
  }
  aString = values[2];
  if (![self bindStringToStatement:statement index:3 string:aString]) {
    return [self logErrorWithSQL:SQL finalizeStatement:statement returnValue:NO];
  }
  NSData *blobData = values[3];
  if (sqlite3_bind_blob(statement, 4, blobData.bytes, (int)blobData.length, NULL) != SQLITE_OK) {
    return [self logErrorWithSQL:SQL finalizeStatement:statement returnValue:NO];
  }
  if (sqlite3_step(statement) != SQLITE_DONE) {
    return [self logErrorWithSQL:SQL finalizeStatement:statement returnValue:NO];
  }
  sqlite3_finalize(statement);
  return YES;
}

- (void)insertExperimentTableWithKey:(NSString *)key
                               value:(NSData *)serializedValue
                   completionHandler:(RCNDBCompletion)handler {
  dispatch_async(_databaseOperationQueue, ^{
    BOOL success = [self insertExperimentTableWithKey:key value:serializedValue];
    if (handler) {
      dispatch_async(dispatch_get_main_queue(), ^{
        handler(success, nil);
      });
    }
  });
}

- (BOOL)insertExperimentTableWithKey:(NSString *)key value:(NSData *)dataValue {
  if ([key isEqualToString:@RCNExperimentTableKeyMetadata]) {
    return [self updateExperimentMetadata:dataValue];
  }

  RCN_MUST_NOT_BE_MAIN_THREAD();
  const char *SQL = "INSERT INTO " RCNTableNameExperiment " (key, value) values (?, ?)";

  sqlite3_stmt *statement = [self prepareSQL:SQL];
  if (!statement) {
    return NO;
  }

  if (![self bindStringToStatement:statement index:1 string:key]) {
    return [self logErrorWithSQL:SQL finalizeStatement:statement returnValue:NO];
  }

  if (sqlite3_bind_blob(statement, 2, dataValue.bytes, (int)dataValue.length, NULL) != SQLITE_OK) {
    return [self logErrorWithSQL:SQL finalizeStatement:statement returnValue:NO];
  }

  if (sqlite3_step(statement) != SQLITE_DONE) {
    return [self logErrorWithSQL:SQL finalizeStatement:statement returnValue:NO];
  }
  sqlite3_finalize(statement);
  return YES;
}

- (BOOL)updateExperimentMetadata:(NSData *)dataValue {
  RCN_MUST_NOT_BE_MAIN_THREAD();
  const char *SQL = "INSERT OR REPLACE INTO " RCNTableNameExperiment
                    " (_id, key, value) values ((SELECT _id from " RCNTableNameExperiment
                    " WHERE key = ?), ?, ?)";

  sqlite3_stmt *statement = [self prepareSQL:SQL];
  if (!statement) {
    return NO;
  }

  if (![self bindStringToStatement:statement index:1 string:@RCNExperimentTableKeyMetadata]) {
    return [self logErrorWithSQL:SQL finalizeStatement:statement returnValue:NO];
  }

  if (![self bindStringToStatement:statement index:2 string:@RCNExperimentTableKeyMetadata]) {
    return [self logErrorWithSQL:SQL finalizeStatement:statement returnValue:NO];
  }
  if (sqlite3_bind_blob(statement, 3, dataValue.bytes, (int)dataValue.length, NULL) != SQLITE_OK) {
    return [self logErrorWithSQL:SQL finalizeStatement:statement returnValue:NO];
  }

  if (sqlite3_step(statement) != SQLITE_DONE) {
    return [self logErrorWithSQL:SQL finalizeStatement:statement returnValue:NO];
  }
  sqlite3_finalize(statement);
  return YES;
}

- (BOOL)insertOrUpdatePersonalizationConfig:(NSDictionary *)dataValue
                                 fromSource:(RCNDBSource)source {
  RCN_MUST_NOT_BE_MAIN_THREAD();

  NSError *error;
  NSData *JSONPayload = [NSJSONSerialization dataWithJSONObject:dataValue
                                                        options:NSJSONWritingPrettyPrinted
                                                          error:&error];

  if (!JSONPayload || error) {
    FIRLogError(kFIRLoggerRemoteConfig, @"I-RCN000075",
                @"Invalid Personalization payload to be serialized.");
  }

  const char *SQL = "INSERT OR REPLACE INTO " RCNTableNamePersonalization
                    " (_id, key, value) values ((SELECT _id from " RCNTableNamePersonalization
                    " WHERE key = ?), ?, ?)";

  sqlite3_stmt *statement = [self prepareSQL:SQL];
  if (!statement) {
    return NO;
  }

  if (sqlite3_bind_int(statement, 1, (int)source) != SQLITE_OK) {
    return [self logErrorWithSQL:SQL finalizeStatement:statement returnValue:NO];
  }

  if (sqlite3_bind_int(statement, 2, (int)source) != SQLITE_OK) {
    return [self logErrorWithSQL:SQL finalizeStatement:statement returnValue:NO];
  }
  if (sqlite3_bind_blob(statement, 3, JSONPayload.bytes, (int)JSONPayload.length, NULL) !=
      SQLITE_OK) {
    return [self logErrorWithSQL:SQL finalizeStatement:statement returnValue:NO];
  }

  if (sqlite3_step(statement) != SQLITE_DONE) {
    return [self logErrorWithSQL:SQL finalizeStatement:statement returnValue:NO];
  }
  sqlite3_finalize(statement);
  return YES;
}

- (void)insertOrUpdateRolloutTableWithKey:(NSString *)key
                                    value:(NSArray<NSDictionary *> *)metadataList
                        completionHandler:(RCNDBCompletion)handler {
  dispatch_async(_databaseOperationQueue, ^{
    BOOL success = [self insertOrUpdateRolloutTableWithKey:key value:metadataList];
    if (handler) {
      dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        handler(success, nil);
      });
    }
  });
}

- (BOOL)insertOrUpdateRolloutTableWithKey:(NSString *)key
                                    value:(NSArray<NSDictionary *> *)arrayValue {
  RCN_MUST_NOT_BE_MAIN_THREAD();
  NSError *error;
  NSData *dataValue = [NSJSONSerialization dataWithJSONObject:arrayValue
                                                      options:NSJSONWritingPrettyPrinted
                                                        error:&error];
  const char *SQL =
      "INSERT OR REPLACE INTO " RCNTableNameRollout
      " (_id, key, value) values ((SELECT _id from " RCNTableNameRollout " WHERE key = ?), ?, ?)";
  sqlite3_stmt *statement = [self prepareSQL:SQL];
  if (!statement) {
    return NO;
  }
  if (![self bindStringToStatement:statement index:1 string:key]) {
    return [self logErrorWithSQL:SQL finalizeStatement:statement returnValue:NO];
  }

  if (![self bindStringToStatement:statement index:2 string:key]) {
    return [self logErrorWithSQL:SQL finalizeStatement:statement returnValue:NO];
  }

  if (sqlite3_bind_blob(statement, 3, dataValue.bytes, (int)dataValue.length, NULL) != SQLITE_OK) {
    return [self logErrorWithSQL:SQL finalizeStatement:statement returnValue:NO];
  }

  if (sqlite3_step(statement) != SQLITE_DONE) {
    return [self logErrorWithSQL:SQL finalizeStatement:statement returnValue:NO];
  }
  sqlite3_finalize(statement);
  return YES;
}

#pragma mark - update

- (void)updateMetadataWithOption:(RCNUpdateOption)option
                       namespace:(NSString *)namespace
                          values:(NSArray *)values
               completionHandler:(RCNDBCompletion)handler {
  dispatch_async(_databaseOperationQueue, ^{
    BOOL success = [self updateMetadataTableWithOption:option namespace:namespace andValues:values];
    if (handler) {
      dispatch_async(dispatch_get_main_queue(), ^{
        handler(success, nil);
      });
    }
  });
}

- (BOOL)updateMetadataTableWithOption:(RCNUpdateOption)option
                            namespace:(NSString *)namespace
                            andValues:(NSArray *)values {
  RCN_MUST_NOT_BE_MAIN_THREAD();
  static const char *SQL =
      "UPDATE " RCNTableNameMetadata " (last_fetch_status, last_fetch_error, last_apply_time, "
      "last_set_defaults_time) values (?, ?, ?, ?) WHERE namespace = ?";
  if (option == RCNUpdateOptionFetchStatus) {
    SQL = "UPDATE " RCNTableNameMetadata
          " SET last_fetch_status = ?, last_fetch_error = ? WHERE namespace = ?";
  } else if (option == RCNUpdateOptionApplyTime) {
    SQL = "UPDATE " RCNTableNameMetadata " SET last_apply_time = ? WHERE namespace = ?";
  } else if (option == RCNUpdateOptionDefaultTime) {
    SQL = "UPDATE " RCNTableNameMetadata " SET last_set_defaults_time = ? WHERE namespace = ?";
  } else {
    return NO;
  }
  sqlite3_stmt *statement = [self prepareSQL:SQL];
  if (!statement) {
    return NO;
  }

  int index = 0;
  if ((option == RCNUpdateOptionApplyTime || option == RCNUpdateOptionDefaultTime) &&
      values.count == 1) {
    double value = [values[0] doubleValue];
    if (sqlite3_bind_double(statement, ++index, value) != SQLITE_OK) {
      return [self logErrorWithSQL:SQL finalizeStatement:statement returnValue:NO];
    }
  } else if (option == RCNUpdateOptionFetchStatus && values.count == 2) {
    int value = [values[0] intValue];
    if (sqlite3_bind_int(statement, ++index, value) != SQLITE_OK) {
      return [self logErrorWithSQL:SQL finalizeStatement:statement returnValue:NO];
    }
    value = [values[1] intValue];
    if (sqlite3_bind_int(statement, ++index, value) != SQLITE_OK) {
      return [self logErrorWithSQL:SQL finalizeStatement:statement returnValue:NO];
    }
  }
  // bind namespace to query
  if (sqlite3_bind_text(statement, ++index, [namespace UTF8String], -1, SQLITE_TRANSIENT) !=
      SQLITE_OK) {
    return [self logErrorWithSQL:SQL finalizeStatement:statement returnValue:NO];
  }

  if (sqlite3_step(statement) != SQLITE_DONE) {
    return [self logErrorWithSQL:SQL finalizeStatement:statement returnValue:NO];
  }
  sqlite3_finalize(statement);
  return YES;
}
#pragma mark - read from DB

- (NSDictionary *)loadMetadataWithBundleIdentifier:(NSString *)bundleIdentifier
                                         namespace:(NSString *)namespace {
  __block NSDictionary *metadataTableResult;
  __weak RCNConfigDBManager *weakSelf = self;
  dispatch_sync(_databaseOperationQueue, ^{
    metadataTableResult = [weakSelf loadMetadataTableWithBundleIdentifier:bundleIdentifier
                                                                namespace:namespace];
  });
  if (metadataTableResult) {
    return metadataTableResult;
  }
  return [[NSDictionary alloc] init];
}

- (NSMutableDictionary *)loadMetadataTableWithBundleIdentifier:(NSString *)bundleIdentifier
                                                     namespace:(NSString *)namespace {
  NSMutableDictionary *dict = [[NSMutableDictionary alloc] init];
  const char *SQL =
      "SELECT bundle_identifier, fetch_time, digest_per_ns, device_context, app_context, "
      "success_fetch_time, failure_fetch_time , last_fetch_status, "
      "last_fetch_error, last_apply_time, last_set_defaults_time FROM " RCNTableNameMetadata
      " WHERE bundle_identifier = ? and namespace = ?";
  sqlite3_stmt *statement = [self prepareSQL:SQL];
  if (!statement) {
    return nil;
  }

  NSArray *params = @[ bundleIdentifier, namespace ];
  [self bindStringsToStatement:statement stringArray:params];

  while (sqlite3_step(statement) == SQLITE_ROW) {
    NSString *dbBundleIdentifier =
        [[NSString alloc] initWithUTF8String:(char *)sqlite3_column_text(statement, 0)];

    if (dbBundleIdentifier && ![dbBundleIdentifier isEqualToString:bundleIdentifier]) {
      FIRLogError(kFIRLoggerRemoteConfig, @"I-RCN000014",
                  @"Load Metadata from table error: Wrong package name %@, should be %@.",
                  dbBundleIdentifier, bundleIdentifier);
      return nil;
    }

    double fetchTime = sqlite3_column_double(statement, 1);
    NSData *digestPerNamespace = [NSData dataWithBytes:(char *)sqlite3_column_blob(statement, 2)
                                                length:sqlite3_column_bytes(statement, 2)];
    NSData *deviceContext = [NSData dataWithBytes:(char *)sqlite3_column_blob(statement, 3)
                                           length:sqlite3_column_bytes(statement, 3)];
    NSData *appContext = [NSData dataWithBytes:(char *)sqlite3_column_blob(statement, 4)
                                        length:sqlite3_column_bytes(statement, 4)];
    NSData *successTimeDigest = [NSData dataWithBytes:(char *)sqlite3_column_blob(statement, 5)
                                               length:sqlite3_column_bytes(statement, 5)];
    NSData *failureTimeDigest = [NSData dataWithBytes:(char *)sqlite3_column_blob(statement, 6)
                                               length:sqlite3_column_bytes(statement, 6)];

    int lastFetchStatus = sqlite3_column_int(statement, 7);
    int lastFetchFailReason = sqlite3_column_int(statement, 8);
    double lastApplyTimestamp = sqlite3_column_double(statement, 9);
    double lastSetDefaultsTimestamp = sqlite3_column_double(statement, 10);

    NSError *error;
    NSMutableDictionary *deviceContextDict = nil;
    if (deviceContext) {
      deviceContextDict = [NSJSONSerialization JSONObjectWithData:deviceContext
                                                          options:NSJSONReadingMutableContainers
                                                            error:&error];
    }

    NSMutableDictionary *appContextDict = nil;
    if (appContext) {
      appContextDict = [NSJSONSerialization JSONObjectWithData:appContext
                                                       options:NSJSONReadingMutableContainers
                                                         error:&error];
    }

    NSMutableDictionary<NSString *, id> *digestPerNamespaceDictionary = nil;
    if (digestPerNamespace) {
      digestPerNamespaceDictionary =
          [NSJSONSerialization JSONObjectWithData:digestPerNamespace
                                          options:NSJSONReadingMutableContainers
                                            error:&error];
    }

    NSMutableArray *successTimes = nil;
    if (successTimeDigest) {
      successTimes = [NSJSONSerialization JSONObjectWithData:successTimeDigest
                                                     options:NSJSONReadingMutableContainers
                                                       error:&error];
    }

    NSMutableArray *failureTimes = nil;
    if (failureTimeDigest) {
      failureTimes = [NSJSONSerialization JSONObjectWithData:failureTimeDigest
                                                     options:NSJSONReadingMutableContainers
                                                       error:&error];
    }

    dict[RCNKeyBundleIdentifier] = bundleIdentifier;
    dict[RCNKeyFetchTime] = @(fetchTime);
    dict[RCNKeyDigestPerNamespace] = digestPerNamespaceDictionary;
    dict[RCNKeyDeviceContext] = deviceContextDict;
    dict[RCNKeyAppContext] = appContextDict;
    dict[RCNKeySuccessFetchTime] = successTimes;
    dict[RCNKeyFailureFetchTime] = failureTimes;
    dict[RCNKeyLastFetchStatus] = @(lastFetchStatus);
    dict[RCNKeyLastFetchError] = @(lastFetchFailReason);
    dict[RCNKeyLastApplyTime] = @(lastApplyTimestamp);
    dict[RCNKeyLastSetDefaultsTime] = @(lastSetDefaultsTimestamp);

    break;
  }
  sqlite3_finalize(statement);
  return dict;
}

- (void)loadExperimentWithCompletionHandler:(RCNDBCompletion)handler {
  __weak RCNConfigDBManager *weakSelf = self;
  dispatch_async(_databaseOperationQueue, ^{
    RCNConfigDBManager *strongSelf = weakSelf;
    if (!strongSelf) {
      return;
    }
    NSMutableArray *experimentPayloads =
        [strongSelf loadExperimentTableFromKey:@RCNExperimentTableKeyPayload];
    if (!experimentPayloads) {
      experimentPayloads = [[NSMutableArray alloc] init];
    }

    NSMutableDictionary *experimentMetadata;
    NSMutableArray *experiments =
        [strongSelf loadExperimentTableFromKey:@RCNExperimentTableKeyMetadata];
    // There should be only one entry for experiment metadata.
    if (experiments.count > 0) {
      NSError *error;
      experimentMetadata = [NSJSONSerialization JSONObjectWithData:experiments[0]
                                                           options:NSJSONReadingMutableContainers
                                                             error:&error];
    }
    if (!experimentMetadata) {
      experimentMetadata = [[NSMutableDictionary alloc] init];
    }

    /// Load activated experiments payload.
    NSMutableArray *activeExperimentPayloads =
        [strongSelf loadExperimentTableFromKey:@RCNExperimentTableKeyActivePayload];
    if (!activeExperimentPayloads) {
      activeExperimentPayloads = [[NSMutableArray alloc] init];
    }

    if (handler) {
      dispatch_async(dispatch_get_main_queue(), ^{
        handler(
            YES, @{
              @RCNExperimentTableKeyPayload : [experimentPayloads copy],
              @RCNExperimentTableKeyMetadata : [experimentMetadata copy],
              /// Activated experiments only need ExperimentsDescriptions data, which
              /// experimentPayloads contains.
              @RCNExperimentTableKeyActivePayload : [activeExperimentPayloads copy]
            });
      });
    }
  });
}

- (NSMutableArray<NSData *> *)loadExperimentTableFromKey:(NSString *)key {
  RCN_MUST_NOT_BE_MAIN_THREAD();

  const char *SQL = "SELECT value FROM " RCNTableNameExperiment " WHERE key = ?";
  sqlite3_stmt *statement = [self prepareSQL:SQL];
  if (!statement) {
    return nil;
  }

  NSArray *params = @[ key ];
  [self bindStringsToStatement:statement stringArray:params];
  NSMutableArray *results = [self loadValuesFromStatement:statement];
  return results;
}

- (NSArray<NSDictionary *> *)loadRolloutTableFromKey:(NSString *)key {
  RCN_MUST_NOT_BE_MAIN_THREAD();
  const char *SQL = "SELECT value FROM " RCNTableNameRollout " WHERE key = ?";
  sqlite3_stmt *statement = [self prepareSQL:SQL];
  if (!statement) {
    return nil;
  }
  NSArray *params = @[ key ];
  [self bindStringsToStatement:statement stringArray:params];
  NSMutableArray *results = [self loadValuesFromStatement:statement];
  // There should be only one entry in this table.
  if (results.count != 1) {
    return nil;
  }
  NSArray *rollout;
  // Convert from NSData to NSArray
  if (results[0]) {
    NSError *error;
    rollout = [NSJSONSerialization JSONObjectWithData:results[0] options:0 error:&error];
    if (!rollout) {
      FIRLogError(kFIRLoggerRemoteConfig, @"I-RCN000011",
                  @"Failed to convert NSData to NSAarry for Rollout Metadata with error %@.",
                  error);
    }
  }
  if (!rollout) {
    rollout = [[NSArray alloc] init];
  }
  return rollout;
}

- (NSMutableArray *)loadValuesFromStatement:(sqlite3_stmt *)statement {
  NSMutableArray *results = [[NSMutableArray alloc] init];
  NSData *value;
  while (sqlite3_step(statement) == SQLITE_ROW) {
    value = [NSData dataWithBytes:(char *)sqlite3_column_blob(statement, 0)
                           length:sqlite3_column_bytes(statement, 0)];
    if (value) {
      [results addObject:value];
    }
  }

  sqlite3_finalize(statement);
  return results;
}

- (void)loadPersonalizationWithCompletionHandler:(RCNDBLoadCompletion)handler {
  __weak RCNConfigDBManager *weakSelf = self;
  dispatch_async(_databaseOperationQueue, ^{
    RCNConfigDBManager *strongSelf = weakSelf;
    if (!strongSelf) {
      dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        handler(NO, [NSMutableDictionary new], [NSMutableDictionary new], nil, nil);
      });
      return;
    }

    NSDictionary *activePersonalization;
    NSData *personalizationResult = [strongSelf loadPersonalizationTableFromKey:RCNDBSourceActive];
    // There should be only one entry for Personalization metadata.
    if (personalizationResult) {
      NSError *error;
      activePersonalization = [NSJSONSerialization JSONObjectWithData:personalizationResult
                                                              options:0
                                                                error:&error];
    }
    if (!activePersonalization) {
      activePersonalization = [[NSMutableDictionary alloc] init];
    }

    NSDictionary *fetchedPersonalization;
    personalizationResult = [strongSelf loadPersonalizationTableFromKey:RCNDBSourceFetched];
    // There should be only one entry for Personalization metadata.
    if (personalizationResult) {
      NSError *error;
      fetchedPersonalization = [NSJSONSerialization JSONObjectWithData:personalizationResult
                                                               options:0
                                                                 error:&error];
    }
    if (!fetchedPersonalization) {
      fetchedPersonalization = [[NSMutableDictionary alloc] init];
    }

    if (handler) {
      dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        handler(YES, fetchedPersonalization, activePersonalization, nil, nil);
      });
    }
  });
}

- (NSData *)loadPersonalizationTableFromKey:(int)key {
  RCN_MUST_NOT_BE_MAIN_THREAD();

  NSMutableArray *results = [[NSMutableArray alloc] init];
  const char *SQL = "SELECT value FROM " RCNTableNamePersonalization " WHERE key = ?";
  sqlite3_stmt *statement = [self prepareSQL:SQL];
  if (!statement) {
    return nil;
  }

  if (sqlite3_bind_int(statement, 1, key) != SQLITE_OK) {
    [self logErrorWithSQL:SQL finalizeStatement:statement returnValue:NO];
    return nil;
  }
  NSData *personalizationData;
  while (sqlite3_step(statement) == SQLITE_ROW) {
    personalizationData = [NSData dataWithBytes:(char *)sqlite3_column_blob(statement, 0)
                                         length:sqlite3_column_bytes(statement, 0)];
    if (personalizationData) {
      [results addObject:personalizationData];
    }
  }

  sqlite3_finalize(statement);
  // There should be only one entry in this table.
  if (results.count != 1) {
    return nil;
  }
  return results[0];
}

/// This method is only meant to be called at init time. The underlying logic will need to be
/// reevaluated if the assumption changes at a later time.
- (void)loadMainWithBundleIdentifier:(NSString *)bundleIdentifier
                   completionHandler:(RCNDBLoadCompletion)handler {
  __weak RCNConfigDBManager *weakSelf = self;
  dispatch_async(_databaseOperationQueue, ^{
    RCNConfigDBManager *strongSelf = weakSelf;
    if (!strongSelf) {
      dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        handler(NO, [NSDictionary new], [NSDictionary new], [NSDictionary new], [NSDictionary new]);
      });
      return;
    }
    __block NSDictionary *fetchedConfig =
        [strongSelf loadMainTableWithBundleIdentifier:bundleIdentifier
                                           fromSource:RCNDBSourceFetched];
    __block NSDictionary *activeConfig =
        [strongSelf loadMainTableWithBundleIdentifier:bundleIdentifier
                                           fromSource:RCNDBSourceActive];
    __block NSDictionary *defaultConfig =
        [strongSelf loadMainTableWithBundleIdentifier:bundleIdentifier
                                           fromSource:RCNDBSourceDefault];

    __block NSArray<NSDictionary *> *fetchedRolloutMetadata =
        [strongSelf loadRolloutTableFromKey:@RCNRolloutTableKeyFetchedMetadata];
    __block NSArray<NSDictionary *> *activeRolloutMetadata =
        [strongSelf loadRolloutTableFromKey:@RCNRolloutTableKeyActiveMetadata];

    if (handler) {
      dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        fetchedConfig = fetchedConfig ? fetchedConfig : [[NSDictionary alloc] init];
        activeConfig = activeConfig ? activeConfig : [[NSDictionary alloc] init];
        defaultConfig = defaultConfig ? defaultConfig : [[NSDictionary alloc] init];
        fetchedRolloutMetadata =
            fetchedRolloutMetadata ? fetchedRolloutMetadata : [[NSArray alloc] init];
        activeRolloutMetadata =
            activeRolloutMetadata ? activeRolloutMetadata : [[NSArray alloc] init];
        NSDictionary *rolloutMetadata = @{
          @RCNRolloutTableKeyActiveMetadata : [activeRolloutMetadata copy],
          @RCNRolloutTableKeyFetchedMetadata : [fetchedRolloutMetadata copy]
        };
        handler(YES, fetchedConfig, activeConfig, defaultConfig, rolloutMetadata);
      });
    }
  });
}

- (NSMutableDictionary *)loadMainTableWithBundleIdentifier:(NSString *)bundleIdentifier
                                                fromSource:(RCNDBSource)source {
  NSMutableDictionary *namespaceToConfig = [[NSMutableDictionary alloc] init];
  const char *SQL = "SELECT bundle_identifier, namespace, key, value FROM " RCNTableNameMain
                    " WHERE bundle_identifier = ?";
  if (source == RCNDBSourceDefault) {
    SQL = "SELECT bundle_identifier, namespace, key, value FROM " RCNTableNameMainDefault
          " WHERE bundle_identifier = ?";
  } else if (source == RCNDBSourceActive) {
    SQL = "SELECT bundle_identifier, namespace, key, value FROM " RCNTableNameMainActive
          " WHERE bundle_identifier = ?";
  }
  NSArray *params = @[ bundleIdentifier ];
  sqlite3_stmt *statement = [self prepareSQL:SQL];
  if (!statement) {
    return nil;
  }
  [self bindStringsToStatement:statement stringArray:params];

  while (sqlite3_step(statement) == SQLITE_ROW) {
    NSString *configNamespace =
        [[NSString alloc] initWithUTF8String:(char *)sqlite3_column_text(statement, 1)];
    NSString *key = [[NSString alloc] initWithUTF8String:(char *)sqlite3_column_text(statement, 2)];
    NSData *value = [NSData dataWithBytes:(char *)sqlite3_column_blob(statement, 3)
                                   length:sqlite3_column_bytes(statement, 3)];
    if (!namespaceToConfig[configNamespace]) {
      namespaceToConfig[configNamespace] = [[NSMutableDictionary alloc] init];
    }

    if (source == RCNDBSourceDefault) {
      namespaceToConfig[configNamespace][key] =
          [[FIRRemoteConfigValue alloc] initWithData:value source:FIRRemoteConfigSourceDefault];
    } else {
      namespaceToConfig[configNamespace][key] =
          [[FIRRemoteConfigValue alloc] initWithData:value source:FIRRemoteConfigSourceRemote];
    }
  }
  sqlite3_finalize(statement);
  return namespaceToConfig;
}

#pragma mark - delete
- (void)deleteRecordFromMainTableWithNamespace:(NSString *)namespace_p
                              bundleIdentifier:(NSString *)bundleIdentifier
                                    fromSource:(RCNDBSource)source {
  __weak RCNConfigDBManager *weakSelf = self;
  dispatch_async(_databaseOperationQueue, ^{
    RCNConfigDBManager *strongSelf = weakSelf;
    if (!strongSelf) {
      return;
    }
    NSArray *params = @[ bundleIdentifier, namespace_p ];
    const char *SQL =
        "DELETE FROM " RCNTableNameMain " WHERE bundle_identifier = ? and namespace = ?";
    if (source == RCNDBSourceDefault) {
      SQL = "DELETE FROM " RCNTableNameMainDefault " WHERE bundle_identifier = ? and namespace = ?";
    } else if (source == RCNDBSourceActive) {
      SQL = "DELETE FROM " RCNTableNameMainActive " WHERE bundle_identifier = ? and namespace = ?";
    }
    [strongSelf executeQuery:SQL withParams:params];
  });
}

- (void)deleteRecordWithBundleIdentifier:(NSString *)bundleIdentifier
                               namespace:(NSString *)namespace {
  __weak RCNConfigDBManager *weakSelf = self;
  dispatch_async(_databaseOperationQueue, ^{
    RCNConfigDBManager *strongSelf = weakSelf;
    if (!strongSelf) {
      return;
    }
    const char *SQL =
        "DELETE FROM " RCNTableNameMetadata " WHERE bundle_identifier = ? and namespace = ?";
    NSArray *params = @[ bundleIdentifier, namespace ];
    [strongSelf executeQuery:SQL withParams:params];
  });
}

- (void)deleteAllRecordsFromTableWithSource:(RCNDBSource)source {
  __weak RCNConfigDBManager *weakSelf = self;
  dispatch_async(_databaseOperationQueue, ^{
    RCNConfigDBManager *strongSelf = weakSelf;
    if (!strongSelf) {
      return;
    }
    const char *SQL = "DELETE FROM " RCNTableNameMain;
    if (source == RCNDBSourceDefault) {
      SQL = "DELETE FROM " RCNTableNameMainDefault;
    } else if (source == RCNDBSourceActive) {
      SQL = "DELETE FROM " RCNTableNameMainActive;
    }
    [strongSelf executeQuery:SQL];
  });
}

- (void)deleteExperimentTableForKey:(NSString *)key {
  __weak RCNConfigDBManager *weakSelf = self;
  dispatch_async(_databaseOperationQueue, ^{
    RCNConfigDBManager *strongSelf = weakSelf;
    if (!strongSelf) {
      return;
    }
    NSArray *params = @[ key ];
    const char *SQL = "DELETE FROM " RCNTableNameExperiment " WHERE key = ?";
    [strongSelf executeQuery:SQL withParams:params];
  });
}

#pragma mark - helper
- (BOOL)executeQuery:(const char *)SQL withParams:(NSArray *)params {
  RCN_MUST_NOT_BE_MAIN_THREAD();
  sqlite3_stmt *statement = [self prepareSQL:SQL];
  if (!statement) {
    return NO;
  }

  [self bindStringsToStatement:statement stringArray:params];
  if (sqlite3_step(statement) != SQLITE_DONE) {
    return [self logErrorWithSQL:SQL finalizeStatement:statement returnValue:NO];
  }
  sqlite3_finalize(statement);
  return YES;
}

/// Params only accept TEXT format string.
- (BOOL)bindStringsToStatement:(sqlite3_stmt *)statement stringArray:(NSArray *)array {
  int index = 1;
  for (NSString *param in array) {
    if (![self bindStringToStatement:statement index:index string:param]) {
      return [self logErrorWithSQL:nil finalizeStatement:statement returnValue:NO];
    }
    index++;
  }
  return YES;
}

- (BOOL)bindStringToStatement:(sqlite3_stmt *)statement index:(int)index string:(NSString *)value {
  if (sqlite3_bind_text(statement, index, [value UTF8String], -1, SQLITE_TRANSIENT) != SQLITE_OK) {
    return [self logErrorWithSQL:nil finalizeStatement:statement returnValue:NO];
  }
  return YES;
}

- (sqlite3_stmt *)prepareSQL:(const char *)SQL {
  sqlite3_stmt *statement = nil;
  if (sqlite3_prepare_v2(_database, SQL, -1, &statement, NULL) != SQLITE_OK) {
    [self logErrorWithSQL:SQL finalizeStatement:statement returnValue:NO];
    return nil;
  }
  return statement;
}

- (NSString *)errorMessage {
  return [NSString stringWithFormat:@"%s", sqlite3_errmsg(_database)];
}

- (int)errorCode {
  return sqlite3_errcode(_database);
}

- (void)logDatabaseError {
  FIRLogError(kFIRLoggerRemoteConfig, @"I-RCN000015", @"Error message: %@. Error code: %d.",
              [self errorMessage], [self errorCode]);
}

- (BOOL)logErrorWithSQL:(const char *)SQL
      finalizeStatement:(sqlite3_stmt *)statement
            returnValue:(BOOL)returnValue {
  if (SQL) {
    FIRLogError(kFIRLoggerRemoteConfig, @"I-RCN000016", @"Failed with SQL: %s.", SQL);
  }
  [self logDatabaseError];

  if (statement) {
    sqlite3_finalize(statement);
  }

  return returnValue;
}

- (BOOL)isNewDatabase {
  return gIsNewDatabase;
}

@end
