/*
 * Copyright 2017 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 "FIRMessagingRmq2PersistentStore.h"

#import <sqlite3.h>

#import "FIRMessagingConstants.h"
#import "FIRMessagingDefines.h"
#import "FIRMessagingLogger.h"
#import "FIRMessagingPersistentSyncMessage.h"
#import "FIRMessagingUtilities.h"
#import "NSError+FIRMessaging.h"
#import "Protos/GtalkCore.pbobjc.h"

#ifndef _FIRMessagingRmqLogAndExit
#define _FIRMessagingRmqLogAndExit(stmt, return_value)   \
do {                              \
[self logErrorAndFinalizeStatement:stmt];  \
return return_value; \
} while(0)
#endif

typedef enum : NSUInteger {
  FIRMessagingRmqDirectoryUnknown,
  FIRMessagingRmqDirectoryDocuments,
  FIRMessagingRmqDirectoryApplicationSupport,
} FIRMessagingRmqDirectory;

static NSString *const kFCMRmqStoreTag = @"FIRMessagingRmqStore:";

// table names
NSString *const kTableOutgoingRmqMessages = @"outgoingRmqMessages";
NSString *const kTableLastRmqId = @"lastrmqid";
NSString *const kOldTableS2DRmqIds = @"s2dRmqIds";
NSString *const kTableS2DRmqIds = @"s2dRmqIds_1";

// Used to prevent de-duping of sync messages received both via APNS and MCS.
NSString *const kTableSyncMessages = @"incomingSyncMessages";

static NSString *const kTablePrefix = @"";

// create tables
static NSString *const kCreateTableOutgoingRmqMessages =
    @"create TABLE IF NOT EXISTS %@%@ "
    @"(_id INTEGER PRIMARY KEY, "
    @"rmq_id INTEGER, "
    @"type INTEGER, "
    @"ts INTEGER, "
    @"data BLOB)";

static NSString *const kCreateTableLastRmqId =
    @"create TABLE IF NOT EXISTS %@%@ "
    @"(_id INTEGER PRIMARY KEY, "
    @"rmq_id INTEGER)";

static NSString *const kCreateTableS2DRmqIds =
    @"create TABLE IF NOT EXISTS %@%@ "
    @"(_id INTEGER PRIMARY KEY, "
    @"rmq_id TEXT)";

static NSString *const kCreateTableSyncMessages =
    @"create TABLE IF NOT EXISTS %@%@ "
    @"(_id INTEGER PRIMARY KEY, "
    @"rmq_id TEXT, "
    @"expiration_ts INTEGER, "
    @"apns_recv INTEGER, "
    @"mcs_recv INTEGER)";

static NSString *const kDropTableCommand =
    @"drop TABLE if exists %@%@";

// table infos
static NSString *const kRmqIdColumn = @"rmq_id";
static NSString *const kDataColumn = @"data";
static NSString *const kProtobufTagColumn = @"type";
static NSString *const kIdColumn = @"_id";

static NSString *const kOutgoingRmqMessagesColumns = @"rmq_id, type, data";

// Sync message columns
static NSString *const kSyncMessagesColumns = @"rmq_id, expiration_ts, apns_recv, mcs_recv";
// Message time expiration in seconds since 1970
static NSString *const kSyncMessageExpirationTimestampColumn = @"expiration_ts";
static NSString *const kSyncMessageAPNSReceivedColumn = @"apns_recv";
static NSString *const kSyncMessageMCSReceivedColumn = @"mcs_recv";

// table data handlers
typedef void(^FCMOutgoingRmqMessagesTableHandler)(int64_t rmqId, int8_t tag, NSData *data);

// Utility to create an NSString from a sqlite3 result code
NSString * _Nonnull FIRMessagingStringFromSQLiteResult(int result) {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wunguarded-availability"
  const char *errorStr = sqlite3_errstr(result);
#pragma pop
  NSString *errorString = [NSString stringWithFormat:@"%d - %s", result, errorStr];
  return errorString;
}

@interface FIRMessagingRmq2PersistentStore () {
  sqlite3 *_database;
}

@property(nonatomic, readwrite, strong) NSString *databaseName;
@property(nonatomic, readwrite, assign) FIRMessagingRmqDirectory currentDirectory;

@end

@implementation FIRMessagingRmq2PersistentStore

- (instancetype)initWithDatabaseName:(NSString *)databaseName {
  self = [super init];
  if (self) {
    _databaseName = [databaseName copy];
    BOOL didMoveToApplicationSupport =
        [self moveToApplicationSupportSubDirectory:kFIRMessagingApplicationSupportSubDirectory];

    _currentDirectory = didMoveToApplicationSupport
                            ? FIRMessagingRmqDirectoryApplicationSupport
                            : FIRMessagingRmqDirectoryDocuments;

    [self openDatabase:_databaseName];
  }
  return self;
}

- (void)dealloc {
  sqlite3_close(_database);
}

- (BOOL)moveToApplicationSupportSubDirectory:(NSString *)subDirectoryName {
  NSArray *directoryPaths = NSSearchPathForDirectoriesInDomains(NSApplicationSupportDirectory,
                                                                NSUserDomainMask, YES);
  NSString *applicationSupportDirPath = directoryPaths.lastObject;
  NSArray *components = @[applicationSupportDirPath, subDirectoryName];
  NSString *subDirectoryPath = [NSString pathWithComponents:components];
  BOOL hasSubDirectory;

  if (![[NSFileManager defaultManager] fileExistsAtPath:subDirectoryPath
                                            isDirectory:&hasSubDirectory]) {
    // Cannot move to non-existent directory
    return NO;
  }

  if ([self doesFileExistInDirectory:FIRMessagingRmqDirectoryDocuments]) {
    NSString *oldPlistPath = [[self class] pathForDatabase:self.databaseName
                                               inDirectory:FIRMessagingRmqDirectoryDocuments];
    NSString *newPlistPath = [[self class]
        pathForDatabase:self.databaseName
            inDirectory:FIRMessagingRmqDirectoryApplicationSupport];

    if ([self doesFileExistInDirectory:FIRMessagingRmqDirectoryApplicationSupport]) {
      // File exists in both Documents and ApplicationSupport, delete the one in Documents
      NSError *deleteError;
      if (![[NSFileManager defaultManager] removeItemAtPath:oldPlistPath error:&deleteError]) {
        FIRMessagingLoggerError(kFIRMessagingMessageCodeRmq2PersistentStore000,
                                @"Failed to delete old copy of %@.sqlite in Documents %@",
                                self.databaseName, deleteError);
      }
      return NO;
    }
    NSError *moveError;
    if (![[NSFileManager defaultManager] moveItemAtPath:oldPlistPath
                                                 toPath:newPlistPath
                                                  error:&moveError]) {
      FIRMessagingLoggerError(kFIRMessagingMessageCodeRmq2PersistentStore001,
                              @"Failed to move file %@ from %@ to %@. Error: %@", self.databaseName,
                              oldPlistPath, newPlistPath, moveError);
      return NO;
    }
  }
  // We moved the file if it existed, otherwise we didn't need to do anything
  return YES;
}

- (BOOL)doesFileExistInDirectory:(FIRMessagingRmqDirectory)directory {
  NSString *path = [[self class] pathForDatabase:self.databaseName inDirectory:directory];
  return [[NSFileManager defaultManager] fileExistsAtPath:path];
}

+ (NSString *)pathForDatabase:(NSString *)dbName inDirectory:(FIRMessagingRmqDirectory)directory {
  NSArray *paths;
  NSArray *components;
  NSString *dbNameWithExtension = [NSString stringWithFormat:@"%@.sqlite", dbName];
  NSString *errorMessage;

  switch (directory) {
    case FIRMessagingRmqDirectoryDocuments:
      paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
      components = @[paths.lastObject, dbNameWithExtension];
      break;

    case FIRMessagingRmqDirectoryApplicationSupport:
      paths = NSSearchPathForDirectoriesInDomains(NSApplicationSupportDirectory,
                                                  NSUserDomainMask,
                                                  YES);
      components = @[
                     paths.lastObject,
                     kFIRMessagingApplicationSupportSubDirectory,
                     dbNameWithExtension
                     ];
      break;

    default:
      errorMessage = [NSString stringWithFormat:@"Invalid directory type %lu",
                      (unsigned long)directory];
      FIRMessagingLoggerError(kFIRMessagingMessageCodeRmq2PersistentStoreInvalidRmqDirectory,
                              @"%@",
                              errorMessage);
      NSAssert(NO, errorMessage);
      break;
  }

  return [NSString pathWithComponents:components];
}

- (void)createTableWithName:(NSString *)tableName command:(NSString *)command {
  char *error;
  NSString *createDatabase = [NSString stringWithFormat:command, kTablePrefix, tableName];
  if (sqlite3_exec(_database, [createDatabase UTF8String], NULL, NULL, &error) != SQLITE_OK) {
    // remove db before failing
    [self removeDatabase];
    NSString *errorMessage = [NSString stringWithFormat:@"Couldn't create table: %@ %@",
                              kCreateTableOutgoingRmqMessages,
                              [NSString stringWithCString:error encoding:NSUTF8StringEncoding]];
    FIRMessagingLoggerError(kFIRMessagingMessageCodeRmq2PersistentStoreErrorCreatingTable,
                            @"%@",
                            errorMessage);
    NSAssert(NO, errorMessage);
  }
}

- (void)dropTableWithName:(NSString *)tableName {
  char *error;
  NSString *dropTableSQL = [NSString stringWithFormat:kDropTableCommand, kTablePrefix, tableName];
  if (sqlite3_exec(_database, [dropTableSQL UTF8String], NULL, NULL, &error) != SQLITE_OK) {
    FIRMessagingLoggerError(kFIRMessagingMessageCodeRmq2PersistentStore002,
                            @"Failed to remove table %@", tableName);
  }
}

- (void)removeDatabase {
  NSString *path = [[self class] pathForDatabase:self.databaseName
                                     inDirectory:self.currentDirectory];
  [[NSFileManager defaultManager] removeItemAtPath:path error:nil];
}

+ (void)removeDatabase:(NSString *)dbName {
  NSString *documentsDirPath = [self pathForDatabase:dbName
                                         inDirectory:FIRMessagingRmqDirectoryDocuments];
  NSString *applicationSupportDirPath =
      [self pathForDatabase:dbName inDirectory:FIRMessagingRmqDirectoryApplicationSupport];
  [[NSFileManager defaultManager] removeItemAtPath:documentsDirPath error:nil];
  [[NSFileManager defaultManager] removeItemAtPath:applicationSupportDirPath error:nil];
}

- (void)openDatabase:(NSString *)dbName {
  NSFileManager *fileManager = [NSFileManager defaultManager];
  NSString *path = [[self class] pathForDatabase:dbName inDirectory:self.currentDirectory];

  BOOL didOpenDatabase = YES;
  if (![fileManager fileExistsAtPath:path]) {
    // We've to separate between different versions here because of backwards compatbility issues.
    int result = sqlite3_open([path UTF8String], &_database);
    if (result != SQLITE_OK) {
      NSString *errorString = FIRMessagingStringFromSQLiteResult(result);
      NSString *errorMessage =
          [NSString stringWithFormat:@"Could not open existing RMQ database at path %@, error: %@",
                                     path,
                                     errorString];
      FIRMessagingLoggerError(kFIRMessagingMessageCodeRmq2PersistentStoreErrorOpeningDatabase,
                              @"%@",
                              errorMessage);
      NSAssert(NO, errorMessage);
      return;
    }
    [self createTableWithName:kTableOutgoingRmqMessages
                      command:kCreateTableOutgoingRmqMessages];

    [self createTableWithName:kTableLastRmqId command:kCreateTableLastRmqId];
    [self createTableWithName:kTableS2DRmqIds command:kCreateTableS2DRmqIds];
  } else {
    // Calling sqlite3_open should create the database, since the file doesn't exist.
    int result = sqlite3_open([path UTF8String], &_database);
    if (result != SQLITE_OK) {
      NSString *errorString = FIRMessagingStringFromSQLiteResult(result);
      NSString *errorMessage =
          [NSString stringWithFormat:@"Could not create RMQ database at path %@, error: %@",
                                     path,
                                     errorString];
      FIRMessagingLoggerError(kFIRMessagingMessageCodeRmq2PersistentStoreErrorCreatingDatabase,
                              @"%@",
                              errorMessage);
      NSAssert(NO, errorMessage);
      didOpenDatabase = NO;
    } else {
      [self updateDbWithStringRmqID];
    }
  }

  if (didOpenDatabase) {
    [self createTableWithName:kTableSyncMessages command:kCreateTableSyncMessages];
  }
}

- (void)updateDbWithStringRmqID {
  [self createTableWithName:kTableS2DRmqIds command:kCreateTableS2DRmqIds];
  [self dropTableWithName:kOldTableS2DRmqIds];
}

#pragma mark - Insert

- (BOOL)saveUnackedS2dMessageWithRmqId:(NSString *)rmqId {
  NSString *insertFormat = @"INSERT INTO %@ (%@) VALUES (?)";
  NSString *insertSQL = [NSString stringWithFormat:insertFormat,
                         kTableS2DRmqIds,
                         kRmqIdColumn];
  sqlite3_stmt *insert_statement;
  if (sqlite3_prepare_v2(_database, [insertSQL UTF8String], -1, &insert_statement, NULL)
      != SQLITE_OK) {
    _FIRMessagingRmqLogAndExit(insert_statement, NO);
  }
  if (sqlite3_bind_text(insert_statement,
                        1,
                        [rmqId UTF8String],
                        (int)[rmqId length],
                        SQLITE_STATIC) != SQLITE_OK) {
    _FIRMessagingRmqLogAndExit(insert_statement, NO);
  }
  if (sqlite3_step(insert_statement) != SQLITE_DONE) {
    _FIRMessagingRmqLogAndExit(insert_statement, NO);
  }
  sqlite3_finalize(insert_statement);
  return YES;
}

- (BOOL)saveMessageWithRmqId:(int64_t)rmqId
                         tag:(int8_t)tag
                        data:(NSData *)data
                       error:(NSError **)error {
  NSString *insertFormat = @"INSERT INTO %@ (%@, %@, %@) VALUES (?, ?, ?)";
  NSString *insertSQL = [NSString stringWithFormat:insertFormat,
                         kTableOutgoingRmqMessages, // table
                         kRmqIdColumn, kProtobufTagColumn, kDataColumn /* columns */];
  sqlite3_stmt *insert_statement;
  if (sqlite3_prepare_v2(_database, [insertSQL UTF8String], -1, &insert_statement, NULL)
      != SQLITE_OK) {
    if (error) {
      *error = [NSError errorWithDomain:[NSString stringWithFormat:@"%s", sqlite3_errmsg(_database)]
                                   code:sqlite3_errcode(_database)
                               userInfo:nil];
    }
    _FIRMessagingRmqLogAndExit(insert_statement, NO);
  }
  if (sqlite3_bind_int64(insert_statement, 1, rmqId) != SQLITE_OK) {
    _FIRMessagingRmqLogAndExit(insert_statement, NO);
  }
  if (sqlite3_bind_int(insert_statement, 2, tag) != SQLITE_OK) {
    _FIRMessagingRmqLogAndExit(insert_statement, NO);
  }
  if (sqlite3_bind_blob(insert_statement, 3, [data bytes], (int)[data length], NULL) != SQLITE_OK) {
    _FIRMessagingRmqLogAndExit(insert_statement, NO);
  }
  if (sqlite3_step(insert_statement) != SQLITE_DONE) {
    _FIRMessagingRmqLogAndExit(insert_statement, NO);
  }

  sqlite3_finalize(insert_statement);
  return YES;
}

