import 'dart:async';
import 'dart:io';
import 'dart:math';

import 'package:flutter/foundation.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:path/path.dart' as p;
import 'package:path_provider/path_provider.dart';
import 'package:sqflite/sqflite.dart' as sqflite_native;
import 'package:sqflite_common_ffi/sqflite_ffi.dart';
import 'package:sqlcipher_flutter_libs/sqlcipher_flutter_libs.dart';
import 'package:sqlite3/open.dart';
import 'package:pyramid/core/auth_log.dart';

/// Läuft im DB-Isolate der FFI-Factory (muss deshalb top-level sein):
/// Auf Android liefert das sqlcipher_flutter_libs-Plugin die Bibliothek als
/// `libsqlcipher.so` – dem sqlite3-Paket muss man das dort sagen. Auf
/// Windows/Linux bündelt dasselbe Plugin SQLCipher bereits UNTER DEM
/// STANDARDNAMEN (`sqlite3.dll` neben der exe, verifiziert im Build-Output),
/// der Default-Loader findet sie also ohne Override.
void _sqlcipherFfiInit() {
  open.overrideFor(OperatingSystem.android, openCipherOnAndroid);
}

/// Gemeinsamer Öffner für die `pyramid.sqlite` – der eine Storage-Baustein,
/// hinter dem Factory-Wahl, Verschlüsselung und Migration stecken (Plan:
/// `docs/SQLCIPHER_MIGRATION.md`, Modularitäts-Leitsatz aus CLAUDE.md).
///
/// Kapselt an EINER Stelle, was vorher in `matrix_client.dart` und
/// `background_push.dart` dupliziert war:
///  * Factory-Wahl pro Plattform (FFI + SQLCipher wo verfügbar),
///  * Schlüsselbeschaffung: 32 zufällige Bytes, hex-kodiert im
///    Plattform-Keystore (`flutter_secure_storage`, Eintrag `db_cipher_key`),
///  * einmalige Migration bestehender Klartext-DBs nach SQLCipher,
///  * die PRAGMAs (WAL, busy_timeout).
///
/// Oberste Regel (CLAUDE.md „Kein Datenverlust"): KEIN Codepfad darf eine
/// bestehende DB beschädigen oder still eine leere neue anlegen. Im Zweifel
/// läuft die App unverschlüsselt weiter und der Fehler steht im auth_log.
class AppDatabase {
  AppDatabase._();

  static const _cipherKeyStorageKey = 'db_cipher_key';

  /// Klartext-Backup `*.premigration` nach so vielen Tagen automatisch
  /// aufräumen (Kompromiss aus dem Migrationsplan: Rollback-Fenster vs.
  /// dauerhaft herumliegende Klartext-Kopie).
  static const _premigrationMaxAge = Duration(days: 7);

  /// Plattformen, auf denen sqlcipher_flutter_libs die Cipher-Bibliothek
  /// mitliefert. iOS/macOS bleiben bewusst beim nativen sqflite-Plugin
  /// (Klartext): der SQLCipher-Pod kollidiert dort mit Paketen, die das
  /// System-sqlite3 linken (z. B. firebase_messaging, siehe Paket-README),
  /// und iOS ist kein Release-Ziel von Pyramid.
  static bool get _isCipherPlatform =>
      !kIsWeb && (Platform.isAndroid || Platform.isWindows || Platform.isLinux);

  static Future<String> _dbPath() async {
    final appDir = await getApplicationSupportDirectory();
    return p.join(appDir.path, 'pyramid.sqlite');
  }

  // ── Öffnen: Haupt-App ────────────────────────────────────────────────────

