import 'dart:async'; import 'dart:convert'; import 'dart:io'; import 'package:shelf/shelf.dart'; import 'package:shelf_router/shelf_router.dart'; import '../cors_headers.dart'; import '../firebase_auth.dart'; import '../pipeline/question_pipeline.dart'; import '../question_dispatcher.dart'; import '../question_service.dart'; import '../questions_db.dart'; const String questionsBasePath = '/v1/me/questions'; Handler questionsHandler({ required FirebaseAuthVerifier auth, required QuestionsDb questionsDb, required QuestionService questionService, required QuestionDispatcher questionDispatcher, QuestionPipeline? questionPipeline, }) { final Router router = Router(); router.post('$questionsBasePath/bootstrap', (Request request) async { final String? firebaseUid = await _verify(auth, request); if (firebaseUid == null) { return _jsonResponse(401, {'error': 'Unauthorized'}); } try { await questionService.bootstrapOnLogin(firebaseUid); final Map? question = await questionDispatcher.dispatchNextQuestion(firebaseUid); final int unansweredCount = await questionDispatcher.countEnabledUnansweredQuestions(firebaseUid); final Map score = await questionsDb.getGuessScoreSummary(firebaseUid); return _jsonResponse(200, { 'question': question, 'unansweredCount': unansweredCount, 'score': score, }); } catch (e, st) { stderr.writeln('Bootstrap questions error: $e\n$st'); return _jsonResponse(500, {'error': 'Internal error'}); } }); router.get(questionsBasePath, (Request request) async { final String? firebaseUid = await _verify(auth, request); if (firebaseUid == null) { return _jsonResponse(401, {'error': 'Unauthorized'}); } try { final List> questions = await questionDispatcher.listPanelQuestions(firebaseUid); return _jsonResponse(200, { 'questions': questions, 'unansweredCount': questions.length, }); } catch (e, st) { stderr.writeln('List questions error: $e\n$st'); return _jsonResponse(500, {'error': 'Internal error'}); } }); router.get('$questionsBasePath/score', (Request request) async { final String? firebaseUid = await _verify(auth, request); if (firebaseUid == null) { return _jsonResponse(401, {'error': 'Unauthorized'}); } try { final Map score = await questionsDb.getGuessScoreSummary(firebaseUid); return _jsonResponse(200, { 'score': score, }); } catch (e, st) { stderr.writeln('Get questions score error: $e\n$st'); return _jsonResponse(500, {'error': 'Internal error'}); } }); router.post('$questionsBasePath/score/reset', (Request request) async { final String? firebaseUid = await _verify(auth, request); if (firebaseUid == null) { return _jsonResponse(401, {'error': 'Unauthorized'}); } try { final Map score = await questionsDb.resetGuessScoreSummary(firebaseUid); final Map? question = await questionDispatcher.dispatchNextQuestion(firebaseUid); final int unansweredCount = await questionDispatcher.countEnabledUnansweredQuestions(firebaseUid); return _jsonResponse(200, { 'score': score, 'unansweredCount': unansweredCount, if (question != null) 'question': question, }); } catch (e, st) { stderr.writeln('Reset questions score error: $e\n$st'); return _jsonResponse(500, {'error': 'Internal error'}); } }); router.post( '$questionsBasePath//answer', (Request request, String id) async { final String? firebaseUid = await _verify(auth, request); if (firebaseUid == null) { return _jsonResponse(401, {'error': 'Unauthorized'}); } try { final String body = await request.readAsString(); final Map json = body.isEmpty ? {} : jsonDecode(body) as Map; final String? answerText = json['answerText'] as String?; final num? answer = json['answer'] as num?; final Map? updated = await questionsDb.submitAnswer( questionId: id, assignedUserId: firebaseUid, userResponse: answer, userResponseText: answerText, ); if (updated == null) { return _jsonResponse(404, {'error': 'Not found'}); } if (questionPipeline != null) { await questionPipeline.onAnswerSubmitted( firebaseUid: firebaseUid, answeredQuestion: updated, userResponse: updated['userResponse'] as num? ?? answer ?? 0, ); } var unansweredCount = await questionDispatcher.countEnabledUnansweredQuestions(firebaseUid); final Map? nextQuestion = await questionDispatcher.nextQuestionAfterAnswer( firebaseUid: firebaseUid, answeredQuestion: updated, ); if (nextQuestion != null) { unansweredCount = (nextQuestion['unansweredCount'] as num?)?.toInt() ?? unansweredCount; } unawaited(questionDispatcher.topUpQueue(firebaseUid)); final bool isMarketAnswer = QuestionsDb.resolveSourceId(updated) == 'market_history'; final Map? score = isMarketAnswer ? await questionsDb.getGuessScoreSummary(firebaseUid) : null; num? correctValue; if (isMarketAnswer) { final Map metadata = Map.from( updated['metadata'] as Map? ?? {}, ); final bool isInverted = metadata['is_inverted'] as bool? ?? false; final num rawCorrect = updated['correctAnswer'] as num? ?? 0; correctValue = isInverted ? -rawCorrect : rawCorrect; } return _jsonResponse(200, { 'question': updated, 'unansweredCount': unansweredCount, if (score != null) 'score': score, if (nextQuestion != null) 'nextQuestion': nextQuestion, if (correctValue != null) 'correctValue': correctValue, }); } catch (e, st) { stderr.writeln('Answer question error: $e\n$st'); return _jsonResponse(500, {'error': 'Internal error'}); } }, ); router.post('$questionsBasePath/spelling/reset', (Request request) async { final String? firebaseUid = await _verify(auth, request); if (firebaseUid == null) { return _jsonResponse(401, {'error': 'Unauthorized'}); } if (questionPipeline == null) { return _jsonResponse(503, {'error': 'Unavailable'}); } try { final Map result = await questionPipeline.resetSpellingProgressForUser(firebaseUid); return _jsonResponse(200, result); } catch (e, st) { stderr.writeln('Reset spelling progress error: $e\n$st'); return _jsonResponse(500, {'error': 'Internal error'}); } }); router.post( '$questionsBasePath//spelling/prepare', (Request request, String id) async { final String? firebaseUid = await _verify(auth, request); if (firebaseUid == null) { return _jsonResponse(401, {'error': 'Unauthorized'}); } if (questionPipeline == null) { return _jsonResponse(503, {'error': 'Unavailable'}); } try { final Map? question = await questionPipeline.prepareSpellingQuestion( firebaseUid: firebaseUid, questionId: id, ); if (question == null) { return _jsonResponse(404, {'error': 'Not found'}); } return _jsonResponse(200, {'question': question}); } catch (e, st) { stderr.writeln('Prepare spelling question error: $e\n$st'); return _jsonResponse(500, {'error': 'Internal error'}); } }, ); router.post( '$questionsBasePath//defer', (Request request, String id) async { final String? firebaseUid = await _verify(auth, request); if (firebaseUid == null) { return _jsonResponse(401, {'error': 'Unauthorized'}); } try { final Map? updated = await questionsDb.deferQuestion( questionId: id, assignedUserId: firebaseUid, ); if (updated == null) { return _jsonResponse(404, {'error': 'Not found'}); } final int unansweredCount = await questionDispatcher.countEnabledUnansweredQuestions(firebaseUid); final Map? nextQuestion = await questionDispatcher.nextQuestionAfterDefer( firebaseUid: firebaseUid, deferredQuestionId: id, ); unawaited(questionDispatcher.topUpQueue(firebaseUid)); return _jsonResponse(200, { 'question': updated, 'unansweredCount': unansweredCount, if (nextQuestion != null) 'nextQuestion': nextQuestion, }); } catch (e, st) { stderr.writeln('Defer question error: $e\n$st'); return _jsonResponse(500, {'error': 'Internal error'}); } }, ); return (Request request) async { if (request.method == 'OPTIONS' && request.requestedUri.path.startsWith(questionsBasePath)) { return Response.ok('', headers: apiCorsHeaders()); } final String? firebaseUid = await _verify(auth, request); if (firebaseUid == null) { return _jsonResponse(401, {'error': 'Unauthorized'}); } final Response response = await router.call(request); if (response.statusCode != 404) { return response; } return _jsonResponse(404, {'error': 'Not found'}); }; } Future _verify(FirebaseAuthVerifier auth, Request request) { return auth.verifyBearerToken( request.headers['Authorization'] ?? request.headers['authorization'], ); } Response _jsonResponse(int status, Map body) { return Response( status, body: jsonEncode(body), headers: { ...apiCorsHeaders(), 'Content-Type': 'application/json', }, ); }