- (int)deleteMessagesFromTable:(NSString *)tableName
                    withRmqIds:(NSArray *)rmqIds {
  _FIRMessagingDevAssert([tableName isEqualToString:kTableOutgoingRmqMessages] ||
                [tableName isEqualToString:kTableLastRmqId] ||
                [tableName isEqualToString:kTableS2DRmqIds] ||
                [tableName isEqualToString:kTableSyncMessages],
                @"%@: Invalid Table Name %@", kFCMRmqStoreTag, tableName);

  BOOL isRmqIDString = NO;
  // RmqID is a string only for outgoing messages
  if ([tableName isEqualToString:kTableS2DRmqIds] ||
      [tableName isEqualToString:kTableSyncMessages]) {
    isRmqIDString = YES;
  }

  NSMutableString *delete = [NSMutableString stringWithFormat:@"DELETE FROM %@ WHERE ", tableName];

  NSString *toDeleteArgument = [NSString stringWithFormat:@"%@ = ? OR ", kRmqIdColumn];

  int toDelete = (int)[rmqIds count];
  if (toDelete == 0) {
    return 0;
  }
  int maxBatchSize = 100;
  int start = 0;
  int deleteCount = 0;
  while (start < toDelete) {

    // construct the WHERE argument
    int end = MIN(start + maxBatchSize, toDelete);
    NSMutableString *whereArgument = [NSMutableString string];
    for (int i = start; i < end; i++) {
      [whereArgument appendString:toDeleteArgument];
    }
    // remove the last * OR * from argument
    NSRange range = NSMakeRange([whereArgument length] -4, 4);
    [whereArgument deleteCharactersInRange:range];
    NSString *deleteQuery = [NSString stringWithFormat:@"%@ %@", delete, whereArgument];


    // sqlite update
    sqlite3_stmt *delete_statement;
    if (sqlite3_prepare_v2(_database, [deleteQuery UTF8String],
                           -1, &delete_statement, NULL) != SQLITE_OK) {
      _FIRMessagingRmqLogAndExit(delete_statement, 0);
    }

    // bind values
    int rmqIndex = 0;
    int placeholderIndex = 1; // placeholders in sqlite3 start with 1
    for (NSString *rmqId in rmqIds) { // objectAtIndex: is O(n) -- would make it slow
      if (rmqIndex < start) {
        rmqIndex++;
        continue;
      } else if (rmqIndex >= end) {
        break;
      } else {
        if (isRmqIDString) {
          if (sqlite3_bind_text(delete_statement,
                                placeholderIndex,
                                [rmqId UTF8String],
                                (int)[rmqId length],
                                SQLITE_STATIC) != SQLITE_OK) {
            FIRMessagingLoggerDebug(kFIRMessagingMessageCodeRmq2PersistentStore003,
                                    @"Failed to bind rmqID %@", rmqId);
            continue;
          }
        } else {
          int64_t rmqIdValue = [rmqId longLongValue];
          sqlite3_bind_int64(delete_statement, placeholderIndex, rmqIdValue);
        }
        placeholderIndex++;
      }
      rmqIndex++;
    }
    if (sqlite3_step(delete_statement) != SQLITE_DONE) {
      _FIRMessagingRmqLogAndExit(delete_statement, deleteCount);
    }
    sqlite3_finalize(delete_statement);
    deleteCount += sqlite3_changes(_database);
    start = end;
  }

  // if we are here all of our sqlite queries should have succeeded
  FIRMessagingLoggerDebug(kFIRMessagingMessageCodeRmq2PersistentStore004,
                          @"%@ Trying to delete %d s2D ID's, successfully deleted %d",
                          kFCMRmqStoreTag, toDelete, deleteCount);
  return deleteCount;
}

