import 'dart:math'; import 'package:shared_preferences/shared_preferences.dart'; import '../data/words_data.dart'; import '../models/history_entry_model.dart'; import '../models/word_model.dart'; import 'true_random_service.dart'; class WordsRepository { static final WordsRepository _instance = WordsRepository._internal(); factory WordsRepository() => _instance; WordsRepository._internal(); static const String _kHistoryKey = 'oracle_999_sessions_history_v2'; static const String _kMappingDateKey = 'oracle_daily_mapping_date_v1'; static const String _kMappingOrderKey = 'oracle_daily_mapping_order_v1'; static const String _kMappingSourceKey = 'oracle_daily_mapping_source_v1'; List _sessionWords = []; final List _sessions = []; bool _useRemoteShuffle = true; String? _dailyMappingDate; RandomSource _dailyMappingSource = RandomSource.local; List get allWords => List.unmodifiable(_sessionWords); List get sessions => List.unmodifiable(_sessions); List get history => sessions; // Alias for compatibility int get totalWords => _sessionWords.length; String? get dailyMappingDate => _dailyMappingDate; RandomSource get dailyMappingSource => _dailyMappingSource; Future init({bool remoteDailyShuffle = true}) async { _useRemoteShuffle = remoteDailyShuffle; await _restoreOrFetchDailyMapping(force: false); await _loadHistoryFromPrefs(); } Future reshuffleSessionNumbers() { return _restoreOrFetchDailyMapping(force: true); } Future _restoreOrFetchDailyMapping({required bool force}) async { final today = utcDateStamp(); if (!force) { final restored = await _loadPersistedMapping(today); if (restored) return _dailyMappingSource; } final lastId = kRussianTop1000Words.length - 1; RangeShuffle shuffle; if (_useRemoteShuffle) { shuffle = await TrueRandomService.instance.permutation( min: 0, max: lastId, ); } else { shuffle = RangeShuffle( TrueRandomService.instance.localPermutation(0, lastId), RandomSource.local, ); } _applyIndexOrder(shuffle.order); _dailyMappingDate = today; _dailyMappingSource = shuffle.source; await _persistMapping(today, shuffle.order, shuffle.source); return shuffle.source; } void _applyIndexOrder(List order) { _sessionWords = [ for (var i = 0; i < order.length; i++) kRussianTop1000Words[order[i]].copyWith(sessionNumber: i), ]; } Future _loadPersistedMapping(String today) async { try { final prefs = await SharedPreferences.getInstance(); final date = prefs.getString(_kMappingDateKey); final stored = prefs.getStringList(_kMappingOrderKey); if (date != today || stored == null) return false; final order = stored.map(int.parse).toList(); if (!isPermutation(order, 0, kRussianTop1000Words.length - 1)) { return false; } _applyIndexOrder(order); _dailyMappingDate = date; final sourceName = prefs.getString(_kMappingSourceKey); _dailyMappingSource = sourceName == RandomSource.randomOrg.name ? RandomSource.randomOrg : RandomSource.local; return true; } catch (_) { return false; } } Future _persistMapping( String date, List order, RandomSource source, ) async { try { final prefs = await SharedPreferences.getInstance(); await prefs.setString(_kMappingDateKey, date); await prefs.setStringList( _kMappingOrderKey, order.map((e) => e.toString()).toList(), ); await prefs.setString(_kMappingSourceKey, source.name); } catch (_) {} } static String utcDateStamp([DateTime? now]) { final d = (now ?? DateTime.now()).toUtc(); final y = d.year.toString().padLeft(4, '0'); final m = d.month.toString().padLeft(2, '0'); final day = d.day.toString().padLeft(2, '0'); return '$y-$m-$day'; } WordItem? getWordBySessionNumber(int number) { if (number < 0 || number >= _sessionWords.length) return null; return _sessionWords[number]; } WordItem getRandomWord() { final random = Random.secure(); final idx = random.nextInt(_sessionWords.length); return _sessionWords[idx]; } WordItem getNextWord(int currentSessionNumber) { int nextNumber = (currentSessionNumber + 1) % _sessionWords.length; return _sessionWords[nextNumber]; } WordItem getPreviousWord(int currentSessionNumber) { int prevNumber = (currentSessionNumber - 1 + _sessionWords.length) % _sessionWords.length; return _sessionWords[prevNumber]; } List search(String query) { query = query.trim().toLowerCase(); if (query.isEmpty) return []; final number = int.tryParse(query); if (number != null && number >= 0 && number < _sessionWords.length) { return [_sessionWords[number]]; } final exact = []; final startsWith = []; final contains = []; for (final w in _sessionWords) { final lower = w.word.toLowerCase(); if (lower == query) { exact.add(w); } else if (lower.startsWith(query)) { startsWith.add(w); } else if (lower.contains(query) || w.description.toLowerCase().contains(query) || w.category.toLowerCase().contains(query) || w.partOfSpeech.toLowerCase().contains(query)) { contains.add(w); } } return [...exact, ...startsWith, ...contains]; } List getCategories() { final set = {'Все'}; for (final w in _sessionWords) { if (w.category.isNotEmpty) { set.add(w.category); } } return set.toList(); } List getPartsOfSpeech() { final set = {'Все'}; for (final w in _sessionWords) { if (w.partOfSpeech.isNotEmpty) { set.add(w.partOfSpeech); } } return set.toList(); } // --- Session History Management --- Future saveSession(OracleSession session) async { final existingIdx = _sessions.indexWhere((s) => s.id == session.id); if (existingIdx != -1) { _sessions[existingIdx] = session; } else { _sessions.insert(0, session); } if (_sessions.length > 500) { _sessions.removeRange(500, _sessions.length); } await _saveHistoryToPrefs(); } // Backward compatibility alias Future addHistoryEntry(OracleSession entry) => saveSession(entry); Future updateSession(String id, {String? aiInterpretation, String? question}) async { final idx = _sessions.indexWhere((e) => e.id == id); if (idx != -1) { final old = _sessions[idx]; _sessions[idx] = old.copyWith( aiInterpretation: aiInterpretation ?? old.aiInterpretation, question: question ?? old.question, ); await _saveHistoryToPrefs(); } } Future updateHistoryEntry(String id, {String? aiInterpretation, String? question}) => updateSession(id, aiInterpretation: aiInterpretation, question: question); Future deleteSession(String id) async { _sessions.removeWhere((e) => e.id == id); await _saveHistoryToPrefs(); } Future deleteHistoryEntry(String id) => deleteSession(id); Future clearHistory() async { _sessions.clear(); await _saveHistoryToPrefs(); } Future _loadHistoryFromPrefs() async { try { final prefs = await SharedPreferences.getInstance(); final jsonList = prefs.getStringList(_kHistoryKey); _sessions.clear(); if (jsonList != null) { for (final item in jsonList) { try { _sessions.add(OracleSession.fromJson(item)); } catch (_) {} } } } catch (_) {} } Future _saveHistoryToPrefs() async { try { final prefs = await SharedPreferences.getInstance(); final jsonList = _sessions.map((e) => e.toJson()).toList(); await prefs.setStringList(_kHistoryKey, jsonList); } catch (_) {} } }