import 'dart:convert';
import 'package:crypto/crypto.dart';
import 'package:flutter/foundation.dart' show kIsWeb;
import 'package:flutter/services.dart' show PlatformException;
import 'package:flutter_facebook_auth/flutter_facebook_auth.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:google_sign_in/google_sign_in.dart';
import 'package:logger/logger.dart';
import 'package:kasy_kit/core/data/api/base_api_exceptions.dart';
import 'package:kasy_kit/core/data/entities/user_entity.dart';
import 'package:kasy_kit/environnements.dart';
import 'package:kasy_kit/google_auth_options.dart';
import 'package:kasy_kit/features/authentication/api/authentication_api_interface.dart';
import 'package:kasy_kit/features/authentication/repositories/exceptions/authentication_exceptions.dart';
import 'package:sign_in_with_apple/sign_in_with_apple.dart';
import 'package:supabase_flutter/supabase_flutter.dart';

final authenticationApiProvider = Provider<AuthenticationApi>(
  (ref) => SupabaseAuthenticationApi(
    client: Supabase.instance.client,
    environment: ref.watch(environmentProvider),
  ),
);

class SupabaseAuthenticationApi implements AuthenticationApi {
  final SupabaseClient client;
  final Environment environment;
  final Logger _logger = Logger();

  SupabaseAuthenticationApi({
    required this.client,
    required this.environment,
  });

  @override
  Future<void> init() async {}

  @override
  Future<void> recoverPassword(String email) {
    return client.auth.resetPasswordForEmail(email);
  }

  @override
  Future<Credentials?> get() async {
    final user = client.auth.currentUser;
    if (user == null) {
      return null;
    }
    return Credentials(id: user.id, token: '');
  }

  @override
  Future<Credentials> signup(String email, String password) async {
    if (client.auth.currentUser?.isAnonymous == true) {
      // Convert anonymous → email user.
      // Must set email first (Supabase rejects password update on anonymous user
      // without email/phone), then set password in a second call.
      try {
        await client.auth.updateUser(UserAttributes(email: email));
      } on AuthException catch (e) {
        if (e.code != 'email_confirmation_required') {
          // Email already in use — delete orphaned anonymous user then sign in.
          await _deleteCurrentAnonymousUser();
          final res = await client.auth.signInWithPassword(email: email, password: password);
          return Credentials(id: res.user!.id, token: res.session?.accessToken ?? '');
        }
        rethrow;
      }
      // If email confirmation is enabled in Supabase, updateUser(email) triggers
      // a confirmation email but does not apply the email immediately. The user
      // stays anonymous until they click the link. Detect this and inform the user.
      if (client.auth.currentUser?.email == null) {
        throw const AuthException(
          'A confirmation email was sent. Please check your inbox to complete signup.',
          code: 'email_confirmation_required',
        );
      }
      await client.auth.updateUser(UserAttributes(password: password));
      final userId = client.auth.currentUser?.id;
      if (userId == null) {
        throw 'Error while updating user';
      }
      return Credentials(
        id: userId,
        token: client.auth.currentSession?.accessToken ?? '',
      );
    }
    return client.auth
        .signUp(
          email: email,
          password: password,
        )
        .then(
          (value) => Credentials(
            id: value.user!.id,
            token: value.session?.accessToken ?? '',
          ),
          onError: (error) {
            Logger().e("Error while signup: $error");
            Logger().e('''
==============================================================
💡 Please check you enabled email authentication in Supabase
  (Supabase dashboard > Authentication > Providers > Email (enable it))
  -> disable Confirm email as you are on mobile. You don't want a user to leave the app to confirm email.
Note: wait a minute after enabling before trying again. It takes a bit of time to propagate.
Second note: Ensure you installed database schema and policies : https://kasy.dev/docs/start/supabase-setup/
==============================================================
                ''');
            return error;
          },
        );
  }

  @override
  Future<Credentials> signin(String email, String password) {
    return client.auth
        .signInWithPassword(
          email: email,
          password: password,
        )
        .then(
          (value) => Credentials(
            id: value.user!.id,
            token: value.session?.accessToken ?? '',
          ),
        );
  }