#pragma mark - Query

- (int64_t)queryHighestRmqId {
  NSString *queryFormat = @"SELECT %@ FROM %@ ORDER BY %@ DESC LIMIT %d";
  NSString *query = [NSString stringWithFormat:queryFormat,
                     kRmqIdColumn, // column
                     kTableOutgoingRmqMessages, // table
                     kRmqIdColumn, // order by column
                     1]; // limit

  sqlite3_stmt *statement;
  int64_t highestRmqId = 0;
  if (sqlite3_prepare_v2(_database, [query UTF8String], -1, &statement, NULL) != SQLITE_OK) {
    _FIRMessagingRmqLogAndExit(statement, highestRmqId);
  }
  if (sqlite3_step(statement) == SQLITE_ROW) {
    highestRmqId = sqlite3_column_int64(statement, 0);
  }
  sqlite3_finalize(statement);
  return highestRmqId;
}

- (int64_t)queryLastRmqId {
  NSString *queryFormat = @"SELECT %@ FROM %@ ORDER BY %@ DESC LIMIT %d";
  NSString *query = [NSString stringWithFormat:queryFormat,
                     kRmqIdColumn, // column
                     kTableLastRmqId, // table
                     kRmqIdColumn, // order by column
                     1]; // limit

  sqlite3_stmt *statement;
  int64_t lastRmqId = 0;
  if (sqlite3_prepare_v2(_database, [query UTF8String], -1, &statement, NULL) != SQLITE_OK) {
    _FIRMessagingRmqLogAndExit(statement, lastRmqId);
  }
  if (sqlite3_step(statement) == SQLITE_ROW) {
    lastRmqId = sqlite3_column_int64(statement, 0);
  }
  sqlite3_finalize(statement);
  return lastRmqId;
}

