import 'dart:async'; import 'dart:io'; 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, }) : _questionsDb = questionsDb, _hubConnections = hubConnections; final QuestionsDb _questionsDb; final QuestionsHubConnections _hubConnections; ProspectiveGuessAssignmentsDb get _assignmentsDb => ProspectiveGuessAssignmentsDb(_questionsDb.connection); /// Called at login: ensures one unanswered prospective question when possible. Future?> bootstrapOnLogin( String firebaseUid, ) async { await _questionsDb.ensureUserExists(firebaseUid); return ensureProspectiveQuestionQueued(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; } /// Creates every top-half question for the user's active slot pair. Future _ensureCurrentSlotBatch( String firebaseUid, { DateTime? now, }) async { 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; for (var index = 0; index < batch.unassignedAssets.length; index++) { 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 Map question = await _questionsDb.createQuestion( assignedUserId: firebaseUid, questionText: picked['questionText']! as String, 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'], }, ); 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; } if (createdAny) { return; } } } /// 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; } /// On SignalR connect: deliver an existing unanswered question only (no create). 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', ); } } }