  @override
  Future<Credentials> signinAnonymously() async {
    try {
      final value = await client.auth.signInAnonymously();
      return Credentials(
        id: value.user!.id,
        token: value.session?.accessToken ?? '',
      );
    } catch (error) {
      Logger().e("Error while signing in anonymously: $error");
      Logger().e('''
==============================================================
💡 Please check you enabled anonymous sign-in in Supabase
  (Supabase dashboard > project settings > Authentication > Allow anonymous sign-in (don't enable captcha)
Note: wait a minute after enabling anonymous sign-in before trying again. It takes a bit of time to propagate.
==============================================================
      ''');
      rethrow;
    }
  }

  @override
  Future<Credentials> signinWithApple() async {
    final rawNonce = client.auth.generateRawNonce();
    final hashedNonce = sha256.convert(utf8.encode(rawNonce)).toString();

    late final AuthorizationCredentialAppleID credential;
    try {
      credential = await SignInWithApple.getAppleIDCredential(
        scopes: [
          AppleIDAuthorizationScopes.email,
          // AppleIDAuthorizationScopes.fullName, // Enable if you want to get the user name
        ],
        nonce: hashedNonce,
      );
    } on SignInWithAppleAuthorizationException catch (e) {
      if (e.code == AuthorizationErrorCode.canceled) throw const UserCancelledSignInException();
      rethrow;
    }

    final idToken = credential.identityToken;
    if (idToken == null) {
      throw const AuthException(
          'Could not find ID Token from generated credential.',
      );
    }

    final res = await client.auth.signInWithIdToken(
      provider: OAuthProvider.apple,
      idToken: idToken,
      nonce: rawNonce,
    );
    return Credentials(id: res.user!.id, token: res.session?.accessToken ?? '');

  }

  @override
  Future<Credentials> signinWithFacebook() async {
    final loginResult = await FacebookAuth.instance.login(
      permissions: ['email', 'public_profile'],
    );

    if (loginResult.status == LoginStatus.cancelled) throw const UserCancelledSignInException();
    if (loginResult.status != LoginStatus.success) {
      throw ApiError(
        code: 401,
        message: loginResult.message ?? 'Facebook login failed',
      );
    }

    final accessToken = loginResult.accessToken?.tokenString;
    if (accessToken == null) {
      throw ApiError(code: 401, message: 'No Facebook access token');
    }

    final res = await client.auth.signInWithIdToken(
      provider: OAuthProvider.facebook,
      idToken: accessToken,
    );

    return Credentials(id: res.user!.id, token: res.session?.accessToken ?? '');
  }

  @override
  Future<Credentials> signinWithGoogle() async {
    final googleSignIn = GoogleSignIn.instance;
    // Web: clientId = Web Client ID (no serverClientId needed)
    // Native iOS: clientId = iOS Client ID, serverClientId = Web Client ID
    // Native Android: serverClientId = Web Client ID (clientId is ignored)
    if (kIsWeb) {
      await googleSignIn.initialize(
        clientId: kGoogleWebClientId.isEmpty ? null : kGoogleWebClientId,
      );
    } else {
      await googleSignIn.initialize(
        clientId: kGoogleIosClientId.isEmpty ? null : kGoogleIosClientId,
        serverClientId: kGoogleWebClientId.isEmpty ? null : kGoogleWebClientId,
      );
    }
    late final GoogleSignInAccount googleUser;
    try {
      googleUser = await googleSignIn.authenticate();
    } on GoogleSignInException catch (e) {
      if (e.code == GoogleSignInExceptionCode.canceled) throw const UserCancelledSignInException();
      rethrow;
    } on PlatformException catch (e) {
      if (e.code == 'sign_in_canceled' || e.code == 'sign_in_cancelled') {
        throw const UserCancelledSignInException();
      }
      rethrow;
    }
    final googleAuth = googleUser.authentication;
    final idToken = googleAuth.idToken;

    if (idToken == null) {
      throw 'No ID Token found.';
    }

    final res = await client.auth.signInWithIdToken(
      provider: OAuthProvider.google,
      idToken: idToken,
    );
    return Credentials(id: res.user!.id, token: res.session?.accessToken ?? '');
    
  }

  @override
  Future<Credentials> signinWithGooglePlay() {
    // Google Play Games sign-in is not supported for Supabase backend.
    // For Google account sign-in, use signinWithGoogle() instead.
    throw UnsupportedError(
      'Google Play Games sign-in is not available for Supabase. Use signinWithGoogle() instead.',
    );
  }