- (BOOL)updateLastOutgoingRmqId:(int64_t)rmqID {
  NSString *queryFormat = @"INSERT OR REPLACE INTO %@ (%@, %@) VALUES (?, ?)";
  NSString *query = [NSString stringWithFormat:queryFormat,
                     kTableLastRmqId, // table
                     kIdColumn, kRmqIdColumn]; // columns
  sqlite3_stmt *statement;
  if (sqlite3_prepare_v2(_database, [query UTF8String], -1, &statement, NULL) != SQLITE_OK) {
    _FIRMessagingRmqLogAndExit(statement, NO);
  }
  if (sqlite3_bind_int(statement, 1, 1) != SQLITE_OK) {
    _FIRMessagingRmqLogAndExit(statement, NO);
  }
  if (sqlite3_bind_int64(statement, 2, rmqID) != SQLITE_OK) {
    _FIRMessagingRmqLogAndExit(statement, NO);
  }
  if (sqlite3_step(statement) != SQLITE_DONE) {
    _FIRMessagingRmqLogAndExit(statement, NO);
  }
  sqlite3_finalize(statement);
  return YES;
}

- (NSArray *)unackedS2dRmqIds {
  NSString *queryFormat = @"SELECT %@ FROM %@ ORDER BY %@ ASC";
  NSString *query = [NSString stringWithFormat:queryFormat,
                     kRmqIdColumn,
                     kTableS2DRmqIds,
                     kRmqIdColumn];
  sqlite3_stmt *statement;
  if (sqlite3_prepare_v2(_database, [query UTF8String], -1, &statement, NULL) != SQLITE_OK) {
    FIRMessagingLoggerDebug(kFIRMessagingMessageCodeRmq2PersistentStore005,
                            @"%@: Could not find s2d ids", kFCMRmqStoreTag);
    _FIRMessagingRmqLogAndExit(statement, @[]);
  }
  NSMutableArray *rmqIDArray = [NSMutableArray array];
  while (sqlite3_step(statement) == SQLITE_ROW) {
    const char *rmqID = (char *)sqlite3_column_text(statement, 0);
    [rmqIDArray addObject:[NSString stringWithUTF8String:rmqID]];
  }
  sqlite3_finalize(statement);
  return rmqIDArray;
}