  /// Öffnet die Datenbank für die Haupt-App. Nur dieser Pfad migriert eine
  /// bestehende Klartext-DB (das Push-Isolate migriert NIE selbst).
  static Future<Database> open() async {
    final dbPath = await _dbPath();

    if (!_isCipherPlatform) {
      // iOS/macOS: unverändert das native sqflite-Plugin, Klartext.
      final db = await sqflite_native.databaseFactory.openDatabase(dbPath);
      await _applyPragmas(db);
      return db;
    }

    if (Platform.isAndroid) {
      // Lade-Workaround für Android 6 (Paket-README) – im Main-Isolate
      // ausführen, BEVOR irgendein Isolate sqlite benutzt.
      try {
        await applyWorkaroundToOpenSqlCipherOnOldAndroidVersions();
      } catch (_) {}
    }

    final factory = createDatabaseFactoryFfi(ffiInit: _sqlcipherFfiInit);

    await _recoverFromInterruptedSwap(dbPath);
    await _clearStaleMarker(dbPath);

    final dbFile = File(dbPath);
    final exists = await dbFile.exists();
    final isPlaintext = exists && await isPlaintextSqlite(dbFile);

    if (!await _cipherAvailable(factory)) {
      // Sicherheitsnetz: sollte auf Cipher-Plattformen nie passieren
      // (Bibliothek wird als Plugin mitgebaut). Eine VERSCHLÜSSELTE DB ohne
      // Cipher-Lib ist ein harter Fehler – niemals still neu anlegen.
      if (exists && !isPlaintext) {
        throw StateError(
          'pyramid.sqlite ist verschlüsselt, aber die SQLCipher-Bibliothek '
          'fehlt in diesem Build. Kein Zugriff möglich – Daten bleiben '
          'unangetastet. Bitte App-Build prüfen (sqlcipher_flutter_libs).',
        );
      }
      await AuthLog.write(
        '[DB] SQLCipher nicht verfügbar – Datenbank bleibt unverschlüsselt.',
      );
      final db = await factory.openDatabase(dbPath);
      await _applyPragmas(db);
      return db;
    }

    if (exists && !isPlaintext) {
      // Bereits verschlüsselt: der Schlüssel MUSS im Keystore liegen.
      final key = await _readStoredKey();
      if (key == null || key.isEmpty) {
        throw StateError(
          'pyramid.sqlite ist verschlüsselt, aber im Schlüsselspeicher fehlt '
          '"$_cipherKeyStorageKey". Es wird KEINE neue Datenbank angelegt, '
          'damit keine Daten verloren gehen. Schlüsselspeicher prüfen '
          '(flutter_secure_storage) oder Backup einspielen.',
        );
      }
      final db = await _openWithKey(factory, dbPath, key);
      await _applyPragmas(db);
      await _cleanupOldBackup(dbPath);
      return db;
    }

    final key = await _getOrCreateKey();

    if (exists && isPlaintext) {
      final migrated = await migratePlaintextToEncrypted(
        factory: factory,
        dbPath: dbPath,
        hexKey: key,
      );
      if (!migrated) {
        // Migration fehlgeschlagen → Klartext-DB ist unversehrt, App läuft
        // wie bisher weiter (Fehlerdetails stehen im auth_log).
        final db = await factory.openDatabase(dbPath);
        await _applyPragmas(db);
        return db;
      }
    }

    // Frische Installation (legt verschlüsselt neu an) oder frisch migriert.
    final db = await _openWithKey(factory, dbPath, key);
    await _applyPragmas(db);
    return db;
  }

  // ── Öffnen: Android-Push-Isolate ─────────────────────────────────────────

  /// Öffnet die Datenbank für das Push-/Hintergrund-Isolate. Gibt `null`
  /// zurück, wenn gerade kein sicherer Zugriff möglich ist (Migration läuft,
  /// DB fehlt, Schlüssel fehlt) – die Notification zeigt dann einmalig den
  /// nativen „Neue Nachricht"-Platzhalter, das ist der akzeptierte Preis.
  /// Dieser Pfad migriert NIE und legt NIE eine neue Datei an.
  static Future<Database?> openForPush() async {
    final dbPath = await _dbPath();

    if (await File('$dbPath.migrating').exists()) {
      debugPrint('[DB] Migration läuft – Push-Zugriff übersprungen.');
      return null;
    }

    final dbFile = File(dbPath);
    if (!await dbFile.exists()) return null;

    if (!_isCipherPlatform) {
      final db = await sqflite_native.databaseFactory.openDatabase(dbPath);
      await _applyPragmas(db);
      return db;
    }

    // noIsolate: databaseFactoryFfi würde ein Unter-Isolate spawnen – genau
    // die Fallenklasse, an der im Firebase-Background-Isolate schon
    // NativeImplementationsIsolate scheiterte (siehe background_push.dart).
    final factory =
        createDatabaseFactoryFfi(ffiInit: _sqlcipherFfiInit, noIsolate: true);

    if (await isPlaintextSqlite(dbFile)) {
      // Noch nicht migriert (macht beim nächsten Start die Haupt-App).
      final db = await factory.openDatabase(dbPath);
      await _applyPragmas(db);
      return db;
    }

    final key = await _readStoredKey();
    if (key == null || key.isEmpty) {
      // Bekanntes Risiko aus dem Plan: secure_storage aus dem
      // Background-Isolate. Niemals raten oder neu anlegen – nur aussteigen.
      debugPrint('[DB] Kein Cipher-Schlüssel im Push-Isolate lesbar.');
      return null;
    }
    final db = await _openWithKey(factory, dbPath, key);
    await _applyPragmas(db);
    return db;
  }

