import 'dart:async'; import 'dart:io'; import 'questions_db.dart'; import 'signalr/questions_hub_connections.dart'; import 'trading/prospective_guess_assignments_db.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); } /// Creates the next slot-based prospective question when the queue is empty. Future?> ensureProspectiveQuestionQueued( String firebaseUid, { DateTime? now, }) async { await _questionsDb.ensureUserExists(firebaseUid); final String? pendingQuestionId = await _assignmentsDb.findPendingQuestionId(firebaseUid); if (pendingQuestionId != null) { final Map? question = await _questionsDb.findQuestionById( questionId: pendingQuestionId, assignedUserId: firebaseUid, ); if (question != null && question['userResponse'] == null) { final int unansweredCount = await _questionsDb.countUnansweredQuestions(firebaseUid); return _questionsDb.toClientPayload( question, unansweredCount: unansweredCount > 0 ? unansweredCount : 1, ); } } 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, ); } final Map? prospective = await _createProspectiveQuestionWithAssignment( firebaseUid, now: now, ); if (prospective == null) { return null; } return prospective; } /// Picks, creates, and records one prospective question when none is queued. /// /// Retries when a concurrent request claims the same user/slot/symbol first. Future?> _createProspectiveQuestionWithAssignment( String firebaseUid, { DateTime? now, }) async { for (var attempt = 0; attempt < 8; attempt++) { final Map? picked = await _questionsDb.pickProspectiveQuestionForUser( firebaseUid, now: now, ); if (picked == null) { return null; } final DateTime olderSlotStart = DateTime.parse( picked['olderSlotStart']! as String, ); final DateTime newerSlotStart = DateTime.parse( picked['newerSlotStart']! as String, ); final String symbol = picked['symbol']! as String; if (await _assignmentsDb.hasAssignmentForSymbolSlotPair( firebaseUid: firebaseUid, olderSlotStart: olderSlotStart, newerSlotStart: newerSlotStart, symbol: symbol, )) { continue; } 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': 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: olderSlotStart, newerSlotStart: newerSlotStart, symbol: symbol, prospectiveQuestionId: picked['id']! as String, questionId: question['id']! as String, ); if (!assigned) { await _questionsDb.deleteUnansweredQuestion( questionId: question['id']! as String, assignedUserId: firebaseUid, ); continue; } return _questionsDb.toClientPayload( question, unansweredCount: 1, ); } return null; } /// 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', ); } } }