import 'dart:async'; import '../models/user_model.dart'; import '../../bootstrap/supabase_client.dart'; import '../../core/utils/unit_conversion_utils.dart'; import 'package:supabase_flutter/supabase_flutter.dart' as supabase; import 'package:google_sign_in/google_sign_in.dart'; class AuthRepository { final supabase.SupabaseClient? _client; StreamSubscription? _authStateSubscription; AuthRepository([supabase.SupabaseClient? client]) : _client = client; Stream get authStateChanges { final client = supabaseClient; if (client == null) { // Return a stream that never emits if Supabase is not initialized return Stream.empty(); } return client.auth.onAuthStateChange.map((data) { final session = data.session; if (session?.user != null) { return _mapSupabaseUserToAppUser(session!.user); } return null; }); } User? get currentUser { final client = supabaseClient; if (client == null) return null; final user = client.auth.currentUser; return user != null ? _mapSupabaseUserToAppUser(user) : null; } bool get isAuthenticated { final client = supabaseClient; if (client == null) return false; return client.auth.currentUser != null; } String? get currentUserId { final client = supabaseClient; if (client == null) return null; return client.auth.currentUser?.id; } Future isSessionValid() async { assert(_client != null, 'Client must not be null'); final session = _client!.auth.currentSession; if (session == null) return false; final now = DateTime.now(); final expiresAt = session.expiresAt; if (expiresAt == null) return true; return now.isBefore(DateTime.fromMillisecondsSinceEpoch(expiresAt * 1000)); } Future refreshSession() async { assert(_client != null, 'Client must not be null'); try { await _client!.auth.refreshSession(); } catch (e) { throw Exception('Failed to refresh session: $e'); } } Future getCurrentSession() async { assert(_client != null, 'Client must not be null'); return _client!.auth.currentSession; } void listenToAuthStateChanges(Function(User?) callback) { assert(_client != null, 'Client must not be null'); _authStateSubscription = _client!.auth.onAuthStateChange.listen((data) { final session = data.session; if (session?.user != null) { callback(_mapSupabaseUserToAppUser(session!.user)); } else { callback(null); } }); } void dispose() { _authStateSubscription?.cancel(); } Future signInWithEmail(String email, String password) async { assert(_client != null, 'Client must not be null'); await _client!.auth.signInWithPassword(email: email, password: password); } Future signUpWithEmail(String email, String password, String username, {double? heightCm, double? weightKg, int? age, Gender? gender, HeightUnit? heightUnit, WeightUnit? weightUnit}) async { assert(_client != null, 'Client must not be null'); final response = await _client!.auth.signUp( email: email, password: password, data: {'username': username}, ); if (response.user != null) { await _createUserProfile(response.user!.id, username, email, heightCm: heightCm, weightKg: weightKg, age: age, gender: gender, heightUnit: heightUnit, weightUnit: weightUnit); } } Future signInWithGoogle() async { assert(_client != null, 'Client must not be null'); try { final GoogleSignIn googleSignIn = GoogleSignIn( scopes: ['email', 'profile'], ); // Check if user is already signed in final googleUser = await googleSignIn.signInSilently(); if (googleUser != null) { await _handleGoogleUser(googleUser); return; } // Sign in interactively final interactiveUser = await googleSignIn.signIn(); if (interactiveUser == null) { throw Exception('Google sign-in was cancelled'); } await _handleGoogleUser(interactiveUser); } catch (e) { throw Exception('Google sign-in failed: ${e.toString()}'); } } Future _handleGoogleUser(dynamic googleUser) async { assert(_client != null, 'Client must not be null'); try { final googleAuth = await googleUser.authentication; final idToken = googleAuth.idToken; final accessToken = googleAuth.accessToken; if (idToken == null && accessToken == null) { throw Exception('No ID token or access token from Google sign-in'); } final response = await _client!.auth.signInWithIdToken( provider: supabase.OAuthProvider.google, idToken: idToken, accessToken: accessToken, ); if (response.user != null) { await _ensureUserProfileExists(response.user!.id, response.user!); } } catch (e) { throw Exception('Failed to authenticate with Google: ${e.toString()}'); } } Future signInWithGithub() async { assert(_client != null, 'Client must not be null'); await _client!.auth.signInWithOAuth( supabase.OAuthProvider.github, ); } Future signOut() async { assert(_client != null, 'Client must not be null'); await _client!.auth.signOut(); } Future resetPassword(String email) async { assert(_client != null, 'Client must not be null'); await _client!.auth.resetPasswordForEmail(email); } Future updateProfile({ String? username, String? bio, String? avatarUrl, bool? isPublicProfile, double? heightCm, double? weightKg, int? age, Gender? gender, HeightUnit? heightUnit, WeightUnit? weightUnit, }) async { assert(_client != null, 'Client must not be null'); final userId = _client!.auth.currentUser?.id; if (userId == null) throw Exception('User not authenticated'); final updates = {}; if (username != null) updates['username'] = username; if (bio != null) updates['bio'] = bio; if (avatarUrl != null) updates['avatar_url'] = avatarUrl; if (isPublicProfile != null) updates['is_public_profile'] = isPublicProfile; if (heightCm != null) updates['height_cm'] = heightCm; if (weightKg != null) updates['weight_kg'] = weightKg; if (age != null) updates['age'] = age; if (gender != null) updates['gender'] = gender.toDatabaseString(); if (heightUnit != null) updates['height_unit'] = heightUnit.code; if (weightUnit != null) updates['weight_unit'] = weightUnit.code; updates['updated_at'] = DateTime.now().toIso8601String(); await _client! .from('users') .update(updates) .eq('id', userId); } Future _createUserProfile(String userId, String username, String email, {double? heightCm, double? weightKg, int? age, Gender? gender, HeightUnit? heightUnit, WeightUnit? weightUnit}) async { assert(_client != null, 'Client must not be null'); final now = DateTime.now().toIso8601String(); try { // First try with the regular client (might fail due to RLS) final response = await _client!.from('users').insert({ 'id': userId, 'username': username, 'email': email, 'height_cm': heightCm, 'weight_kg': weightKg, 'age': age, 'gender': gender?.toDatabaseString(), 'height_unit': heightUnit?.code ?? HeightUnit.metric.code, 'weight_unit': weightUnit?.code ?? WeightUnit.metric.code, 'created_at': now, 'updated_at': now, }).select(); if (response.isNotEmpty) { return _mapSupabaseDataToUser(response.first); } } catch (e) { // If regular client fails due to RLS, try with service role client try { final serviceClient = getServiceRoleClient(); final response = await serviceClient.from('users').insert({ 'id': userId, 'username': username, 'email': email, 'height_cm': heightCm, 'weight_kg': weightKg, 'age': age, 'gender': gender?.toDatabaseString(), 'height_unit': heightUnit?.code ?? HeightUnit.metric.code, 'weight_unit': weightUnit?.code ?? WeightUnit.metric.code, 'created_at': now, 'updated_at': now, }).select(); if (response.isNotEmpty) { return _mapSupabaseDataToUser(response.first); } } catch (e2) { // If both fail, create a basic user profile from auth metadata // This allows the app to function even without database profile creation return User( id: userId, username: username, email: email, storedAge: age, heightCm: heightCm, weightKg: weightKg, gender: gender, heightUnit: heightUnit ?? HeightUnit.metric, weightUnit: weightUnit ?? WeightUnit.metric, createdAt: DateTime.parse(now), updatedAt: DateTime.parse(now), ); } } // Fallback if no response but no error return User( id: userId, username: username, email: email, storedAge: age, heightCm: heightCm, weightKg: weightKg, gender: gender, heightUnit: heightUnit ?? HeightUnit.metric, weightUnit: weightUnit ?? WeightUnit.metric, createdAt: DateTime.parse(now), updatedAt: DateTime.parse(now), ); } Future _ensureUserProfileExists(String userId, dynamic supabaseUser) async { assert(_client != null, 'Client must not be null'); try { final existingProfile = await _client! .from('users') .select('id') .eq('id', userId) .maybeSingle(); if (existingProfile == null) { final username = supabaseUser.userMetadata?['username'] ?? 'user_${userId.substring(0, 8)}'; final email = supabaseUser.email ?? ''; await _createUserProfile(userId, username, email); } } catch (e) { // If RLS policy prevents reading, we'll assume the profile doesn't exist // and let the _createUserProfile method handle the creation gracefully final username = supabaseUser.userMetadata?['username'] ?? 'user_${userId.substring(0, 8)}'; final email = supabaseUser.email ?? ''; await _createUserProfile(userId, username, email); } } User _mapSupabaseUserToAppUser(dynamic supabaseUser) { return User( id: supabaseUser.id, username: supabaseUser.userMetadata?['username'] ?? '', email: supabaseUser.email ?? '', createdAt: DateTime.tryParse(supabaseUser.createdAt ?? '') ?? DateTime.now(), updatedAt: DateTime.tryParse(supabaseUser.updatedAt ?? '') ?? DateTime.now(), ); } User _mapSupabaseDataToUser(Map data) { return User( id: data['id'], username: data['username'], email: data['email'], avatarUrl: data['avatar_url'], bio: data['bio'], isPublicProfile: data['is_public_profile'] ?? false, countdownStartDate: data['countdown_start_date'] != null ? DateTime.parse(data['countdown_start_date']) : null, countdownEndDate: data['countdown_end_date'] != null ? DateTime.parse(data['countdown_end_date']) : null, gender: data['gender'] != null ? Gender.fromString(data['gender']) : null, storedAge: data['age'] as int?, heightCm: (data['height_cm'] as num?)?.toDouble(), weightKg: (data['weight_kg'] as num?)?.toDouble(), createdAt: DateTime.parse(data['created_at']), updatedAt: DateTime.parse(data['updated_at']), ); } }