//import 'package:supabase_flutter/supabase_flutter.dart'; import 'package:flutter/foundation.dart'; import '../main.dart'; /// Data model for a collection. class Collection { final String id; final String name; final String? description; final String ownerId; final DateTime createdAt; final String role; // 'owner' or 'member' final int itemCount; final int memberCount; Collection({ required this.id, required this.name, this.description, required this.ownerId, required this.createdAt, required this.role, this.itemCount = 0, this.memberCount = 1, }); bool get isOwner => role == 'owner'; bool get isMember => role == 'member'; bool get isViewer => role == 'viewer'; bool get canModifyCars => isOwner || isMember; } /// Member of a collection. class CollectionMember { final String id; final String userId; final String email; final String role; final DateTime joinedAt; CollectionMember({ required this.id, required this.userId, required this.email, required this.role, required this.joinedAt, }); bool get isOwner => role == 'owner'; bool get isViewer => role == 'viewer'; } /// Service for managing collections and membership. class CollectionService { CollectionService._(); static String _requireUserId() { final userId = supabase.auth.currentUser?.id; if (userId == null) { throw Exception('You must be signed in to perform this action.'); } return userId; } /// Ensures the current user has at least one collection membership. /// Creates a default collection on first login. static Future ensureDefaultCollection() async { final userId = supabase.auth.currentUser?.id; if (userId == null) return null; final existing = await supabase .from('collection_members') .select('collection_id') .eq('user_id', userId) .limit(1); if (existing.isNotEmpty) { return existing.first['collection_id'] as String; } final created = await create( name: 'My Garage', description: 'Your default collection', ); return created.id; } /// Fetch all collections the current user is a member of, /// including item count and member count. static Future> getMyCollections() async { final userId = _requireUserId(); // Get memberships with collection data. final memberships = await supabase .from('collection_members') .select('role, collections(id, name, description, owner_id, created_at)') .eq('user_id', userId); // If the user has no memberships, return early. if (memberships.isEmpty) { return []; } // Collect all distinct collection IDs from memberships. final collectionIds = {}; for (final m in memberships) { final c = m['collections'] as Map; final collectionId = c['id'] as String; collectionIds.add(collectionId); } final collectionIdList = collectionIds.toList(); final itemCounts = await getCollectionItemCounts(collectionIdList); final memberCounts = {}; await Future.wait(collectionIdList.map((collectionId) async { try { final rows = await supabase.rpc('get_collection_members', params: { 'p_collection_id': collectionId, }); memberCounts[collectionId] = (rows as List).length; } catch (e) { debugPrint('CollectionService.getMyCollections member RPC error: $e'); } })); final unresolvedIds = collectionIdList .where((id) => !memberCounts.containsKey(id)) .toList(growable: false); if (unresolvedIds.isNotEmpty) { final members = await supabase .from('collection_members') .select('id, collection_id') .inFilter('collection_id', unresolvedIds); for (final member in members) { final collectionId = member['collection_id'] as String; memberCounts[collectionId] = (memberCounts[collectionId] ?? 0) + 1; } } final collections = []; for (final m in memberships) { final c = m['collections'] as Map; final collectionId = c['id'] as String; collections.add(Collection( id: collectionId, name: c['name'] as String, description: c['description'] as String?, ownerId: c['owner_id'] as String, createdAt: DateTime.parse(c['created_at'] as String), role: m['role'] as String, itemCount: itemCounts[collectionId] ?? 0, memberCount: memberCounts[collectionId] ?? 1, )); } // Sort: owned first, then by name. collections.sort((a, b) { if (a.isOwner && !b.isOwner) return -1; if (!a.isOwner && b.isOwner) return 1; return a.name.compareTo(b.name); }); return collections; } static Future> getCollectionItemCounts( List collectionIds) async { if (collectionIds.isEmpty) return {}; final itemCounts = {}; try { final rows = await supabase.rpc('get_collection_counts', params: { 'p_collection_ids': collectionIds, }); for (final row in rows as List) { final collectionId = row['collection_id'] as String?; if (collectionId == null) continue; itemCounts[collectionId] = (row['total_count'] as num?)?.toInt() ?? 0; } } catch (e) { debugPrint('CollectionService.getCollectionItemCounts RPC error: $e'); } final unresolvedIds = collectionIds .where((collectionId) => !itemCounts.containsKey(collectionId)) .toList(growable: false); if (unresolvedIds.isNotEmpty) { await Future.wait(unresolvedIds.map((collectionId) async { itemCounts[collectionId] = await _countCarsPaginated(collectionId); })); } return itemCounts; } static Future<({int total, int recent})> getCollectionStats( String collectionId) async { try { final rows = await supabase.rpc('get_collection_counts', params: { 'p_collection_ids': [collectionId], }); if (rows is List && rows.isNotEmpty) { final first = rows.first as Map; return ( total: (first['total_count'] as num?)?.toInt() ?? 0, recent: (first['recent_count'] as num?)?.toInt() ?? 0, ); } } catch (e) { debugPrint('CollectionService.getCollectionStats RPC error: $e'); } final weekAgoIso = DateTime.now() .subtract(const Duration(days: 7)) .toUtc() .toIso8601String(); final total = await _countCarsPaginated(collectionId); final recent = await _countCarsPaginated(collectionId, sinceIso: weekAgoIso); return (total: total, recent: recent); } static Future _countCarsPaginated(String collectionId, {String? sinceIso}) async { const pageSize = 1000; var from = 0; var total = 0; while (true) { dynamic query = supabase .from('hotwheels') .select('id') .eq('collection_id', collectionId) .range(from, from + pageSize - 1); if (sinceIso != null) { query = query.gte('created_at', sinceIso); } final rows = await query; final count = (rows as List).length; total += count; if (count < pageSize) break; from += pageSize; } return total; } /// Create a new collection. The caller is automatically added as owner. static Future create({ required String name, String? description, }) async { final userId = _requireUserId(); final trimmedDescription = description?.trim(); final normalizedDescription = (trimmedDescription != null && trimmedDescription.isNotEmpty) ? trimmedDescription : null; try { final rpcResult = await supabase.rpc( 'create_collection_with_owner', params: { 'p_name': name, 'p_description': normalizedDescription, }, ); final row = rpcResult is List ? (rpcResult.isNotEmpty ? rpcResult.first as Map : {}) : rpcResult as Map; if (row.isNotEmpty) { return Collection( id: row['id'] as String, name: row['name'] as String, description: row['description'] as String?, ownerId: row['owner_id'] as String, createdAt: DateTime.parse(row['created_at'] as String), role: 'owner', itemCount: 0, memberCount: 1, ); } } catch (e) { final message = e.toString(); final rpcUnavailable = message.contains('create_collection_with_owner') && (message.contains('not found') || message.contains('does not exist') || message.contains('PGRST202')); if (!rpcUnavailable) { rethrow; } debugPrint('CollectionService.create RPC unavailable, using fallback: $e'); } final row = await supabase .from('collections') .insert({ 'name': name, 'owner_id': userId, 'description': normalizedDescription, }) .select() .single(); // Add owner as a member. try { await supabase.from('collection_members').insert({ 'collection_id': row['id'], 'user_id': userId, 'role': 'owner', }); } catch (e) { debugPrint('CollectionService.create member insert failed: $e'); try { await supabase.from('collections').delete().eq('id', row['id']); } catch (cleanupError) { debugPrint('CollectionService.create rollback failed: $cleanupError'); } rethrow; } return Collection( id: row['id'] as String, name: row['name'] as String, description: row['description'] as String?, ownerId: userId, createdAt: DateTime.parse(row['created_at'] as String), role: 'owner', itemCount: 0, memberCount: 1, ); } /// Update a collection's name/description. Owner only. static Future update({ required String collectionId, required String name, String? description, }) async { final userId = _requireUserId(); final collection = await supabase .from('collections') .select('owner_id') .eq('id', collectionId) .maybeSingle(); if (collection == null) { throw Exception('Collection not found.'); } if (collection['owner_id'] != userId) { throw Exception('Only the collection owner can perform this action.'); } await supabase.from('collections').update({ 'name': name, if (description != null && description.isNotEmpty) 'description': description, }).eq('id', collectionId); } /// Delete a collection. Owner only. Cascade deletes members & items. static Future delete(String collectionId) async { final userId = _requireUserId(); final collection = await supabase .from('collections') .select('owner_id') .eq('id', collectionId) .maybeSingle(); if (collection == null) { throw Exception('Collection not found.'); } if (collection['owner_id'] != userId) { throw Exception('Only the collection owner can perform this action.'); } await supabase.from('collections').delete().eq('id', collectionId); } /// Get members of a collection (with emails via RPC). static Future> getMembers( String collectionId) async { final rows = await supabase.rpc('get_collection_members', params: { 'p_collection_id': collectionId, }); final members = []; for (final r in rows) { members.add(CollectionMember( id: r['id'] as String, userId: r['user_id'] as String, email: r['email'] as String? ?? 'unknown', role: r['role'] as String, joinedAt: DateTime.parse(r['created_at'] as String), )); } return members; } /// Invite a user by email. Looks up auth.users via an RPC function, /// then inserts a collection_members row. static Future inviteByEmail({ required String collectionId, required String email, String role = 'member', }) async { final normalizedRole = role.trim().toLowerCase(); if (normalizedRole != 'member' && normalizedRole != 'viewer') { throw Exception('Unsupported role "$role".'); } final currentUserId = _requireUserId(); final collection = await supabase .from('collections') .select('owner_id') .eq('id', collectionId) .maybeSingle(); if (collection == null) { throw Exception('Collection not found.'); } if (collection['owner_id'] != currentUserId) { throw Exception('Only the collection owner can invite members.'); } // Call an RPC to look up the user ID by email. final result = await supabase.rpc('get_user_id_by_email', params: { 'lookup_email': email.trim().toLowerCase(), }); if (result == null || (result is List && result.isEmpty)) { throw Exception( 'No user found with that email. They must create an account first.'); } final userId = result is List ? result.first['id'] as String : result as String; if (userId == currentUserId) { throw Exception('You are already in this collection.'); } // Check if already a member. final existing = await supabase .from('collection_members') .select('id') .eq('collection_id', collectionId) .eq('user_id', userId) .maybeSingle(); if (existing != null) { throw Exception('This user is already a member of this collection.'); } try { await supabase.from('collection_members').insert({ 'collection_id': collectionId, 'user_id': userId, 'role': normalizedRole, }); } catch (e) { final message = e.toString(); if (normalizedRole == 'viewer' && message.contains('collection_members_role_check')) { throw Exception( 'Viewer role is currently unavailable. Please contact the app administrator.', ); } rethrow; } } /// Remove a member from a collection. static Future removeMember({ required String collectionId, required String memberUserId, }) async { final currentUserId = _requireUserId(); final collection = await supabase .from('collections') .select('owner_id') .eq('id', collectionId) .maybeSingle(); if (collection == null) { throw Exception('Collection not found.'); } if (collection['owner_id'] != currentUserId) { throw Exception('Only the collection owner can remove members.'); } if (memberUserId == collection['owner_id']) { throw Exception('Collection owner cannot be removed.'); } final membersBefore = await getMembers(collectionId); final targetMembership = membersBefore.where((member) => member.userId == memberUserId); if (targetMembership.isEmpty) { throw Exception('Member not found in this collection.'); } final target = targetMembership.first; if (target.role == 'owner') { throw Exception('Collection owner cannot be removed.'); } var deleted = false; try { await supabase.rpc('remove_collection_member', params: { 'p_collection_id': collectionId, 'p_member_user_id': memberUserId, }); deleted = true; } catch (e) { final message = e.toString(); final rpcUnavailable = message.contains('remove_collection_member') && (message.contains('not found') || message.contains('does not exist') || message.contains('PGRST202')); if (!rpcUnavailable) { rethrow; } await supabase .from('collection_members') .delete() .eq('collection_id', collectionId) .eq('user_id', memberUserId); deleted = true; } if (!deleted) { throw Exception('Member removal failed.'); } final membersAfter = await getMembers(collectionId); final stillExists = membersAfter.any((member) => member.userId == memberUserId); if (stillExists) { throw Exception( 'Member could not be removed. Please try again in a moment.', ); } } /// Leave a collection (for non-owners). static Future leave(String collectionId) async { final userId = _requireUserId(); final membership = await supabase .from('collection_members') .select('role') .eq('collection_id', collectionId) .eq('user_id', userId) .maybeSingle(); if (membership == null) { throw Exception('You are not a member of this collection.'); } if (membership['role'] == 'owner') { throw Exception('Owner cannot leave. Delete the collection instead.'); } await supabase .from('collection_members') .delete() .eq('collection_id', collectionId) .eq('user_id', userId); } }