#pragma mark - Scan

- (void)scanOutgoingRmqMessagesWithHandler:(FCMOutgoingRmqMessagesTableHandler)handler {
  static NSString *queryFormat = @"SELECT %@ FROM %@ WHERE %@ != 0 ORDER BY %@ ASC";
  NSString *query = [NSString stringWithFormat:queryFormat,
                     kOutgoingRmqMessagesColumns, // select (rmq_id, type, data)
                     kTableOutgoingRmqMessages, // from table
                     kRmqIdColumn, // where
                     kRmqIdColumn]; // order by
  sqlite3_stmt *statement;
  if (sqlite3_prepare_v2(_database, [query UTF8String], -1, &statement, NULL) != SQLITE_OK) {
    [self logError];
    sqlite3_finalize(statement);
    return;
  }
  // can query sqlite3 for this but this is fine
  const int rmqIdColumnNumber = 0;
  const int typeColumnNumber = 1;
  const int dataColumnNumber = 2;
  while (sqlite3_step(statement) == SQLITE_ROW) {
    int64_t rmqId = sqlite3_column_int64(statement, rmqIdColumnNumber);
    int8_t type = sqlite3_column_int(statement, typeColumnNumber);
    const void *bytes = sqlite3_column_blob(statement, dataColumnNumber);
    int length = sqlite3_column_bytes(statement, dataColumnNumber);
    _FIRMessagingDevAssert(bytes != NULL,
                           @"%@ Message with no data being stored in Rmq",
                           kFCMRmqStoreTag);
    NSData *data = [NSData dataWithBytes:bytes length:length];
    handler(rmqId, type, data);
  }
  sqlite3_finalize(statement);
}

