import 'package:flutter/material.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; import 'package:nhance_app_pwa/features/chatbot/data/chatbot_api_service.dart'; import 'package:nhance_app_pwa/features/chatbot/data/chatbot_repository.dart'; import 'package:nhance_app_pwa/features/chatbot/domain/models/chatbot_models.dart'; import 'package:nhance_app_pwa/pages/helpers/ecard_download_notification_service.dart'; import 'package:nhance_app_pwa/pages/helpers/ecard_download_service.dart'; import 'package:nhance_app_pwa/pages/service/TokenService.dart'; import 'package:url_launcher/url_launcher.dart'; final chatbotRepositoryProvider = Provider((ref) { return ChatbotRepository(ChatbotApiService()); }); final chatbotControllerProvider = StateNotifierProvider((ref) { return ChatbotController(ref.read(chatbotRepositoryProvider)); }); class ChatbotUiState { final bool isOpen; final bool isIntroAnimating; final bool isLoading; final String? errorMessage; final ChatbotFlowStep step; final List messages; final List options; final List policies; final ChatbotPolicy? selectedPolicy; final List claimStatuses; const ChatbotUiState({ required this.isOpen, required this.isIntroAnimating, required this.isLoading, required this.errorMessage, required this.step, required this.messages, required this.options, required this.policies, required this.selectedPolicy, required this.claimStatuses, }); factory ChatbotUiState.initial() { return ChatbotUiState( isOpen: false, isIntroAnimating: false, isLoading: false, errorMessage: null, step: ChatbotFlowStep.rootMenu, messages: [ ChatbotMessage( text: 'Welcome! I’m here to assist you with your policy services. Please choose an option to continue.', isBot: true, createdAt: DateTime.now(), ), ], options: const [ ChatbotOption(id: 'my_policies', label: 'My Policies'), ChatbotOption(id: 'claims_procedure', label: 'Claims Procedure'), ChatbotOption(id: 'my_claims_status', label: 'My Claims Status'), ChatbotOption(id: 'help', label: 'Help'), ], policies: const [], selectedPolicy: null, claimStatuses: const [], ); } ChatbotUiState copyWith({ bool? isOpen, bool? isIntroAnimating, bool? isLoading, String? errorMessage, bool clearError = false, ChatbotFlowStep? step, List? messages, List? options, List? policies, ChatbotPolicy? selectedPolicy, bool clearSelectedPolicy = false, List? claimStatuses, }) { return ChatbotUiState( isOpen: isOpen ?? this.isOpen, isIntroAnimating: isIntroAnimating ?? this.isIntroAnimating, isLoading: isLoading ?? this.isLoading, errorMessage: clearError ? null : (errorMessage ?? this.errorMessage), step: step ?? this.step, messages: messages ?? this.messages, options: options ?? this.options, policies: policies ?? this.policies, selectedPolicy: clearSelectedPolicy ? null : (selectedPolicy ?? this.selectedPolicy), claimStatuses: claimStatuses ?? this.claimStatuses, ); } } class ChatbotController extends StateNotifier { final ChatbotRepository _repository; final EcardDownloadService _ecardDownloadService = const EcardDownloadService(); List? _policiesCache; DateTime? _policiesCacheAt; static const Duration _cacheTtl = Duration(minutes: 10); bool _didHomeIntroAnimation = false; ChatbotController(this._repository) : super(ChatbotUiState.initial()); void toggleOpen() { state = state.copyWith(isOpen: !state.isOpen); } void close() { state = state.copyWith(isOpen: false); } void resetAll() { _policiesCache = null; _policiesCacheAt = null; _didHomeIntroAnimation = false; state = ChatbotUiState.initial(); } Future triggerHomeIntroAnimation() async { if (_didHomeIntroAnimation) return; _didHomeIntroAnimation = true; state = state.copyWith(isIntroAnimating: true); await Future.delayed(const Duration(milliseconds: 900)); state = state.copyWith(isIntroAnimating: false); } List _latestThreeClaims(List claims) { if (claims.length <= 3) return List.from(claims.reversed); return claims.reversed.take(3).toList(); } void _appendUserMessage(String text) { final updated = List.from(state.messages) ..add( ChatbotMessage( text: text, isBot: false, createdAt: DateTime.now(), ), ); state = state.copyWith(messages: updated); } void _appendBotMessage(String text) { final updated = List.from(state.messages) ..add( ChatbotMessage( text: text, isBot: true, createdAt: DateTime.now(), ), ); state = state.copyWith(messages: updated); } void _showRootMenu() { state = state.copyWith( step: ChatbotFlowStep.rootMenu, options: const [ ChatbotOption(id: 'my_policies', label: 'My Policies'), ChatbotOption(id: 'claims_procedure', label: 'Claims Procedure'), ChatbotOption(id: 'my_claims_status', label: 'My Claims Status'), ChatbotOption(id: 'help', label: 'Help'), ], clearSelectedPolicy: true, clearError: true, ); } Future onOptionSelected( BuildContext context, ChatbotOption option, ) async { _appendUserMessage(option.label); switch (option.id) { case 'my_policies': await loadPolicies(context); return; case 'claims_procedure': _appendBotMessage('Opening claims page.'); close(); context.go('/claimprocess', extra: 2); return; case 'my_claims_status': await loadClaimsStatus(context); return; case 'help': _appendBotMessage('Opening help page.'); close(); context.go('/help'); return; case 'covered_member_list': showCoveredMembers(); return; case 'ecard_download': await openEcard(context); return; case 'covered_hospital_list': await openHospitalList(); return; case 'back_to_root': _appendBotMessage('Back to main menu.'); _showRootMenu(); return; case 'back_to_policies': _appendBotMessage('Choose a policy.'); state = state.copyWith( step: ChatbotFlowStep.policyList, options: _policyOptions(state.policies), clearSelectedPolicy: true, clearError: true, ); return; default: if (option.id.startsWith('policy::')) { final policyId = option.id.replaceFirst('policy::', ''); final selected = state.policies.firstWhere( (policy) => policy.clientPolicyId == policyId, orElse: () => state.policies.first, ); selectPolicy(selected); } return; } } List _policyOptions(List policies) { final policyOptions = policies .map((policy) => ChatbotOption( id: 'policy::${policy.clientPolicyId}', label: policy.policyName, )) .toList(); return [ ...policyOptions, const ChatbotOption(id: 'back_to_root', label: 'Back'), ]; } Future loadPolicies(BuildContext context) async { final now = DateTime.now(); final canUseCache = _policiesCache != null && _policiesCacheAt != null && now.difference(_policiesCacheAt!) < _cacheTtl; if (canUseCache) { final cachedPolicies = _policiesCache!; if (cachedPolicies.isEmpty) { _appendBotMessage('No active policies found.'); _showRootMenu(); } else { _appendBotMessage('Choose a policy.'); state = state.copyWith( isLoading: false, policies: cachedPolicies, step: ChatbotFlowStep.policyList, options: _policyOptions(cachedPolicies), clearError: true, ); } return; } state = state.copyWith(isLoading: true, clearError: true); try { final policies = await _repository.getPolicies(context); _policiesCache = policies; _policiesCacheAt = DateTime.now(); if (policies.isEmpty) { _appendBotMessage('No active policies found.'); _showRootMenu(); } else { _appendBotMessage('Choose a policy.'); state = state.copyWith( isLoading: false, policies: policies, step: ChatbotFlowStep.policyList, options: _policyOptions(policies), ); } } catch (_) { state = state.copyWith( isLoading: false, errorMessage: 'Unable to load policies. Please try again.', ); _appendBotMessage('Unable to load policies right now.'); _showRootMenu(); } } void selectPolicy(ChatbotPolicy policy) { _appendBotMessage('Selected ${policy.policyName}.'); final isGmcPolicy = _isGmcPolicy(policy); final actionOptions = [ const ChatbotOption(id: 'covered_member_list', label: 'Covered Member List'), if (isGmcPolicy) const ChatbotOption(id: 'ecard_download', label: 'E-Card Download'), if (isGmcPolicy) const ChatbotOption( id: 'covered_hospital_list', label: 'Covered Hospital List', ), const ChatbotOption(id: 'back_to_policies', label: 'Back to Policies'), ]; state = state.copyWith( selectedPolicy: policy, step: ChatbotFlowStep.policyActions, options: actionOptions, clearError: true, ); } bool _isGmcPolicy(ChatbotPolicy policy) { final normalizedType = policy.policyType.trim().toUpperCase(); return normalizedType.startsWith('GMC'); } void showCoveredMembers() { final policy = state.selectedPolicy; if (policy == null) { _appendBotMessage('Please choose a policy first.'); return; } if (policy.members.isEmpty) { _appendBotMessage('No covered members found for this policy.'); } else { final names = policy.members .map((member) => '${member.name} (${member.relationship})') .join('\n'); _appendBotMessage('Covered members:\n$names'); } } Future openEcard(BuildContext context) async { final policy = state.selectedPolicy; if (policy == null) { _appendBotMessage('Please choose a policy first.'); return; } final url = policy.eCardDownloadUrl.trim(); if (url.isEmpty) { _appendBotMessage('E-Card is not available for this policy.'); return; } final uri = Uri.tryParse(url); if (uri == null) { _appendBotMessage('E-Card link is invalid.'); return; } final fileName = _chatbotEcardFileName(policy); final postToken = await TokenService.getPostToken(); final headers = { 'APP-SIGNATURE': 'nhance-2025-signature-T8f6gV9k5fK2wf5V5c4vId3b0J8z6cAm2x8Y', if (postToken != null && postToken.isNotEmpty) 'Authorization': 'Bearer $postToken', }; if (!kIsWeb) { _appendBotMessage('Downloading eCard...'); } final result = await _ecardDownloadService.downloadEcard( url: uri.toString(), fileName: fileName, headers: headers, ); if (!result.success) { _appendBotMessage(result.message ?? 'Unable to download E-Card.'); return; } if (!kIsWeb && result.savedPath != null && result.savedPath!.isNotEmpty) { await EcardDownloadNotificationService.showDownloadCompleted( filePath: result.savedPath!, fileName: fileName, ); } _appendBotMessage(result.message ?? 'E-Card downloaded successfully.'); } String _chatbotEcardFileName(ChatbotPolicy policy) { final sanitized = policy.clientPolicyId.replaceAll(RegExp(r'[^\w\-.]'), '_'); if (sanitized.isEmpty) return 'ecard.pdf'; return 'ecard_$sanitized.pdf'; } Future openHospitalList() async { final policy = state.selectedPolicy; if (policy == null) { _appendBotMessage('Please choose a policy first.'); return; } final url = policy.networkHospitalsUrl.trim(); if (url.isEmpty) { _appendBotMessage('Hospital list link is not available.'); return; } final uri = Uri.tryParse(url); if (uri == null) { _appendBotMessage('Hospital list link is invalid.'); return; } final launched = await launchUrl(uri, mode: LaunchMode.externalApplication); if (!launched) { _appendBotMessage('Unable to open hospital list link.'); return; } _appendBotMessage('Opened hospital list in browser.'); } Future loadClaimsStatus(BuildContext context) async { state = state.copyWith(isLoading: true, clearError: true); try { final claims = await _repository.getClaimStatuses(context); final latestClaims = _latestThreeClaims(claims); if (claims.isEmpty) { _appendBotMessage('No claims found.'); } else { _appendBotMessage('Here is your latest claims status.'); } state = state.copyWith( isLoading: false, step: ChatbotFlowStep.claimStatusList, claimStatuses: latestClaims, options: const [ChatbotOption(id: 'back_to_root', label: 'Back')], ); } catch (_) { state = state.copyWith( isLoading: false, errorMessage: 'Unable to load claims status. Please try again.', ); _appendBotMessage('Unable to load claims status right now.'); _showRootMenu(); } } }