// Dart imports:
import 'dart:io';
import 'dart:isolate';

// Flutter imports:
import 'package:flutter/foundation.dart' show kDebugMode;

// Package imports:
import 'package:drift/drift.dart';
import 'package:drift/isolate.dart';
import 'package:drift/native.dart';
import 'package:path/path.dart' as p;
import 'package:path_provider/path_provider.dart';
import 'package:remever/inject.dart';
import 'package:remever/services/core/enc_keys_service.dart';

/// Obtains a database connection for running drift in a Dart VM.
///
/// The [NativeDatabase] from drift will synchronously use sqlite3's C APIs.
/// To move synchronous database work off the main thread, we use a
/// [DriftIsolate], which can run queries in a background isolate under the
/// hood.
///
DatabaseConnection connect() {
  return DatabaseConnection.delayed(
    Future<DatabaseConnection>.sync(() async {
      // Background isolates can't use platform channels, so let's use
      // `path_provider` in the main isolate and just send the result containing
      // the path over to the background isolate.
      final Directory appDir = await getApplicationDocumentsDirectory();
      final Directory dbDir = Directory(p.join(appDir.path, 'databases'));

      if (!dbDir.existsSync()) {
        await dbDir.create(recursive: true);
      }

      final String dbPath = p.join(appDir.path, 'databases', 'app.db.enc');

      final EncKeysService encKeyService = getIt<EncKeysService>();
      final String key = await encKeyService.getRawKey('db');

      final ReceivePort receiveDriftIsolate = ReceivePort();
      await Isolate.spawn(
        _entrypointForDriftIsolate,
        _IsolateStartRequest(receiveDriftIsolate.sendPort, dbPath, key),
      );

      final DriftIsolate driftIsolate =
          await receiveDriftIsolate.first as DriftIsolate;

      return driftIsolate.connect();
    }),
  );
}

/// The entrypoint of isolates can only take a single message, but we need two
/// (a send port to reach the originating isolate and the database's path that
/// should be opened on the background isolate). So, we bundle this information
/// in a single class.
class _IsolateStartRequest {
  final SendPort talkToMain;
  final String databasePath;
  final String key;

  _IsolateStartRequest(this.talkToMain, this.databasePath, this.key);
}

/// The entrypoint for a background isolate launching a drift server.
///
/// The main isolate can then connect to that isolate server to transparently
/// run queries in the background.
void _entrypointForDriftIsolate(_IsolateStartRequest request) {
  /// @see https://drift.simonbinder.eu/docs/other-engines/encryption/
  // open
  //   ..overrideFor(OperatingSystem.android, openCipherOnAndroid)
  //   ..overrideFor(
  //     OperatingSystem.linux,
  //     () => DynamicLibrary.open('libsqlcipher.so'),
  //   )
  //   ..overrideFor(
  //     OperatingSystem.windows,
  //     () => DynamicLibrary.open('sqlcipher.dll'),
  //   );

  if (kDebugMode) {
    print('[DRIFT] Path -> ${request.databasePath}');
  }

  // The native database synchronously uses sqlite3's C API with `dart:ffi` for
  // a fast database implementation that doesn't require platform channels.
  final NativeDatabase databaseImpl = NativeDatabase(
    File(request.databasePath),
    logStatements: false,
    // setup: (Database db) {
    //   // Check that we're actually running with SQLCipher by quering the
    //   // cipher_version pragma.

    //   if (Platform.isAndroid) {
    //     final ResultSet result = db.select('pragma cipher_version');

    //     if (result.isEmpty) {
    //       throw UnsupportedError(
    //         'This database needs to run with SQLCipher, but that library is '
    //         'not available!',
    //       );
    //     }
    //   }

    //   // Then, apply the key to encrypt the database. Unfortunately, this
    //   // pragma doesn't seem to support prepared statements so we inline the
    //   // key.
    //   final String escapedKey = request.key.replaceAll("'", "''");
    //   db.execute("pragma key = '$escapedKey'");

    //   // Test that the key is correct by selecting from a table
    //   db.execute('select count(*) from sqlite_master');
    // },
  );

  // We can use DriftIsolate.inCurrent because this function is the entrypoint
  // of a background isolate itself.
  final DriftIsolate driftServer = DriftIsolate.inCurrent(
    () => DatabaseConnection(databaseImpl),
  );

  // Inform the main isolate about the server we just created.
  request.talkToMain.send(driftServer);
}