#pragma mark - Sync Messages

- (FIRMessagingPersistentSyncMessage *)querySyncMessageWithRmqID:(NSString *)rmqID {
  _FIRMessagingDevAssert([rmqID length], @"Invalid rmqID key %@ to search in SYNC_RMQ", rmqID);

  NSString *queryFormat = @"SELECT %@ FROM %@ WHERE %@ = '%@'";
  NSString *query = [NSString stringWithFormat:queryFormat,
                     kSyncMessagesColumns, // SELECT (rmq_id, expiration_ts, apns_recv, mcs_recv)
                     kTableSyncMessages,   // FROM sync_rmq
                     kRmqIdColumn,         // WHERE rmq_id
                     rmqID];

  sqlite3_stmt *stmt;
  if (sqlite3_prepare_v2(_database, [query UTF8String], -1, &stmt, NULL) != SQLITE_OK) {
    [self logError];
    sqlite3_finalize(stmt);
    return nil;
  }

  const int rmqIDColumn = 0;
  const int expirationTimestampColumn = 1;
  const int apnsReceivedColumn = 2;
  const int mcsReceivedColumn = 3;

  int count = 0;
  FIRMessagingPersistentSyncMessage *persistentMessage;

  while (sqlite3_step(stmt) == SQLITE_ROW) {
    NSString *rmqID =
        [NSString stringWithUTF8String:(char *)sqlite3_column_text(stmt, rmqIDColumn)];
    int64_t expirationTimestamp = sqlite3_column_int64(stmt, expirationTimestampColumn);
    BOOL apnsReceived = sqlite3_column_int(stmt, apnsReceivedColumn);
    BOOL mcsReceived = sqlite3_column_int(stmt, mcsReceivedColumn);

    // create a new persistent message
    persistentMessage =
        [[FIRMessagingPersistentSyncMessage alloc] initWithRMQID:rmqID expirationTime:expirationTimestamp];
    persistentMessage.apnsReceived = apnsReceived;
    persistentMessage.mcsReceived = mcsReceived;

    count++;
  }
  sqlite3_finalize(stmt);

  _FIRMessagingDevAssert(count <= 1, @"Found multiple messages in %@ with same RMQ ID", kTableSyncMessages);
  return persistentMessage;
}