  // ── Migration Klartext → SQLCipher ───────────────────────────────────────

  /// Wandelt eine bestehende Klartext-DB in eine SQLCipher-DB um – der heikle
  /// Teil des Plans. Reihenfolge exakt wie in `docs/SQLCIPHER_MIGRATION.md`:
  /// Marker → WAL-Checkpoint → Byte-Backup → Export in NEUE Datei →
  /// Verifikation (integrity_check + Zeilenzahlen ALLER Tabellen) → atomarer
  /// Tausch. Schlägt irgendetwas fehl, bleibt die Klartext-DB unangetastet
  /// und die Funktion gibt `false` zurück.
  @visibleForTesting
  static Future<bool> migratePlaintextToEncrypted({
    required DatabaseFactory factory,
    required String dbPath,
    required String hexKey,
  }) async {
    final marker = File('$dbPath.migrating');
    final encPath = '$dbPath.enc';
    final backupPath = '$dbPath.premigration';
    try {
      // Marker zuerst: hält das Push-Isolate fern (Race aus dem Plan).
      await marker.writeAsString(DateTime.now().toIso8601String());

      // 1. WAL eindampfen – sonst fehlen im Export Daten, die noch in
      //    pyramid.sqlite-wal liegen.
      var db = await factory.openDatabase(dbPath);
      try {
        await db.rawQuery('PRAGMA wal_checkpoint(TRUNCATE)');
      } finally {
        await db.close();
      }

      // 2. Byte-Backup (Kopie, kein rename) – bleibt bis zum verifizierten
      //    Erfolg + Aufräumfrist liegen.
      await File(dbPath).copy(backupPath);

      // 3. Export in NEUE Datei; das Original wird nie beschrieben.
      final encFile = File(encPath);
      if (await encFile.exists()) await encFile.delete();
      final Map<String, int> sourceCounts;
      db = await factory.openDatabase(dbPath);
      try {
        sourceCounts = await _tableRowCounts(db);
        await db.rawQuery(
          'ATTACH DATABASE \'${_escapeSqlString(encPath)}\' AS enc '
          'KEY "x\'$hexKey\'"',
        );
        await db.rawQuery("SELECT sqlcipher_export('enc')");
        await db.rawQuery('DETACH DATABASE enc');
      } finally {
        await db.close();
      }

      // 4. Verifikation der NEUEN Datei, bevor irgendetwas getauscht wird.
      final encDb = await _openWithKey(factory, encPath, hexKey);
      try {
        final integrity = await encDb.rawQuery('PRAGMA integrity_check');
        final integrityOk = integrity.length == 1 &&
            integrity.first.values.first.toString() == 'ok';
        if (!integrityOk) {
          throw StateError('integrity_check fehlgeschlagen: $integrity');
        }
        final encCounts = await _tableRowCounts(encDb);
        for (final entry in sourceCounts.entries) {
          if (encCounts[entry.key] != entry.value) {
            throw StateError(
              'Zeilenzahl weicht ab in "${entry.key}": '
              '${entry.value} (Original) != ${encCounts[entry.key]} (Export)',
            );
          }
        }
      } finally {
        await encDb.close();
      }

      // 5. Tausch. rename auf derselben Partition ist atomar; für das
      //    Zeitfenster zwischen delete und rename existiert das Byte-Backup
      //    (Wiederanlauf repariert das, siehe _recoverFromInterruptedSwap).
      for (final suffix in ['-wal', '-shm']) {
        final sidecar = File('$dbPath$suffix');
        if (await sidecar.exists()) await sidecar.delete();
      }
      await File(dbPath).delete();
      await File(encPath).rename(dbPath);

      await AuthLog.write(
        '[DB] Klartext-DB erfolgreich nach SQLCipher migriert '
        '(${sourceCounts.length} Tabellen verifiziert).',
      );
      return true;
    } catch (e) {
      // Aufräumen: halbfertigen Export löschen, Original bleibt maßgeblich.
      try {
        final encFile = File(encPath);
        if (await encFile.exists()) await encFile.delete();
      } catch (_) {}
      await AuthLog.write(
        '[DB] SQLCipher-Migration fehlgeschlagen – Klartext-DB bleibt '
        'unangetastet: $e',
      );
      return false;
    } finally {
      try {
        if (await marker.exists()) await marker.delete();
      } catch (_) {}
    }
  }