  @override
  Future<void> signout() {
    return client.auth.signOut();
  }

  
  @override
  Future<Credentials> signupFromAnonymousWithApple() async {
    final rawNonce = client.auth.generateRawNonce();
    final hashedNonce = sha256.convert(utf8.encode(rawNonce)).toString();

    late final AuthorizationCredentialAppleID credential;
    try {
      credential = await SignInWithApple.getAppleIDCredential(
        scopes: [AppleIDAuthorizationScopes.email],
        nonce: hashedNonce,
      );
    } on SignInWithAppleAuthorizationException catch (e) {
      if (e.code == AuthorizationErrorCode.canceled) throw const UserCancelledSignInException();
      rethrow;
    }
    final idToken = credential.identityToken;
    if (idToken == null) {
      throw const AuthException('Could not find ID Token from generated credential.');
    }

    try {
      final response = await client.auth.linkIdentityWithIdToken(
        provider: OAuthProvider.apple,
        idToken: idToken,
        nonce: rawNonce,
      );
      return Credentials(id: response.user!.id, token: response.session?.accessToken ?? '');
    } on AuthException catch (e) {
      if (e.code == 'identity_already_exists') {
        await _deleteCurrentAnonymousUser();
        final res = await client.auth.signInWithIdToken(
          provider: OAuthProvider.apple,
          idToken: idToken,
          nonce: rawNonce,
        );
        return Credentials(id: res.user!.id, token: res.session?.accessToken ?? '');
      }
      rethrow;
    }
  }

  @override
  Future<Credentials> signupFromAnonymousWithGoogle() async {
    final scopes = ['email'];
    final googleSignIn = GoogleSignIn.instance;

    if (kIsWeb) {
      await googleSignIn.initialize(
        clientId: kGoogleWebClientId.isEmpty ? null : kGoogleWebClientId,
      );
    } else {
      await googleSignIn.initialize(
        clientId: kGoogleIosClientId.isEmpty ? null : kGoogleIosClientId,
        serverClientId: kGoogleWebClientId.isEmpty ? null : kGoogleWebClientId,
      );
    }
    late final GoogleSignInAccount googleUser;
    try {
      googleUser = await googleSignIn.authenticate(scopeHint: scopes);
    } on GoogleSignInException catch (e) {
      if (e.code == GoogleSignInExceptionCode.canceled) throw const UserCancelledSignInException();
      rethrow;
    } on PlatformException catch (e) {
      if (e.code == 'sign_in_canceled' || e.code == 'sign_in_cancelled') {
        throw const UserCancelledSignInException();
      }
      rethrow;
    }
    final googleAuth = googleUser.authentication;
    final idToken = googleAuth.idToken;
    if (idToken == null) {
      throw 'No ID Token found.';
    }

    try {
      final response = await client.auth.linkIdentityWithIdToken(
        provider: OAuthProvider.google,
        idToken: idToken,
      );
      return Credentials(id: response.user!.id, token: response.session?.accessToken ?? '');
    } on AuthException catch (e) {
      if (e.code == 'identity_already_exists') {
        // Google account already exists — delete orphaned anonymous user via Edge
        // Function (uses service role key, cascades to public.users + devices),
        // then sign in with the existing account.
        await _deleteCurrentAnonymousUser();
        final res = await client.auth.signInWithIdToken(
          provider: OAuthProvider.google,
          idToken: idToken,
        );
        return Credentials(id: res.user!.id, token: res.session?.accessToken ?? '');
      }
      rethrow;
    }
  }

  @override
  Future<Credentials> signupFromAnonymousWithFacebook() async {
    final loginResult = await FacebookAuth.instance.login(
      permissions: ['email', 'public_profile'],
    );
    if (loginResult.status == LoginStatus.cancelled) throw const UserCancelledSignInException();
    if (loginResult.status != LoginStatus.success) {
      throw ApiError(code: 401, message: loginResult.message ?? 'Facebook login failed');
    }
    final accessToken = loginResult.accessToken?.tokenString;
    if (accessToken == null) {
      throw ApiError(code: 401, message: 'No Facebook access token');
    }
    try {
      final response = await client.auth.linkIdentityWithIdToken(
        provider: OAuthProvider.facebook,
        idToken: accessToken,
      );
      return Credentials(id: response.user!.id, token: response.session?.accessToken ?? '');
    } on AuthException catch (e) {
      if (e.code == 'identity_already_exists') {
        await _deleteCurrentAnonymousUser();
        final res = await client.auth.signInWithIdToken(
          provider: OAuthProvider.facebook,
          idToken: accessToken,
        );
        return Credentials(id: res.user!.id, token: res.session?.accessToken ?? '');
      }
      rethrow;
    }
  }

