import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:mileograph_flutter/objects/objects.dart'; import 'package:mileograph_flutter/services/authservice.dart'; import 'package:mileograph_flutter/services/data_service.dart'; class ProfilePage extends StatefulWidget { const ProfilePage({super.key}); @override State createState() => _ProfilePageState(); } class _ProfilePageState extends State { final TextEditingController _searchController = TextEditingController(); List _searchResults = []; bool _searching = false; String? _searchError; bool _fetched = false; UserSummary? _selectedUser; Friendship? _status; bool _statusLoading = false; bool _actionLoading = false; @override void initState() { super.initState(); WidgetsBinding.instance.addPostFrameCallback((_) { if (!mounted || _fetched) return; _fetched = true; final data = context.read(); data.fetchFriendships(); data.fetchPendingFriendships(); }); } @override void dispose() { _searchController.dispose(); super.dispose(); } Future _searchUsers() async { final query = _searchController.text.trim(); if (query.isEmpty) { setState(() { _searchResults = []; _searchError = null; }); return; } setState(() { _searching = true; _searchError = null; }); try { final results = await context.read().searchUsers(query); if (!mounted) return; setState(() { _searchResults = results; }); } catch (e) { if (!mounted) return; setState(() { _searchError = 'Search failed'; }); } finally { if (mounted) setState(() => _searching = false); } } Future _loadStatus(UserSummary user) async { setState(() { _selectedUser = user; _statusLoading = true; }); try { final status = await context.read().fetchFriendshipStatus(user.userId); if (!mounted) return; setState(() => _status = status); } catch (_) { if (!mounted) return; setState(() => _status = null); } finally { if (mounted) setState(() => _statusLoading = false); } } Future _sendRequest(UserSummary user) async { setState(() => _actionLoading = true); try { final status = await context.read().requestFriendship( user.userId, targetUser: user, ); if (!mounted) return; setState(() => _status = status); _showSnack('Friend request sent'); } catch (e) { _showSnack('Failed to send request: $e'); } finally { if (mounted) setState(() => _actionLoading = false); } } Future _cancelRequest(Friendship status) async { final id = status.id; if (id == null || id.isEmpty) return; setState(() => _actionLoading = true); try { await context.read().cancelFriendship(id); if (!mounted) return; setState(() => _status = status.copyWith(status: 'none')); _showSnack('Request cancelled'); } catch (e) { _showSnack('Failed to cancel: $e'); } finally { if (mounted) setState(() => _actionLoading = false); } } Future _accept(Friendship status) async { final id = status.id; if (id == null || id.isEmpty) return; setState(() => _actionLoading = true); try { final updated = await context.read().acceptFriendship(id); if (!mounted) return; setState(() => _status = updated); _showSnack('Friend request accepted'); } catch (e) { _showSnack('Failed to accept: $e'); } finally { if (mounted) setState(() => _actionLoading = false); } } Future _reject(Friendship status) async { final id = status.id; if (id == null || id.isEmpty) return; setState(() => _actionLoading = true); try { final updated = await context.read().rejectFriendship(id); if (!mounted) return; setState(() => _status = updated); _showSnack('Friend request rejected'); } catch (e) { _showSnack('Failed to reject: $e'); } finally { if (mounted) setState(() => _actionLoading = false); } } Future _unfriend(Friendship status) async { final id = status.id; if (id == null || id.isEmpty) return; final confirm = await showDialog( context: context, builder: (ctx) => AlertDialog( title: const Text('Remove friend'), content: const Text('Are you sure you want to remove this friend?'), actions: [ TextButton( onPressed: () => Navigator.of(ctx).pop(false), child: const Text('Cancel'), ), TextButton( onPressed: () => Navigator.of(ctx).pop(true), child: const Text('Remove'), ), ], ), ); if (confirm != true) return; if (!mounted) return; setState(() => _actionLoading = true); try { await context.read().deleteFriendship(id); if (!mounted) return; setState(() => _status = status.copyWith(status: 'none')); _showSnack('Friend removed'); } catch (e) { _showSnack('Failed to remove friend: $e'); } finally { if (mounted) setState(() => _actionLoading = false); } } void _showSnack(String message) { ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(message))); } @override Widget build(BuildContext context) { final auth = context.watch(); final data = context.watch(); final statsUser = data.homepageStats?.user; final name = auth.fullName?.isNotEmpty == true ? auth.fullName! : (statsUser?.fullName ?? ''); final username = auth.username ?? statsUser?.username ?? ''; final email = statsUser?.email ?? ''; return Scaffold( appBar: AppBar( title: const Text('Profile'), ), body: RefreshIndicator( onRefresh: () async { await data.fetchFriendships(); await data.fetchPendingFriendships(); if (_selectedUser != null) { await _loadStatus(_selectedUser!); } }, child: ListView( padding: const EdgeInsets.all(16), children: [ _buildUserCard(name: name, username: username, email: email), const SizedBox(height: 16), _buildSearchSection(), const SizedBox(height: 16), _buildSelectedUserSection(auth), const SizedBox(height: 16), _buildFriendsList(auth), ], ), ), ); } Widget _buildUserCard({ required String name, required String username, required String email, }) { return Card( child: ListTile( leading: const CircleAvatar(child: Icon(Icons.person)), title: Text(name.isNotEmpty ? name : 'You'), subtitle: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ if (username.isNotEmpty) Text('@$username'), if (email.isNotEmpty) Text(email), ], ), ), ); } Widget _buildSearchSection() { return Card( child: Padding( padding: const EdgeInsets.all(12.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( 'Find a user', style: Theme.of(context).textTheme.titleMedium, ), const SizedBox(height: 8), Row( children: [ Expanded( child: TextField( controller: _searchController, decoration: const InputDecoration( labelText: 'Search by username, email, or name', border: OutlineInputBorder(), ), onSubmitted: (_) => _searchUsers(), ), ), const SizedBox(width: 8), ElevatedButton( onPressed: _searching ? null : _searchUsers, child: _searching ? const SizedBox( width: 16, height: 16, child: CircularProgressIndicator(strokeWidth: 2), ) : const Text('Search'), ), ], ), if (_searchError != null) Padding( padding: const EdgeInsets.only(top: 8.0), child: Text( _searchError!, style: TextStyle(color: Theme.of(context).colorScheme.error), ), ), if (_searchResults.isNotEmpty) const SizedBox(height: 12), if (_searchResults.isNotEmpty) ..._searchResults.map( (user) => ListTile( leading: const Icon(Icons.person), title: Text(user.displayName), subtitle: user.username.isNotEmpty ? Text('@${user.username}') : null, trailing: TextButton( onPressed: () => _loadStatus(user), child: const Text('View'), ), ), ), ], ), ), ); } Widget _buildSelectedUserSection(AuthService auth) { final user = _selectedUser; if (user == null) return const SizedBox.shrink(); final status = _status; final loading = _statusLoading; final isSelf = auth.userId == user.userId; return Card( child: Padding( padding: const EdgeInsets.all(12.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Text( user.displayName, style: Theme.of(context).textTheme.titleMedium, ), const SizedBox(width: 8), if (loading) const SizedBox( width: 16, height: 16, child: CircularProgressIndicator(strokeWidth: 2), ), if (!loading && status != null) _buildStatusChip(status, auth), ], ), if (user.username.isNotEmpty) Text('@${user.username}'), const SizedBox(height: 8), if (!isSelf) _buildActions(status, user, auth), if (isSelf) const Text('This is you.', style: TextStyle(fontStyle: FontStyle.italic)), ], ), ), ); } Widget _buildStatusChip(Friendship status, AuthService auth) { String label = status.status; Color color = Colors.grey; switch (status.status.toLowerCase()) { case 'accepted': label = 'Friends'; color = Colors.green; break; case 'pending': final isRequester = status.requesterId == auth.userId; label = isRequester ? 'Pending (you sent)' : 'Pending (waiting on you)'; color = Colors.orange; break; case 'blocked': color = Colors.red; label = 'Blocked'; break; case 'declined': case 'rejected': label = 'Declined'; break; default: label = 'Not friends'; } final bg = Color.alphaBlend( color.withValues(alpha: 0.15), Theme.of(context).colorScheme.surface, ); return Container( margin: const EdgeInsets.only(left: 6), padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6), decoration: BoxDecoration( color: bg, borderRadius: BorderRadius.circular(20), ), child: Text(label), ); } Widget _buildActions( Friendship? status, UserSummary user, AuthService auth, ) { if (status == null) { return const SizedBox.shrink(); } final isRequester = status.requesterId == auth.userId; final id = status.id; final buttons = []; if (status.isNone || status.isDeclined) { buttons.add( ElevatedButton.icon( onPressed: _actionLoading ? null : () => _sendRequest(user), icon: const Icon(Icons.person_add), label: const Text('Send friend request'), ), ); } else if (status.isPending) { if (isRequester) { buttons.add( OutlinedButton( onPressed: _actionLoading || id == null || id.isEmpty ? null : () => _cancelRequest(status), child: const Text('Cancel request'), ), ); } else { buttons.add( ElevatedButton( onPressed: _actionLoading || id == null || id.isEmpty ? null : () => _accept(status), child: const Text('Accept'), ), ); buttons.add( OutlinedButton( onPressed: _actionLoading || id == null || id.isEmpty ? null : () => _reject(status), child: const Text('Reject'), ), ); } } else if (status.isAccepted) { buttons.add( ElevatedButton.icon( onPressed: _actionLoading || id == null || id.isEmpty ? null : () => _unfriend(status), icon: const Icon(Icons.person_remove), label: const Text('Unfriend'), ), ); // Block action temporarily removed until backend support exists. } else if (status.isBlocked) { buttons.add(const Text('User is blocked.')); } if (buttons.isEmpty) return const SizedBox.shrink(); return Wrap( spacing: 8, runSpacing: 8, children: buttons, ); } Widget _buildFriendsList(AuthService auth) { final data = context.watch(); final friends = data.friendships; final incoming = data.pendingIncoming; final outgoing = data.pendingOutgoing; final loading = data.isFriendshipsLoading; final pendingLoading = data.isPendingFriendshipsLoading; return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ if (pendingLoading && incoming.isEmpty && outgoing.isEmpty) const Padding( padding: EdgeInsets.symmetric(vertical: 12.0), child: Center(child: CircularProgressIndicator()), ), if (incoming.isNotEmpty || outgoing.isNotEmpty) Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( 'Pending requests', style: Theme.of(context).textTheme.titleMedium, ), const SizedBox(height: 8), if (outgoing.isNotEmpty) ...outgoing.map((f) { final otherUser = _otherUser(f, auth.userId); return Card( child: ListTile( leading: const Icon(Icons.person), title: Text(otherUser?.displayName ?? 'User'), subtitle: otherUser?.username.isNotEmpty == true ? Text('@${otherUser!.username}') : null, trailing: Row( mainAxisSize: MainAxisSize.min, children: [ const Icon(Icons.north_east, size: 18), const SizedBox(width: 6), TextButton( onPressed: f.id == null || _actionLoading ? null : () => _cancelRequest(f), child: const Text('Cancel'), ), ], ), ), ); }), if (incoming.isNotEmpty && outgoing.isNotEmpty) const SizedBox(height: 8), if (incoming.isNotEmpty) ...incoming.map((f) { final otherUser = _otherUser(f, auth.userId); return Card( child: ListTile( leading: const Icon(Icons.person), title: Text(otherUser?.displayName ?? 'User'), subtitle: otherUser?.username.isNotEmpty == true ? Text('@${otherUser!.username}') : null, trailing: Row( mainAxisSize: MainAxisSize.min, children: [ const Icon(Icons.south_west, size: 18), const SizedBox(width: 6), Wrap( spacing: 8, children: [ TextButton( onPressed: f.id == null || _actionLoading ? null : () => _accept(f), child: const Text('Accept'), ), TextButton( onPressed: f.id == null || _actionLoading ? null : () => _reject(f), child: const Text('Reject'), ), ], ), ], ), ), ); }), const SizedBox(height: 12), ], ), Text( 'Friends', style: Theme.of(context).textTheme.titleMedium, ), const SizedBox(height: 8), if (loading && friends.isEmpty) const Padding( padding: EdgeInsets.symmetric(vertical: 12.0), child: Center(child: CircularProgressIndicator()), ) else if (friends.isEmpty) const Text('No friends yet.') else ...friends.map((f) { final otherUser = _otherUser(f, auth.userId); return Card( child: ListTile( leading: const Icon(Icons.person), title: Text(otherUser?.displayName ?? 'User'), subtitle: otherUser?.username.isNotEmpty == true ? Text('@${otherUser!.username}') : null, trailing: TextButton( onPressed: () { final user = otherUser; if (user != null) { _loadStatus(user); } }, child: const Text('Manage'), ), ), ); }), ], ); } UserSummary? _otherUser(Friendship friendship, String? currentUserId) { final selfId = currentUserId ?? ''; if (friendship.requester?.userId == selfId) return friendship.addressee; if (friendship.addressee?.userId == selfId) return friendship.requester; if (friendship.addresseeId == selfId && friendship.requester != null) { return friendship.requester; } if (friendship.requesterId == selfId && friendship.addressee != null) { return friendship.addressee; } if (friendship.addressee != null) return friendship.addressee; if (friendship.requester != null) return friendship.requester; return null; } }