import 'package:drift/drift.dart'; import 'package:notas/data/app_database.dart'; import 'package:notas/data/api_client.dart'; import 'package:notas/data/sync_models.dart'; import 'package:notas/models/note.dart'; import 'package:notas/models/category.dart'; import 'package:notas/data/note_encryption.dart'; class NoteRepository { NoteRepository({ required AppDatabase database, required AuthApi authApi, required String masterKey, }) : _database = database, _authApi = authApi, _masterKey = masterKey; final AppDatabase _database; final AuthApi _authApi; final String _masterKey; Future> loadNotes() async { return _loadNotesFromDatabase(); } Future createNote(Note note) async { final int id = await _database.insertNoteAtTop( NotesCompanion.insert( uuid: note.uuid, title: note.title, body: note.body, createdAt: note.createdAt, updatedAt: note.updatedAt, sortIndex: 0, serverVersion: const Value(0), isDeleted: const Value(false), categoryId: const Value(null), ), ); return note.copyWith(id: id, index: 0); } Future updateNote(Note note) async { final int noteId = note.id ?? (throw ArgumentError('Note id is required to update a note.')); await _database.updateNoteRow( DbNote( id: noteId, uuid: note.uuid, title: note.title, body: note.body, createdAt: note.createdAt, updatedAt: note.updatedAt, sortIndex: note.index, serverVersion: note.serverVersion, isDeleted: note.isDeleted, categoryId: note.categoryId, ), ); return note; } Future deleteNote(Note note) async { final int noteId = note.id ?? (throw ArgumentError('Note id is required to delete a note.')); await _database.deleteNoteAndShift(id: noteId, removedIndex: note.index); } Future moveNote(Note note, int newIndex) async { final int noteId = note.id ?? (throw ArgumentError('Note id is required to reorder a note.')); await _database.moveNote( id: noteId, oldIndex: note.index, newIndex: newIndex, ); } // ========== Sync logic ========== /// Sincroniza notas con el servidor. /// Requiere que el usuario esté autenticado (token válido). Future> performSync({bool forceFull = false}) async { try { // Get last sync timestamp final DateTime? lastSync = await _authApi.getLastSyncAt(); final DateTime? lastSyncForRequest = forceFull ? DateTime.utc(1970, 1, 1) : lastSync; // Collect pending local changes. // If we already synced at least once, use updatedAt to avoid re-sending // old notes that were already uploaded. final List unsyncedNotes; final List unsyncedCategories; if (forceFull || lastSync == null) { unsyncedNotes = await _database.getUnsyncedNotes(); unsyncedCategories = await _database.getUnsyncedCategories(); } else { unsyncedNotes = await _database.getNotesChangedSince(lastSync); unsyncedCategories = await _database.getCategoriesChangedSince( lastSync, ); } // Build sync request (note: we send encrypted data, but locally we have plaintext) // Encrypt all notes before sending final List encryptedNotesPayload = []; for (final dbNote in unsyncedNotes) { final note = _fromDbNote(dbNote); final encryptedTitle = await NoteEncryption.encryptNote( note.title, _masterKey, ); final encryptedBody = await NoteEncryption.encryptNote( note.body, _masterKey, ); encryptedNotesPayload.add( SyncNotePayload.fromNote( note, encryptedTitle: encryptedTitle, encryptedBody: encryptedBody, ), ); } final List categoriesPayload = unsyncedCategories .map((cat) => SyncCategoryPayload.fromCategory(_fromDbCategory(cat))) .toList(); final SyncRequest syncRequest = SyncRequest( lastSyncAt: lastSyncForRequest, changes: SyncChanges( categories: categoriesPayload, notes: encryptedNotesPayload, ), ); // Call sync API final Map syncResult = await _authApi.sync(syncRequest); if (syncResult['error'] == true) { return {'error': true, 'message': syncResult['body']}; } final SyncResponse response = syncResult['data'] as SyncResponse; // Apply server changes to local database await _applySyncResponse(response); // Update lastSyncAt await _authApi.setLastSyncAt(response.serverTimestamp); return { 'error': false, 'synced': response.synced, 'notesCount': response.changes.notes.length, 'categoriesCount': response.changes.categories.length, }; } catch (e) { return {'error': true, 'message': e.toString()}; } } Future _applySyncResponse(SyncResponse response) async { // Apply categories from server for (final SyncCategoryResponse catResponse in response.changes.categories) { await _database.upsertCategory( CategoriesCompanion( uuid: Value(catResponse.id), encryptedName: Value(catResponse.encryptedName), serverVersion: Value(catResponse.serverVersion), isDeleted: Value(catResponse.isDeleted), updatedAt: Value(catResponse.updatedAt), ), ); } // Apply notes from server for (final SyncNoteResponse noteResponse in response.changes.notes) { final existingNote = await (_database.select( _database.notes, )..where((n) => n.uuid.equals(noteResponse.id))).getSingleOrNull(); // Decrypt note content String decryptedTitle = 'Encrypted'; String decryptedBody = 'Encrypted'; try { decryptedTitle = await NoteEncryption.decryptNote( noteResponse.encryptedTitle, _masterKey, ); decryptedBody = await NoteEncryption.decryptNote( noteResponse.encryptedBody, _masterKey, ); } catch (e) { // If decryption fails, keep default encrypted placeholders print('Failed to decrypt note ${noteResponse.id}: $e'); } if (existingNote != null) { // Update existing note await _database.updateNoteRow( DbNote( id: existingNote.id, uuid: noteResponse.id, title: decryptedTitle, body: decryptedBody, createdAt: existingNote.createdAt, updatedAt: noteResponse.updatedAt, sortIndex: noteResponse.position, serverVersion: noteResponse.serverVersion, isDeleted: noteResponse.isDeleted, categoryId: noteResponse.categoryId, ), ); } else { // Insert new note await _database .into(_database.notes) .insert( NotesCompanion( uuid: Value(noteResponse.id), title: Value(decryptedTitle), body: Value(decryptedBody), createdAt: Value(noteResponse.updatedAt), updatedAt: Value(noteResponse.updatedAt), sortIndex: Value(noteResponse.position), serverVersion: Value(noteResponse.serverVersion), isDeleted: Value(noteResponse.isDeleted), categoryId: Value(noteResponse.categoryId), ), ); } } } Future> _loadNotesFromDatabase() async { final List rows = await _database.getAllNotes(); return rows.map(_fromDbNote).toList(); } Note _fromDbNote(DbNote row) { return Note( id: row.id, uuid: row.uuid, title: row.title, body: row.body, createdAt: row.createdAt, updatedAt: row.updatedAt, index: row.sortIndex, serverVersion: row.serverVersion, isDeleted: row.isDeleted, categoryId: row.categoryId, ); } Category _fromDbCategory(DbCategory row) { return Category( uuid: row.uuid, encryptedName: row.encryptedName, serverVersion: row.serverVersion, isDeleted: row.isDeleted, updatedAt: row.updatedAt, ); } }