  @override
  Future<String> signinWithPhone(String phoneNumber) async {
    try {
      _logger.d('Requesting OTP for phone number: $phoneNumber');
      final normalizedPhone = _normalizePhoneNumber(phoneNumber);

      await client.auth.signInWithOtp(
        phone: normalizedPhone,
      );

      return normalizedPhone;
    } catch (e) {
      _logger.e('Error requesting phone authentication: $e');
      throw ApiError(
        code: 0,
        message: 'Failed to send verification code: $e',
      );
    }
  }

  @override
  Future<String> updateAuthPhone(String phoneNumber) async {
    final user = client.auth.currentUser;
    if (user == null) {
      throw ApiError(
        code: 401,
        message: 'User not found',
      );
    }
    try {
      final normalizedPhone = _normalizePhoneNumber(phoneNumber);
      await client.auth.updateUser(UserAttributes(phone: normalizedPhone));
      return normalizedPhone;
    } on AuthException catch (e) {
      final code = e.code;
      _logger.e('Error updating phone number: $code');
      if (code == 'phone_exists') {
        throw PhoneAlreadyLinkedException();
      }
      throw ApiError(
        code: 401,
        message: 'Failed to update phone number: $e',
      );
    } catch (e) {
      _logger.e('Error updating phone number: $e');
      throw ApiError(
        code: 0,
        message: 'Failed to update phone number: $e',
      );
    }
  }

  @override
  Future<Credentials> confirmLinkPhoneAuth(
    String verificationId,
    String otp,
  ) async {
    try {
      _logger.d('confirmLinkPhoneAuth OTP for phone: $verificationId');
      final phoneNumber = verificationId;

      final res = await client.auth.verifyOTP(
        phone: phoneNumber,
        token: otp,
        type: OtpType.phoneChange,
      );

      if (res.user == null) {
        throw ApiError(
          code: 401,
          message: 'Verification failed. Invalid OTP code.',
        );
      }

      return Credentials(
        id: res.user!.id,
        token: res.session?.accessToken ?? '',
      );
    } catch (e) {
      _logger.e('Error verifying phone OTP: $e');
      throw ApiError(
        code: 401,
        message: 'Failed to verify OTP: $e',
      );
    }
  }

  @override
  Future<Credentials> verifyPhoneAuth(String verificationId, String otp) async {
    try {
      _logger.d('Verifying OTP for phone: $verificationId');
      final phoneNumber = verificationId;

      final res = await client.auth.verifyOTP(
        phone: phoneNumber,
        token: otp,
        type: OtpType.sms,
      );

      if (res.user == null) {
        throw ApiError(
          code: 401,
          message: 'Verification failed. Invalid OTP code.',
        );
      }

      return Credentials(
        id: res.user!.id,
        token: res.session?.accessToken ?? '',
      );
    } catch (e) {
      _logger.e('Error verifying phone OTP: $e');
      throw ApiError(
        code: 401,
        message: 'Failed to verify OTP: $e',
      );
    }
  }

  @override
  Future<String?> getCurrentUserEmail() async => client.auth.currentUser?.email;

  @override
  Future<String?> getCurrentUserDisplayName() async =>
      client.auth.currentUser?.userMetadata?['full_name'] as String?;

  String _normalizePhoneNumber(String phoneNumber) {
    String normalized = phoneNumber.replaceAll(RegExp(r'\D'), '');
    if (!normalized.startsWith('+')) {
      normalized = '+$normalized';
    }
    return normalized;
  }

  /// Deletes the current anonymous user via the `delete-user-account` Edge
  /// Function, which runs with the service role key and cascades to
  /// public.users, devices, etc. Called before falling back to sign-in when
  /// `identity_already_exists` — prevents orphaned anonymous accounts.
  Future<void> _deleteCurrentAnonymousUser() async {
    if (client.auth.currentUser?.isAnonymous != true) return;
    try {
      await client.functions.invoke('delete-user-account');
    } catch (_) {
      // Deletion failed (e.g. function not deployed yet) — proceed anyway.
      // Orphaned accounts can be cleaned via Supabase Dashboard →
      // Auth → Configuration → "Clean up anonymous users".
    }
  }
}