  /// Absturz exakt zwischen „Original gelöscht" und „Export umbenannt":
  /// dann fehlt `pyramid.sqlite`, aber das Byte-Backup existiert. Backup
  /// zurückkopieren (die Migration läuft danach einfach erneut). Ein evtl.
  /// liegengebliebener Export ist nicht als vollständig beweisbar → löschen.
  static Future<void> _recoverFromInterruptedSwap(String dbPath) async {
    final backup = File('$dbPath.premigration');
    if (await File(dbPath).exists() || !await backup.exists()) return;
    try {
      final enc = File('$dbPath.enc');
      if (await enc.exists()) await enc.delete();
      await backup.copy(dbPath);
      await AuthLog.write(
        '[DB] Unterbrochene Migration erkannt – Klartext-Backup '
        'wiederhergestellt, Migration läuft erneut.',
      );
    } catch (e) {
      await AuthLog.write('[DB] Wiederherstellung aus .premigration fehlgeschlagen: $e');
    }
  }

  /// Ein Marker ohne laufende Migration ist ein Absturz-Überbleibsel – beim
  /// Start der Haupt-App (es gibt nur eine) gefahrlos entfernbar.
  static Future<void> _clearStaleMarker(String dbPath) async {
    try {
      final marker = File('$dbPath.migrating');
      if (await marker.exists()) {
        await marker.delete();
        await AuthLog.write('[DB] Verwaisten Migrations-Marker entfernt.');
      }
    } catch (_) {}
  }

  static Future<void> _cleanupOldBackup(String dbPath) async {
    try {
      final backup = File('$dbPath.premigration');
      if (!await backup.exists()) return;
      final age = DateTime.now().difference((await backup.stat()).modified);
      if (age > _premigrationMaxAge) {
        await backup.delete();
        await AuthLog.write(
          '[DB] Klartext-Backup .premigration nach '
          '${_premigrationMaxAge.inDays} Tagen gelöscht.',
        );
      }
    } catch (_) {}
  }

  // ── Bausteine ────────────────────────────────────────────────────────────

  /// Klartext-SQLite-Dateien beginnen mit dem 16-Byte-Header
  /// `SQLite format 3\0`; SQLCipher-Dateien haben keinen lesbaren Header.
  /// Leere/winzige Dateien zählen als Klartext (die Migration exportiert sie
  /// dann schlicht leer).
  @visibleForTesting
  static Future<bool> isPlaintextSqlite(File file) async {
    final raf = await file.open();
    try {
      final header = await raf.read(16);
      if (header.length < 16) return true;
      // 16-Byte-Magic: "SQLite format 3" gefolgt von einem NUL-Byte.
      const magic = 'SQLite format 3';
      for (var i = 0; i < magic.length; i++) {
        if (header[i] != magic.codeUnitAt(i)) return false;
      }
      return header[15] == 0x00;
    } finally {
      await raf.close();
    }
  }

