import 'dart:io'; import 'package:flutter/foundation.dart'; import 'package:path_provider/path_provider.dart'; import 'package:path/path.dart' as p; import 'package:drift/drift.dart'; import 'package:drift/native.dart'; import 'package:drift/drift.dart' as drift; import 'package:search_engine/services/config_service.dart'; import 'package:search_engine/services/mimir_service.dart'; part 'database.g.dart'; class Draft { final String id; final String title; final DateTime date; final int activity; final String thumbnail; final int draft; final String locale; final String country; final String city; final String? body; final String? pdf; final String languagesCode; final Map? searchResultData; final int position; final int length; Draft({ required this.id, required this.title, required this.date, required this.activity, required this.thumbnail, required this.draft, required this.locale, required this.country, required this.city, this.body, this.pdf, required this.languagesCode, this.searchResultData, this.position = 0, this.length = 0, }); } class Messages extends Table { TextColumn get id => text()(); TextColumn get country => text()(); TextColumn get city => text()(); DateTimeColumn get date => dateTime()(); IntColumn get activity => integer()(); IntColumn get draft => integer()(); TextColumn get thumbnail => text()(); @override Set get primaryKey => {id}; } @TableIndex(name: 'title_index', columns: {#title}) @TableIndex(name: 'body_index', columns: {#body}) @TableIndex(name: 'message_id_index', columns: {#messageId}) class Translations extends Table { TextColumn get messageId => text()(); TextColumn get title => text()(); TextColumn get body => text()(); TextColumn get languagesCode => text()(); TextColumn get pdf => text().nullable()(); @override Set get primaryKey => {messageId, languagesCode}; } class Favorites extends Table { TextColumn get id => text()(); @override Set get primaryKey => {id}; } @DriftDatabase(tables: [Messages, Translations, Favorites]) class AppDatabase extends _$AppDatabase { AppDatabase() : super(_createConnection()); static DatabaseConnection _createConnection() { return DatabaseConnection.delayed(Future(() async { final dbFolder = await getApplicationDocumentsDirectory(); final filePath = p.join(dbFolder.path, 'LGCC_Search/internal/db.sqlite'); final file = File(filePath); final database = NativeDatabase(file, logStatements: false, cachePreparedStatements: true); return DatabaseConnection(database); })); } @override int get schemaVersion => 1; Future addMessages(List drafts) async { if (drafts.isEmpty) { return; // No hay nada que insertar } try { // Preparar las listas para mensajes y traducciones List messagesList = []; List translationsList = []; // Validar y preparar los datos for (final draft in drafts) { // Validar que el ID no esté vacío if (draft.id.isEmpty) { if (kDebugMode) { print('Saltando mensaje con ID vacío'); } continue; } // Preparar el mensaje messagesList.add(MessagesCompanion( id: Value(draft.id), country: Value(draft.country), city: Value(draft.city), date: Value(draft.date), activity: Value(draft.activity), thumbnail: Value(draft.thumbnail), draft: Value(draft.draft), )); // Preparar la traducción translationsList.add(TranslationsCompanion( messageId: Value(draft.id), body: Value(draft.body ?? ''), languagesCode: Value(draft.languagesCode), title: Value(draft.title), pdf: Value(draft.pdf ?? ''), )); } // Ejecutar las inserciones en una transacción para garantizar la atomicidad await transaction(() async { // Insertar mensajes con manejo de conflictos for (var message in messagesList) { await into(messages).insert( message, onConflict: DoUpdate( (old) => MessagesCompanion( country: message.country, city: message.city, date: message.date, activity: message.activity, thumbnail: message.thumbnail, draft: message.draft, ), target: [messages.id], ), ); } // Insertar traducciones con manejo de conflictos for (var translation in translationsList) { await into(translations).insert( translation, onConflict: DoUpdate( (old) => TranslationsCompanion( title: translation.title, body: translation.body, pdf: translation.pdf, ), target: [translations.messageId, translations.languagesCode], ), ); } }); } catch (e) { if (kDebugMode) { print('Error al insertar mensajes en la base de datos: $e'); } // Reintento con enfoque más seguro en caso de error try { // Insertar uno por uno para identificar registros problemáticos for (int i = 0; i < drafts.length; i++) { final draft = drafts[i]; try { // Insertar mensaje await into(messages).insert( MessagesCompanion( id: Value(draft.id), country: Value(draft.country), city: Value(draft.city), date: Value(draft.date), activity: Value(draft.activity), thumbnail: Value(draft.thumbnail), draft: Value(draft.draft), ), onConflict: DoUpdate( (old) => MessagesCompanion( country: Value(draft.country), city: Value(draft.city), date: Value(draft.date), activity: Value(draft.activity), thumbnail: Value(draft.thumbnail), draft: Value(draft.draft), ), target: [messages.id], ), ); // Insertar traducción await into(translations).insert( TranslationsCompanion( messageId: Value(draft.id), body: Value(draft.body ?? ''), languagesCode: Value(draft.languagesCode), title: Value(draft.title), pdf: Value(draft.pdf ?? ''), ), onConflict: DoUpdate( (old) => TranslationsCompanion( title: Value(draft.title), body: Value(draft.body ?? ''), pdf: Value(draft.pdf ?? ''), ), target: [translations.messageId, translations.languagesCode], ), ); } catch (innerError) { if (kDebugMode) { print( 'Error al insertar mensaje #$i (ID: ${draft.id}): $innerError'); } // Continuar con el siguiente registro } } } catch (fallbackError) { if (kDebugMode) { print('Error en el proceso de recuperación: $fallbackError'); } } } } Future>> getPdfList() async { final locale = await ConfigService.getLocale(); try { final queryResult = await customSelect( "SELECT pdf, id, t.title, date, m.country, activity FROM messages LEFT JOIN translations ON messages.id = translations.message_id WHERE pdf <> '' AND locale = '$locale'", readsFrom: {messages}).map((row) { return { 'pdf': row.read('pdf'), 'title': row.read('title') }; }).get(); return queryResult; } catch (e) { if (kDebugMode) { print('Error fetching PDF list: $e'); } return []; } } Future> getMessages(bool isDraft, [String? localeParam]) async { final locale = localeParam ?? await ConfigService.getLocale(); if (locale.isEmpty) { return []; } try { // Consulta SQL optimizada sin usar INDEXED BY final queryResult = await customSelect( """ SELECT DISTINCT m.id, m.date, m.activity, m.thumbnail, m.draft, m.country, m.city, t.languages_code as locale, t.title, t.pdf, t.languages_code FROM messages m JOIN translations t ON m.id = t.message_id WHERE m.draft = ? AND t.languages_code = ? ORDER BY m.date DESC LIMIT 20 """, variables: [ Variable(isDraft ? 1 : 0), Variable.withString(locale), ], readsFrom: {messages, translations}, ).map((row) { return Draft( id: row.read('id'), title: row.read('title'), date: row.read('date'), activity: row.read('activity'), thumbnail: row.read('thumbnail'), draft: row.read('draft'), locale: row.read('locale'), country: row.read('country'), city: row.read('city'), // Cargar el cuerpo solo cuando sea necesario para mejorar el rendimiento body: '', pdf: row.read('pdf') ?? '', languagesCode: row.read('languages_code'), ); }).get(); return queryResult; } catch (e) { if (kDebugMode) { print('Error fetching messages: $e'); } return []; } } // Nuevo método para cargar el cuerpo de un mensaje específico cuando sea necesario Future getMessageBody(String messageId, String languageCode) async { try { final result = await customSelect( """ SELECT body FROM translations WHERE message_id = ? AND languages_code = ? """, variables: [ Variable.withString(messageId), Variable.withString(languageCode), ], ).getSingleOrNull(); return result?.read('body') ?? ''; } catch (e) { if (kDebugMode) { print('Error fetching message body: $e'); } return ''; } } Future> getYearActivities(String year, String month) async { final locale = await ConfigService.getLocale(); try { final queryResult = await customSelect( "SELECT DISTINCT m.id, t.title, m.date, m.activity, m.thumbnail, m.draft, t.languages_code as locale, m.country, m.city, t.body, t.pdf, t.languages_code, strftime('%Y', datetime(m.date, 'unixepoch')) AS year, strftime('%m', datetime(m.date, 'unixepoch')) AS month " "FROM messages m JOIN translations t ON m.id = t.message_id " "WHERE strftime('%Y', datetime(m.date, 'unixepoch')) = ? AND strftime('%m', datetime(m.date, 'unixepoch')) = ? AND t.languages_code = ? " "ORDER BY m.date DESC, m.activity DESC", variables: [ Variable.withString(year), Variable.withString(month), Variable.withString(locale) ], readsFrom: {messages, translations}, ).map((row) { final id = row.read('id'); final title = row.read('title'); final date = row.read('date'); final activity = row.read('activity'); final thumbnail = row.read('thumbnail'); final draft = row.read('draft'); final locale = row.read('locale'); final country = row.read('country'); final city = row.read('city'); final body = row.read('body') ?? ''; final pdf = row.read('pdf') ?? ''; final languagesCode = row.read('languages_code'); return Draft( id: id, title: title, date: date, activity: activity, thumbnail: thumbnail, draft: draft, locale: locale, country: country, city: city, body: body, pdf: pdf, languagesCode: languagesCode, ); }).get(); return queryResult; } catch (e) { if (kDebugMode) { print('Error fetching activities: $e'); } return []; } } Future> getMonths(String year) async { try { final queryResult = await customSelect( "SELECT DISTINCT strftime('%m', datetime(date, 'unixepoch')) AS month FROM messages WHERE strftime('%Y', datetime(date, 'unixepoch')) = ? ORDER BY month ASC", variables: [Variable.withString(year)], readsFrom: {messages}, ).map((row) { return row.read('month'); }).get(); return queryResult; } catch (e) { if (kDebugMode) { print('Error fetching months: $e'); } return []; } } Future toggleFavorite(String id) async { final result = await customSelect( 'SELECT COUNT(*) AS count FROM favorites WHERE id = ?', variables: [Variable.withString(id)], ).getSingle(); final isFavorite = result.read('count') > 0; if (isFavorite) { delete(favorites).delete(FavoritesCompanion(id: Value(id))); } else { into(favorites).insert( FavoritesCompanion(id: Value(id)), ); } } Future checkFavorite(String id) async { final result = await customSelect( 'SELECT DISTINCT COUNT(*) AS count FROM favorites WHERE id = ?', variables: [Variable.withString(id)], ).getSingle(); final isFavorite = result.read('count') > 0; return isFavorite; } Future> getFavorites() async { final result = await customSelect('SELECT DISTINCT id FROM favorites') .map((row) => row.read('id')) .get(); return result; } // Método para obtener los años disponibles en la base de datos Future> getAvailableYears() async { try { final queryResult = await customSelect( "SELECT DISTINCT strftime('%Y', datetime(date, 'unixepoch')) AS year FROM messages ORDER BY year DESC", readsFrom: {messages}, ).map((row) { return row.read('year'); }).get(); return queryResult; } catch (e) { if (kDebugMode) { print('Error fetching available years: $e'); } return []; } } // Método para obtener mensajes filtrados por año y mes Future> getFilteredMessages(String year, String month, [String? localeParam]) async { final locale = localeParam ?? await ConfigService.getLocale(); if (locale.isEmpty) { return []; } try { final queryResult = await customSelect( """ SELECT DISTINCT m.id, m.date, m.activity, m.thumbnail, m.draft, m.country, m.city, t.languages_code as locale, t.title, t.pdf, t.languages_code FROM messages m JOIN translations t ON m.id = t.message_id WHERE strftime('%Y', datetime(m.date, 'unixepoch')) = ? AND strftime('%m', datetime(m.date, 'unixepoch')) = ? AND t.languages_code = ? ORDER BY m.date DESC """, variables: [ Variable.withString(year), Variable.withString(month), Variable.withString(locale), ], readsFrom: {messages, translations}, ).map((row) { return Draft( id: row.read('id'), title: row.read('title'), date: row.read('date'), activity: row.read('activity'), thumbnail: row.read('thumbnail'), draft: row.read('draft'), locale: row.read('locale'), country: row.read('country'), city: row.read('city'), body: '', pdf: row.read('pdf') ?? '', languagesCode: row.read('languages_code'), ); }).get(); return queryResult; } catch (e) { if (kDebugMode) { print('Error fetching filtered messages: $e'); } return []; } } // Método para obtener todos los mensajes (borradores y no borradores) Future> getAllMessages([String? localeParam, int? limit]) async { final locale = localeParam ?? await ConfigService.getLocale(); if (locale.isEmpty) { return []; } try { // Consulta SQL optimizada para obtener todos los mensajes var query = """ SELECT DISTINCT m.id, m.date, m.activity, m.thumbnail, m.draft, m.country, m.city, t.languages_code as locale, t.title, t.body, t.pdf, t.languages_code FROM messages m JOIN translations t ON m.id = t.message_id WHERE t.languages_code = ? ORDER BY m.date DESC """; // Añadir límite si se especifica if (limit != null) { query += " LIMIT $limit"; } final queryResult = await customSelect( query, variables: [ Variable.withString(locale), ], readsFrom: {messages, translations}, ).map((row) { return Draft( id: row.read('id'), title: row.read('title'), date: row.read('date'), activity: row.read('activity'), thumbnail: row.read('thumbnail'), draft: row.read('draft'), locale: row.read('locale'), country: row.read('country'), city: row.read('city'), body: row.read('body') ?? '', pdf: row.read('pdf') ?? '', languagesCode: row.read('languages_code'), ); }).get(); return queryResult; } catch (e) { if (kDebugMode) { print('Error fetching all messages: $e'); } return []; } } Future> searchMessages(String query, String languagesCode) async { try { final mimirService = MimirService(); await mimirService.initialize(); // Buscar en Mimir para obtener los IDs relevantes y metadatos final searchResult = await mimirService.search(query, languagesCode); // Extraer IDs de los resultados List> searchResultsData = searchResult['results']; List docIds = searchResultsData.map((doc) => doc['id'] as String).toList(); if (docIds.isEmpty) { return []; } // Convertir los IDs en una cadena para la consulta SQL final String idList = docIds.map((id) => "'$id'").join(','); // Buscar los documentos completos en la base de datos final queryResult = await customSelect( ''' SELECT DISTINCT m.id, m.date, m.activity, m.thumbnail, m.draft, m.country, m.city, t.languages_code as locale, t.title, t.pdf, t.languages_code, t.body FROM messages m JOIN translations t ON m.id = t.message_id WHERE m.id IN ($idList) AND t.languages_code = ? ORDER BY m.date DESC ''', variables: [Variable.withString(languagesCode)], readsFrom: {messages, translations}, ).map((row) { final String id = row.read('id'); // Buscar los datos del resultado de búsqueda correspondiente Map? resultData; for (var data in searchResultsData) { if (data['id'] == id) { resultData = data; break; } } return Draft( id: id, title: row.read('title'), date: row.read('date'), activity: row.read('activity'), thumbnail: row.read('thumbnail'), draft: row.read('draft'), locale: row.read('locale'), country: row.read('country'), city: row.read('city'), body: row.read('body'), pdf: row.read('pdf') ?? '', languagesCode: row.read('languages_code'), searchResultData: { ...resultData ?? {}, 'allResultIds': docIds, 'mimirSearchResult': searchResult, }, ); }).get(); return queryResult; } catch (e) { if (kDebugMode) { print('Error searching messages: $e'); } return []; } } // Método para obtener todos los PDFs disponibles Future> getAvailablePdfs(String locale) async { try { final query = """ SELECT DISTINCT m.id, m.date, m.activity, m.thumbnail, m.draft, m.country, m.city, t.languages_code as locale, t.title, t.pdf, t.languages_code FROM messages m JOIN translations t ON m.id = t.message_id WHERE t.languages_code = ? AND t.pdf IS NOT NULL AND t.pdf != '' ORDER BY m.date DESC """; final queryResult = await customSelect( query, variables: [ Variable.withString(locale), ], readsFrom: {messages, translations}, ).map((row) { return Draft( id: row.read('id'), title: row.read('title'), date: row.read('date'), activity: row.read('activity'), thumbnail: row.read('thumbnail'), draft: row.read('draft'), locale: row.read('locale'), country: row.read('country'), city: row.read('city'), body: '', pdf: row.read('pdf') ?? '', languagesCode: row.read('languages_code'), ); }).get(); return queryResult; } catch (e) { if (kDebugMode) { print('Error fetching available PDFs: $e'); } return []; } } // Método para obtener mensajes por IDs Future> getMessagesByIds(List ids, String locale) async { if (ids.isEmpty) return []; try { // Convertir la lista de IDs a un formato que pueda usarse en la consulta SQL final String idList = ids.map((id) => "'$id'").join(','); final queryResult = await customSelect( ''' SELECT DISTINCT m.id, m.date, m.activity, m.thumbnail, m.draft, m.country, m.city, t.languages_code as locale, t.title, t.body, t.pdf, t.languages_code FROM messages m JOIN translations t ON m.id = t.message_id WHERE m.id IN ($idList) AND t.languages_code = ? ORDER BY m.date DESC ''', variables: [Variable.withString(locale)], readsFrom: {messages, translations}, ).map((row) { return Draft( id: row.read('id'), title: row.read('title'), date: row.read('date'), activity: row.read('activity'), thumbnail: row.read('thumbnail'), draft: row.read('draft'), locale: row.read('locale'), country: row.read('country'), city: row.read('city'), body: row.read('body') ?? '', pdf: row.read('pdf') ?? '', languagesCode: row.read('languages_code'), ); }).get(); if (kDebugMode) { print( 'Encontrados ${queryResult.length} mensajes de ${ids.length} IDs'); } return queryResult; } catch (e) { if (kDebugMode) { print('Error obteniendo mensajes por IDs: $e'); } return []; } } }