- (BOOL)deleteSyncMessageWithRmqID:(NSString *)rmqID {
  _FIRMessagingDevAssert([rmqID length], @"Invalid rmqID key %@ to delete in SYNC_RMQ", rmqID);
  return [self deleteMessagesFromTable:kTableSyncMessages withRmqIds:@[rmqID]] > 0;
}

- (int)deleteExpiredOrFinishedSyncMessages:(NSError *__autoreleasing *)error {
  int64_t now = FIRMessagingCurrentTimestampInSeconds();
  NSString *deleteSQL = @"DELETE FROM %@ "
                        @"WHERE %@ < %lld OR "  // expirationTime < now
                        @"(%@ = 1 AND %@ = 1)";  // apns_received = 1 AND mcs_received = 1
  NSString *query = [NSString stringWithFormat:deleteSQL,
                     kTableSyncMessages,
                     kSyncMessageExpirationTimestampColumn,
                     now,
                     kSyncMessageAPNSReceivedColumn,
                     kSyncMessageMCSReceivedColumn];

  NSString *errorReason = @"Failed to save delete expired sync messages from store.";

  sqlite3_stmt *stmt;
  if (sqlite3_prepare_v2(_database, [query UTF8String], -1, &stmt, NULL) != SQLITE_OK) {
    if (error) {
      *error = [NSError fcm_errorWithCode:sqlite3_errcode(_database)
                                 userInfo:@{ @"error" : errorReason }];
    }
    _FIRMessagingRmqLogAndExit(stmt, 0);
  }

  if (sqlite3_step(stmt) != SQLITE_DONE) {
    if (error) {
      *error = [NSError fcm_errorWithCode:sqlite3_errcode(_database)
                                 userInfo:@{ @"error" : errorReason }];
    }
    _FIRMessagingRmqLogAndExit(stmt, 0);
  }

  sqlite3_finalize(stmt);
  int deleteCount = sqlite3_changes(_database);
  return deleteCount;
}

