import 'dart:async'; import 'dart:io'; import 'dart:math'; import 'question_source_preferences_db.dart'; import 'questions_db.dart'; import 'signalr/questions_hub_connections.dart'; import 'trading/prospective_guess_selection.dart'; import 'trading/prospective_guess_assignments_db.dart'; import 'trading/market_history_question_audit.dart'; /// Creates questions in Postgres and delivers them over SignalR. class QuestionService { QuestionService({ required QuestionsDb questionsDb, required QuestionsHubConnections hubConnections, QuestionSourcePreferencesDb? questionSourcePreferencesDb, }) : _questionsDb = questionsDb, _hubConnections = hubConnections, _questionSourcePreferencesDb = questionSourcePreferencesDb; final QuestionsDb _questionsDb; final QuestionsHubConnections _hubConnections; final QuestionSourcePreferencesDb? _questionSourcePreferencesDb; ProspectiveGuessAssignmentsDb get _assignmentsDb => ProspectiveGuessAssignmentsDb(_questionsDb.connection); /// Called at login: ensures the user row exists (queue top-up uses [QuestionDispatcher]). Future bootstrapOnLogin(String firebaseUid) async { await _questionsDb.ensureUserExists(firebaseUid); } /// Ensures the active slot's top-half batch exists, then returns the FIFO head. Future?> ensureProspectiveQuestionQueued( String firebaseUid, { DateTime? now, }) async { await _questionsDb.ensureUserExists(firebaseUid); await _ensureCurrentSlotBatch(firebaseUid, now: now); final int queued = await _questionsDb.countUnansweredQuestions(firebaseUid); if (queued > 0) { final Map? existing = await _questionsDb.findUnansweredQuestion(firebaseUid); if (existing == null) { return null; } return _questionsDb.toClientPayload( existing, unansweredCount: queued, ); } return null; } /// Enqueues market questions when enabled; returns whether the queue grew. Future tryEnqueueMarketQuestions( String firebaseUid, { DateTime? now, int? maxCreates, }) async { if (!await _isMarketHistoryEnabled(firebaseUid)) { return false; } final int before = await _questionsDb.countUnansweredQuestions(firebaseUid); await _ensureCurrentSlotBatch( firebaseUid, now: now, maxCreates: maxCreates, ); final int after = await _questionsDb.countUnansweredQuestions(firebaseUid); return after > before; } /// Creates every top-half question for the user's active slot pair. Future _ensureCurrentSlotBatch( String firebaseUid, { DateTime? now, int? maxCreates, }) async { if (!await _isMarketHistoryEnabled(firebaseUid)) { return; } final ProspectiveGuessSelection selection = ProspectiveGuessSelection( connection: _questionsDb.connection, ); for (var attempt = 0; attempt < 8; attempt++) { final ProspectiveSlotBatch? batch = await selection.resolveUnassignedBatch(firebaseUid, now: now); if (batch == null || batch.unassignedAssets.isEmpty) { return; } final DateTime baseOrder = DateTime.now().toUtc(); var createdAny = false; var createdCount = 0; for (var index = 0; index < batch.unassignedAssets.length; index++) { if (maxCreates != null && createdCount >= maxCreates) { return; } final QuestionAuditAsset asset = batch.unassignedAssets[index]; if (await _assignmentsDb.hasAssignmentForSymbolSlotPair( firebaseUid: firebaseUid, olderSlotStart: batch.olderSlotStart, newerSlotStart: batch.newerSlotStart, symbol: asset.symbol, )) { continue; } final Map picked = await selection.buildPickForAsset( asset: asset, olderSlotStart: batch.olderSlotStart, newerSlotStart: batch.newerSlotStart, ); final bool isInverted = Random().nextBool(); final String text = picked['questionText']! as String; final String questionText = isInverted ? '$text [Inverted]' : text; final Map question = await _questionsDb.createQuestion( assignedUserId: firebaseUid, questionText: questionText, correctAnswer: picked['correctAnswer']! as num, sourceTag: 'market_history:prospective', pipelineKey: 'trading', pipelineStep: 'guess_weekly_move:await_answer', metadata: { 'prospective_question_id': picked['id'], 'symbol': asset.symbol, 'older_slot_start': picked['olderSlotStart'], 'newer_slot_start': picked['newerSlotStart'], 'price_delta_pct': picked['priceDeltaPct'], 'is_inverted': isInverted, }, ); final bool assigned = await _assignmentsDb.insertPendingIfAbsent( firebaseUid: firebaseUid, olderSlotStart: batch.olderSlotStart, newerSlotStart: batch.newerSlotStart, symbol: asset.symbol, prospectiveQuestionId: picked['id']! as String, questionId: question['id']! as String, viewOrderAt: baseOrder.add(Duration(milliseconds: index)), ); if (!assigned) { await _questionsDb.deleteUnansweredQuestion( questionId: question['id']! as String, assignedUserId: firebaseUid, ); continue; } createdAny = true; createdCount++; } if (createdAny) { return; } } } Future _isMarketHistoryEnabled(String firebaseUid) async { final QuestionSourcePreferencesDb? preferencesDb = _questionSourcePreferencesDb; if (preferencesDb == null) { return true; } return preferencesDb.isSourceEnabled(firebaseUid, 'market_history'); } /// Inserts a question and pushes it to connected SignalR clients. Future> createAndDeliverQuestion({ required String assignedUserId, required String questionText, required num correctAnswer, String? sourceTag, String? pipelineKey, String? pipelineStep, Map? metadata, }) async { final Map question = await _questionsDb.createQuestion( assignedUserId: assignedUserId, questionText: questionText, correctAnswer: correctAnswer, sourceTag: sourceTag, pipelineKey: pipelineKey, pipelineStep: pipelineStep, metadata: metadata, ); final int unansweredCount = await _questionsDb.countUnansweredQuestions(assignedUserId); final Map payload = _questionsDb.toClientPayload( question, unansweredCount: unansweredCount, ); await _hubConnections.pushQuestion(assignedUserId, payload); return payload; } Future deliverPendingQuestionOnConnect( QuestionsHubConnection connection, ) async { try { final String uid = connection.firebaseUid; final Map? question = await _questionsDb.findUnansweredQuestion(uid); if (question == null) { return; } final int unansweredCount = await _questionsDb.countUnansweredQuestions(uid); final Map payload = _questionsDb.toClientPayload( question, unansweredCount: unansweredCount, ); await _hubConnections.pushQuestionToConnection(connection, payload); } catch (e, st) { stderr.writeln( 'Failed to deliver pending question for ${connection.firebaseUid}: $e\n$st', ); } } }