import 'dart:async'; import 'package:em2rp/models/event_model.dart'; import 'package:em2rp/services/api_service.dart'; import 'package:em2rp/utils/debug_log.dart'; /// Représente un tour de conversation dans le chat. class AiAssistantChatTurn { final bool isUser; final String text; const AiAssistantChatTurn({required this.isUser, required this.text}); } /// Un item proposé par l'IA dans la liste de matériel. class AiEquipmentProposalItem { final String equipmentId; final int quantity; final String rationale; const AiEquipmentProposalItem({ required this.equipmentId, required this.quantity, required this.rationale, }); } /// Proposition complète retournée par l'IA. class AiEquipmentProposal { final String summary; final List items; /// Équipements individuels prêts à être injectés dans l'état local de l'événement. final List asEventEquipment; /// IDs des containers (flight cases) proposés par l'IA. final List containerIds; const AiEquipmentProposal({ required this.summary, required this.items, required this.asEventEquipment, required this.containerIds, }); } /// Réponse complète de l'assistant IA (message + proposition optionnelle). class AiEquipmentAssistantResponse { final String assistantMessage; final AiEquipmentProposal? proposal; const AiEquipmentAssistantResponse({ required this.assistantMessage, this.proposal, }); } /// Service assistant IA logisticien. /// Délègue tous les appels Gemini à la Cloud Function [aiEquipmentProposal]. /// L'authentification Firebase (token Bearer) suffit — aucune clé API côté client. class AiEquipmentAssistantService { final ApiService _apiService; AiEquipmentAssistantService({ApiService? apiService}) : _apiService = apiService ?? FirebaseFunctionsApiService(); /// Envoie un message et retourne la réponse de l'assistant IA. Future generateProposal({ required DateTime startDate, required DateTime endDate, required List history, required String userMessage, String? eventTypeId, String? excludeEventId, List currentAssignedEquipment = const [], List workingProposalEquipment = const [], }) async { final payload = { 'startDate': startDate.toIso8601String(), 'endDate': endDate.toIso8601String(), 'userMessage': userMessage.trim(), 'history': history .where((turn) => turn.text.trim().isNotEmpty) .map((turn) => {'isUser': turn.isUser, 'text': turn.text.trim()}) .toList(), 'currentEquipment': currentAssignedEquipment .map((eq) => {'equipmentId': eq.equipmentId, 'quantity': eq.quantity}) .toList(), 'workingProposal': workingProposalEquipment .map((eq) => {'equipmentId': eq.equipmentId, 'quantity': eq.quantity}) .toList(), }; if (eventTypeId != null) payload['eventTypeId'] = eventTypeId; if (excludeEventId != null) payload['excludeEventId'] = excludeEventId; try { DebugLog.info('[AiEquipmentAssistantService] Calling aiEquipmentProposal Cloud Function'); final result = await _apiService.call('aiEquipmentProposal', payload); final assistantMessage = result['assistantMessage']?.toString().trim() ?? ''; final proposal = _parseProposal(result['proposal']); DebugLog.info( '[AiEquipmentAssistantService] Response received, items: ${proposal?.items.length ?? 0}', ); return AiEquipmentAssistantResponse( assistantMessage: assistantMessage.isNotEmpty ? assistantMessage : 'Je n\'ai pas pu générer de réponse.', proposal: proposal, ); } on ApiException catch (e) { DebugLog.error('[AiEquipmentAssistantService] API error', e); if (e.isUnauthorized) { throw Exception('Vous n\'êtes pas authentifié. Reconnectez-vous et réessayez.'); } throw Exception('Erreur du service IA (${e.statusCode}): ${e.message}'); } catch (e) { DebugLog.error('[AiEquipmentAssistantService] Error', e); rethrow; } } AiEquipmentProposal? _parseProposal(dynamic rawProposal) { if (rawProposal == null || rawProposal is! Map) return null; final proposalItems = []; final eventEquipmentList = []; final containerIds = []; final rawItems = rawProposal['items']; if (rawItems is List) { for (final rawItem in rawItems) { if (rawItem is! Map) continue; final item = Map.from(rawItem); final equipmentId = item['equipmentId']?.toString().trim() ?? ''; final quantity = int.tryParse(item['quantity']?.toString() ?? '1') ?? 1; if (equipmentId.isEmpty || quantity <= 0) continue; final rationale = item['rationale']?.toString().trim() ?? 'Proposition IA'; proposalItems.add(AiEquipmentProposalItem( equipmentId: equipmentId, quantity: quantity, rationale: rationale, )); eventEquipmentList.add(EventEquipment(equipmentId: equipmentId, quantity: quantity)); } } final rawContainers = rawProposal['containers']; if (rawContainers is List) { for (final rawContainer in rawContainers) { if (rawContainer is! Map) continue; final container = Map.from(rawContainer); final containerId = container['containerId']?.toString().trim() ?? ''; if (containerId.isNotEmpty) { containerIds.add(containerId); } } } if (proposalItems.isEmpty && containerIds.isEmpty) return null; return AiEquipmentProposal( summary: rawProposal['summary']?.toString().trim().isNotEmpty == true ? rawProposal['summary'].toString().trim() : 'Proposition matériel générée automatiquement.', items: proposalItems, asEventEquipment: eventEquipmentList, containerIds: containerIds, ); } }