  /// Öffnet eine Datei mit Schlüssel. `PRAGMA key` MUSS das allererste
  /// Statement der Verbindung sein → onConfigure. Der sqlite_master-Zugriff
  /// danach lässt einen falschen/fehlenden Schlüssel sofort hier scheitern
  /// statt irgendwann später mitten im Betrieb.
  static Future<Database> _openWithKey(
    DatabaseFactory factory,
    String path,
    String hexKey,
  ) async {
    final db = await factory.openDatabase(
      path,
      options: OpenDatabaseOptions(
        onConfigure: (db) async {
          await db.rawQuery('PRAGMA key = "x\'$hexKey\'"');
        },
      ),
    );
    try {
      await db.rawQuery('SELECT count(*) FROM sqlite_master');
    } catch (e) {
      await db.close();
      rethrow;
    }
    return db;
  }

  @visibleForTesting
  static Future<Database> openEncrypted(
    DatabaseFactory factory,
    String path,
    String hexKey,
  ) =>
      _openWithKey(factory, path, hexKey);

  /// `PRAGMA cipher_version` liefert bei normalem sqlite3 KEINE Zeile – der
  /// vom Paket-README empfohlene Wächter dagegen, versehentlich unverschlüsselt
  /// weiterzulaufen, weil die falsche Bibliothek geladen wurde.
  static Future<bool> _cipherAvailable(DatabaseFactory factory) async {
    final db = await factory.openDatabase(inMemoryDatabasePath);
    try {
      final rows = await db.rawQuery('PRAGMA cipher_version');
      if (rows.isEmpty) return false;
      final value = rows.first.values.first;
      return value != null && value.toString().isNotEmpty;
    } finally {
      await db.close();
    }
  }

  /// Wie bisher in matrix_client.dart/background_push.dart: WAL gegen
  /// "database is locked" zwischen Haupt-App und Push-Isolate.
  static Future<void> _applyPragmas(Database db) async {
    try {
      await db.execute('PRAGMA journal_mode=WAL');
      await db.execute('PRAGMA busy_timeout = 5000');
    } catch (e) {
      if (kDebugMode) debugPrint('Could not set PRAGMAs: $e');
    }
  }

  static Future<String?> _readStoredKey() =>
      const FlutterSecureStorage().read(key: _cipherKeyStorageKey);

  /// Erzeugt beim allerersten Mal 32 kryptografisch zufällige Bytes und legt
  /// sie hex-kodiert im Keystore ab. Der Read-Back danach ist Pflicht: würde
  /// das Schreiben still scheitern und wir trotzdem damit verschlüsseln,
  /// wäre die DB beim nächsten Start unlesbar (Totalverlust).
  static Future<String> _getOrCreateKey() async {
    const storage = FlutterSecureStorage();
    final existing = await storage.read(key: _cipherKeyStorageKey);
    if (existing != null && existing.isNotEmpty) return existing;

    final rnd = Random.secure();
    final key = List.generate(
      32,
      (_) => rnd.nextInt(256).toRadixString(16).padLeft(2, '0'),
    ).join();
    await storage.write(key: _cipherKeyStorageKey, value: key);
    final readBack = await storage.read(key: _cipherKeyStorageKey);
    if (readBack != key) {
      throw StateError(
        'Schlüsselspeicher-Verifikation fehlgeschlagen: '
        '"$_cipherKeyStorageKey" konnte nicht zuverlässig abgelegt werden. '
        'Es wird NICHT verschlüsselt (sonst wäre die DB später unlesbar).',
      );
    }
    return key;
  }

  static Future<Map<String, int>> _tableRowCounts(Database db) async {
    final tables = await db.rawQuery(
      "SELECT name FROM sqlite_master "
      "WHERE type = 'table' AND name NOT LIKE 'sqlite_%'",
    );
    final result = <String, int>{};
    for (final row in tables) {
      final name = row['name'] as String;
      final count = await db.rawQuery('SELECT count(*) AS c FROM "$name"');
      result[name] = (count.first['c'] as num).toInt();
    }
    return result;
  }

  /// SQL-String-Literal: einfache Anführungszeichen verdoppeln (der Pfad
  /// kommt aus dem App-Verzeichnis, aber sicher ist sicher).
  static String _escapeSqlString(String value) => value.replaceAll("'", "''");
}
