//import 'package:supabase_flutter/supabase_flutter.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._(); /// 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 = supabase.auth.currentUser!.id; // 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(); // Fetch all items for these collections in a single query and count them in memory. final items = await supabase .from('hotwheels') .select('id, collection_id') .inFilter('collection_id', collectionIdList); final itemCounts = {}; for (final item in items) { final collectionId = item['collection_id'] as String; itemCounts[collectionId] = (itemCounts[collectionId] ?? 0) + 1; } 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 (_) { // Keep fallback below when RPC fails. } })); // Fallback member count for collections where RPC did not return data. 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; } /// Create a new collection. The caller is automatically added as owner. static Future create({ required String name, String? description, }) async { final userId = supabase.auth.currentUser!.id; final row = await supabase .from('collections') .insert({ 'name': name, 'owner_id': userId, if (description != null && description.isNotEmpty) 'description': description, }) .select() .single(); // Add owner as a member. await supabase.from('collection_members').insert({ 'collection_id': row['id'], 'user_id': userId, 'role': 'owner', }); 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 = supabase.auth.currentUser!.id; 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 = supabase.auth.currentUser!.id; 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 = supabase.auth.currentUser!.id; 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 not enabled in your database yet. Please apply the viewer-role migration first.', ); } rethrow; } } /// Remove a member from a collection. static Future removeMember({ required String collectionId, required String memberUserId, }) async { final currentUserId = supabase.auth.currentUser!.id; 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 members = await getMembers(collectionId); final targetMembership = members.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.'); } final deleted = await supabase .from('collection_members') .delete() .eq('collection_id', collectionId) .eq('user_id', memberUserId) .select('id'); if (deleted.isEmpty) { throw Exception( 'Member could not be removed (blocked by database policy).', ); } final stillExists = await supabase .from('collection_members') .select('id') .eq('collection_id', collectionId) .eq('user_id', memberUserId) .maybeSingle(); if (stillExists != null) { throw Exception('Member removal did not persist. Please try again.'); } } /// Leave a collection (for non-owners). static Future leave(String collectionId) async { final userId = supabase.auth.currentUser!.id; 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); } }