cyberhybridhub/lib/services/questions_api_service.dart
2026-06-03 05:12:02 -05:00

223 lines
7.1 KiB
Dart

import 'dart:convert';
import 'package:flutter/foundation.dart';
import 'package:http/http.dart' as http;
import '../config/api_config.dart';
import '../models/guess_score_reset_result.dart';
import '../models/guess_score_summary.dart';
import '../models/incoming_question.dart';
import '../models/question_defer_result.dart';
import '../models/question_submit_result.dart';
import 'auth_service.dart';
/// HTTP client for question queue, answers, and deferrals.
class QuestionsApiService {
QuestionsApiService({http.Client? client}) : _client = client ?? http.Client();
final http.Client _client;
/// Ensures login question state is initialized for the signed-in user.
///
/// Returns the first unanswered question when present and always attempts to
/// return persisted [GuessScoreSummary] for this Firebase user when the API
/// includes it in the bootstrap payload.
Future<({IncomingQuestion? question, GuessScoreSummary? score})>
bootstrapOnLogin() async {
final String? token = await AuthService.instance.getIdToken();
if (token == null) {
return (question: null, score: null);
}
final http.Response response = await _client.post(
Uri.parse('$apiBaseUrl/v1/me/questions/bootstrap'),
headers: _authHeaders(token),
);
if (response.statusCode != 200) {
debugPrint(
'bootstrapOnLogin failed: ${response.statusCode} ${response.body}',
);
return (question: null, score: null);
}
final Map<String, dynamic> body =
jsonDecode(response.body) as Map<String, dynamic>;
final Map<String, dynamic>? questionJson =
body['question'] as Map<String, dynamic>?;
final Map<String, dynamic>? scoreJson =
body['score'] as Map<String, dynamic>?;
return (
question: questionJson == null
? null
: IncomingQuestion.fromJson(questionJson),
score: scoreJson == null
? null
: GuessScoreSummary.fromJson(scoreJson),
);
}
Future<List<IncomingQuestion>> fetchUnanswered() async {
final String? token = await AuthService.instance.getIdToken();
if (token == null) {
return <IncomingQuestion>[];
}
final http.Response response = await _client.get(
Uri.parse('$apiBaseUrl/v1/me/questions'),
headers: _authHeaders(token),
);
if (response.statusCode != 200) {
debugPrint(
'fetchUnanswered failed: ${response.statusCode} ${response.body}',
);
return <IncomingQuestion>[];
}
final Map<String, dynamic> body =
jsonDecode(response.body) as Map<String, dynamic>;
final List<dynamic> raw = body['questions'] as List<dynamic>? ?? <dynamic>[];
return raw
.map(
(dynamic item) =>
IncomingQuestion.fromJson(item as Map<String, dynamic>),
)
.toList();
}
Future<QuestionSubmitResult?> submitAnswer({
required String questionId,
num answer = 0,
}) async {
final String? token = await AuthService.instance.getIdToken();
if (token == null) {
return null;
}
final http.Response response = await _client.post(
Uri.parse('$apiBaseUrl/v1/me/questions/$questionId/answer'),
headers: _authHeaders(token),
body: jsonEncode(<String, num>{'answer': answer}),
);
if (response.statusCode != 200) {
debugPrint(
'submitAnswer failed: ${response.statusCode} ${response.body}',
);
return null;
}
final Map<String, dynamic> body =
jsonDecode(response.body) as Map<String, dynamic>;
final Map<String, dynamic>? scoreJson =
body['score'] as Map<String, dynamic>?;
final Map<String, dynamic>? nextQuestionJson =
body['nextQuestion'] as Map<String, dynamic>?;
return QuestionSubmitResult(
unansweredCount: (body['unansweredCount'] as num?)?.toInt() ?? 0,
score: scoreJson == null
? GuessScoreSummary.empty
: GuessScoreSummary.fromJson(scoreJson),
nextQuestion: nextQuestionJson == null
? null
: IncomingQuestion.fromJson(nextQuestionJson),
);
}
Future<QuestionDeferResult?> deferQuestion({required String questionId}) async {
final String? token = await AuthService.instance.getIdToken();
if (token == null) {
return null;
}
final http.Response response = await _client.post(
Uri.parse('$apiBaseUrl/v1/me/questions/$questionId/defer'),
headers: _authHeaders(token),
);
if (response.statusCode != 200) {
debugPrint(
'deferQuestion failed: ${response.statusCode} ${response.body}',
);
return null;
}
final Map<String, dynamic> body =
jsonDecode(response.body) as Map<String, dynamic>;
final Map<String, dynamic>? nextQuestionJson =
body['nextQuestion'] as Map<String, dynamic>?;
return QuestionDeferResult(
unansweredCount: (body['unansweredCount'] as num?)?.toInt() ?? 0,
nextQuestion: nextQuestionJson == null
? null
: IncomingQuestion.fromJson(nextQuestionJson),
);
}
Future<GuessScoreSummary?> fetchGuessScoreSummary() async {
final String? token = await AuthService.instance.getIdToken();
if (token == null) {
return null;
}
final http.Response response = await _client.get(
Uri.parse('$apiBaseUrl/v1/me/questions/score'),
headers: _authHeaders(token),
);
if (response.statusCode != 200) {
debugPrint(
'fetchGuessScoreSummary failed: ${response.statusCode} ${response.body}',
);
return null;
}
final Map<String, dynamic> body =
jsonDecode(response.body) as Map<String, dynamic>;
final Map<String, dynamic>? scoreJson =
body['score'] as Map<String, dynamic>?;
if (scoreJson == null) {
return GuessScoreSummary.empty;
}
return GuessScoreSummary.fromJson(scoreJson);
}
Future<GuessScoreResetResult?> resetGuessScoreSummary() async {
final String? token = await AuthService.instance.getIdToken();
if (token == null) {
return null;
}
final http.Response response = await _client.post(
Uri.parse('$apiBaseUrl/v1/me/questions/score/reset'),
headers: _authHeaders(token),
);
if (response.statusCode != 200) {
debugPrint(
'resetGuessScoreSummary failed: ${response.statusCode} ${response.body}',
);
return null;
}
final Map<String, dynamic> body =
jsonDecode(response.body) as Map<String, dynamic>;
final Map<String, dynamic>? scoreJson =
body['score'] as Map<String, dynamic>?;
final Map<String, dynamic>? questionJson =
body['question'] as Map<String, dynamic>?;
return GuessScoreResetResult(
score: scoreJson == null
? GuessScoreSummary.empty
: GuessScoreSummary.fromJson(scoreJson),
unansweredCount: (body['unansweredCount'] as num?)?.toInt() ?? 0,
question: questionJson == null
? null
: IncomingQuestion.fromJson(questionJson),
);
}
Map<String, String> _authHeaders(String token) {
return <String, String>{
'Authorization': 'Bearer $token',
'Content-Type': 'application/json',
'Accept': 'application/json',
};
}
}