- (BOOL)saveSyncMessageWithRmqID:(NSString *)rmqID
                  expirationTime:(int64_t)expirationTime
                    apnsReceived:(BOOL)apnsReceived
                     mcsReceived:(BOOL)mcsReceived
                           error:(NSError **)error {
  _FIRMessagingDevAssert([rmqID length], @"Invalid nil message to persist to SYNC_RMQ");

  NSString *insertFormat = @"INSERT INTO %@ (%@, %@, %@, %@) VALUES (?, ?, ?, ?)";
  NSString *insertSQL = [NSString stringWithFormat:insertFormat,
                         kTableSyncMessages, // Table name
                         kRmqIdColumn, // rmq_id
                         kSyncMessageExpirationTimestampColumn, // expiration_ts
                         kSyncMessageAPNSReceivedColumn, // apns_recv
                         kSyncMessageMCSReceivedColumn /* mcs_recv */];

  sqlite3_stmt *stmt;

  if (sqlite3_prepare_v2(_database, [insertSQL UTF8String], -1, &stmt, NULL) != SQLITE_OK) {
    if (error) {
      *error = [NSError fcm_errorWithCode:sqlite3_errcode(_database)
                                 userInfo:@{ @"error" : @"Failed to save sync message to store." }];
    }
    _FIRMessagingRmqLogAndExit(stmt, NO);
  }

  if (sqlite3_bind_text(stmt, 1, [rmqID UTF8String], (int)[rmqID length], NULL) != SQLITE_OK) {
    _FIRMessagingRmqLogAndExit(stmt, NO);
  }

  if (sqlite3_bind_int64(stmt, 2, expirationTime) != SQLITE_OK) {
    _FIRMessagingRmqLogAndExit(stmt, NO);
  }

  if (sqlite3_bind_int(stmt, 3, apnsReceived ? 1 : 0) != SQLITE_OK) {
    _FIRMessagingRmqLogAndExit(stmt, NO);
  }

  if (sqlite3_bind_int(stmt, 4, mcsReceived ? 1 : 0) != SQLITE_OK) {
    _FIRMessagingRmqLogAndExit(stmt, NO);
  }

  if (sqlite3_step(stmt) != SQLITE_DONE) {
    _FIRMessagingRmqLogAndExit(stmt, NO);
  }

  sqlite3_finalize(stmt);
  return YES;
}

- (BOOL)updateSyncMessageViaAPNSWithRmqID:(NSString *)rmqID
                                    error:(NSError **)error {
  return [self updateSyncMessageWithRmqID:rmqID
                                   column:kSyncMessageAPNSReceivedColumn
                                    value:YES
                                    error:error];
}

- (BOOL)updateSyncMessageViaMCSWithRmqID:(NSString *)rmqID
                                   error:(NSError *__autoreleasing *)error {
  return [self updateSyncMessageWithRmqID:rmqID
                                   column:kSyncMessageMCSReceivedColumn
                                    value:YES
                                    error:error];
}

- (BOOL)updateSyncMessageWithRmqID:(NSString *)rmqID
                            column:(NSString *)column
                             value:(BOOL)value
                             error:(NSError **)error {
  _FIRMessagingDevAssert([column isEqualToString:kSyncMessageAPNSReceivedColumn] ||
                [column isEqualToString:kSyncMessageMCSReceivedColumn],
                @"Invalid column name %@ for SYNC_RMQ", column);
  NSString *queryFormat = @"UPDATE %@ "  // Table name
                          @"SET %@ = %d "  // column=value
                          @"WHERE %@ = ?";  // condition
  NSString *query = [NSString stringWithFormat:queryFormat,
                     kTableSyncMessages,
                     column,
                     value ? 1 : 0,
                     kRmqIdColumn];
  sqlite3_stmt *stmt;

  if (sqlite3_prepare_v2(_database, [query UTF8String], -1, &stmt, NULL) != SQLITE_OK) {
    if (error) {
      *error = [NSError fcm_errorWithCode:sqlite3_errcode(_database)
                                 userInfo:@{ @"error" : @"Failed to update sync message"}];
    }
    _FIRMessagingRmqLogAndExit(stmt, NO);
  }

  if (sqlite3_bind_text(stmt, 1, [rmqID UTF8String], (int)[rmqID length], NULL) != SQLITE_OK) {
    _FIRMessagingRmqLogAndExit(stmt, NO);
  }

  if (sqlite3_step(stmt) != SQLITE_DONE) {
    _FIRMessagingRmqLogAndExit(stmt, NO);
  }

  sqlite3_finalize(stmt);
  return YES;

}

#pragma mark - Private

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

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

- (void)logError {
  FIRMessagingLoggerError(kFIRMessagingMessageCodeRmq2PersistentStore006,
                          @"%@ error: code (%d) message: %@", kFCMRmqStoreTag, [self lastErrorCode],
                          [self lastErrorMessage]);
}

- (void)logErrorAndFinalizeStatement:(sqlite3_stmt *)stmt {
  [self logError];
  sqlite3_finalize(stmt);
}

@end
