// Copyright (C) 2022 WYATT GROUP
// Please see the AUTHORS file for details.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.

import 'package:wyatt_authentication_bloc/src/data/models/account_model_firebase.dart';
import 'package:wyatt_authentication_bloc/wyatt_authentication_bloc.dart';
import 'package:wyatt_type_utils/wyatt_type_utils.dart';

class AuthenticationFirebaseDataSourceImpl
    extends AuthenticationRemoteDataSource {
  AuthenticationFirebaseDataSourceImpl({
    FirebaseAuth? firebaseAuth,
    GoogleSignIn? googleSignIn,
  })  : _firebaseAuth = firebaseAuth ?? FirebaseAuth.instance,
        _googleSignIn = googleSignIn ?? GoogleSignIn();

  final FirebaseAuth _firebaseAuth;
  final GoogleSignIn _googleSignIn;
  UserCredential? _latestCreds;

  @override
  Future<Account> signInWithEmailAndPassword({
    required String email,
    required String password,
  }) async {
    try {
      final userCredential = await _firebaseAuth.signInWithEmailAndPassword(
        email: email,
        password: password,
      );
      _latestCreds = userCredential;
      final user = userCredential.user;
      return AccountModelFirebase.fromFirebaseUser(user);
    } on FirebaseAuthException catch (e) {
      throw SignInWithEmailAndPasswordFailureFirebase.fromCode(e.code);
    } catch (_) {
      throw SignInWithEmailAndPasswordFailureFirebase();
    }
  }

  @override

  /// {@macro signup}
  Future<Account> signUp({
    required String email,
    required String password,
  }) async {
    try {
      final userCredential = await _firebaseAuth.createUserWithEmailAndPassword(
        email: email,
        password: password,
      );
      _latestCreds = userCredential;
      final user = userCredential.user;
      return AccountModelFirebase.fromFirebaseUser(user);
    } on FirebaseAuthException catch (e) {
      throw SignUpWithEmailAndPasswordFailureFirebase.fromCode(e.code);
    } catch (_) {
      throw SignUpWithEmailAndPasswordFailureFirebase();
    }
  }

  @override
  Future<void> signOut() async {
    try {
      _latestCreds = null;
      await _firebaseAuth.signOut();
    } catch (_) {
      throw SignOutFailureFirebase();
    }
  }

  @override
  Future<String> getIdentityToken() async {
    try {
      final token = await _firebaseAuth.currentUser?.getIdToken();
      if (token.isNotNull) {
        return token!;
      } else {
        throw Exception(); // Get caught just after.
      }
    } on FirebaseAuthException catch (e) {
      throw GetIdTokenFailureFirebase.fromCode(e.code);
    } catch (_) {
      throw GetIdTokenFailureFirebase();
    }
  }

  @override
  Stream<Account?> streamAccount() =>
      _firebaseAuth.userChanges().map<Account?>((user) {
        try {
          return AccountModelFirebase.fromFirebaseUser(user);
        } on FirebaseAuthException {
          return null;
        }
      });

  @override
  Future<void> confirmPasswordReset({
    required String code,
    required String newPassword,
  }) async {
    try {
      await _firebaseAuth.confirmPasswordReset(
        code: code,
        newPassword: newPassword,
      );
    } on FirebaseAuthException catch (e) {
      throw ConfirmPasswordResetFailureFirebase.fromCode(e.code);
    } catch (_) {
      throw ConfirmPasswordResetFailureFirebase();
    }
  }

  @override
  Future<void> sendEmailVerification() async {
    try {
      await _firebaseAuth.currentUser!.sendEmailVerification();
    } on FirebaseAuthException catch (e) {
      throw SendEmailVerificationFailureFirebase.fromCode(e.code);
    } catch (_) {
      throw SendEmailVerificationFailureFirebase();
    }
  }

  @override
  Future<void> sendPasswordResetEmail({required String email}) async {
    try {
      await _firebaseAuth.sendPasswordResetEmail(email: email);
    } on FirebaseAuthException catch (e) {
      throw SendPasswordResetEmailFailureFirebase.fromCode(e.code);
    } catch (_) {
      throw SendPasswordResetEmailFailureFirebase();
    }
  }

  @override
  Future<Account> signInAnonymously() async {
    try {
      final userCredential = await _firebaseAuth.signInAnonymously();
      _latestCreds = userCredential;
      final user = userCredential.user;
      return AccountModelFirebase.fromFirebaseUser(user);
    } on FirebaseAuthException catch (e) {
      throw SignInAnonymouslyFailureFirebase.fromCode(e.code);
    } catch (_) {
      throw SignInAnonymouslyFailureFirebase();
    }
  }

  @override
  Future<Account> signInWithGoogle() async {
    try {
      // Trigger the authentication flow
      final GoogleSignInAccount? googleUser = await _googleSignIn.signIn();

      // Obtain the auth details from the request
      final GoogleSignInAuthentication? googleAuth =
          await googleUser?.authentication;

      // Create a new credential
      final credential = GoogleAuthProvider.credential(
        accessToken: googleAuth?.accessToken,
        idToken: googleAuth?.idToken,
      );

      final userCredential =
          await _firebaseAuth.signInWithCredential(credential);

      _latestCreds = userCredential;
      final user = userCredential.user;
      return AccountModelFirebase.fromFirebaseUser(user);
    } on FirebaseAuthException catch (e) {
      throw SignInWithGoogleFailureFirebase.fromCode(e.code);
    } catch (_) {
      throw SignInWithGoogleFailureFirebase();
    }
  }

  @override
  Future<bool> verifyPasswordResetCode({required String code}) async {
    try {
      final email = await _firebaseAuth.verifyPasswordResetCode(code);
      return email.isNotNullOrEmpty;
    } on FirebaseAuthException catch (e) {
      throw VerifyPasswordResetCodeFailureFirebase.fromCode(e.code);
    } catch (_) {
      throw VerifyPasswordResetCodeFailureFirebase();
    }
  }

  @override
  Future<void> refresh() async {
    try {
      await _firebaseAuth.currentUser!.reload();
    } on FirebaseAuthException catch (e) {
      throw RefreshFailureFirebase.fromCode(e.code);
    } catch (_) {
      throw RefreshFailureFirebase();
    }
  }

  @override
  Future<Account> reauthenticateWithCredential() async {
    try {
      if (_latestCreds?.credential != null) {
        await _firebaseAuth.currentUser
            ?.reauthenticateWithCredential(_latestCreds!.credential!);
      } else {
        throw Exception(); // Get caught just after.
      }
      final user = _firebaseAuth.currentUser;
      return AccountModelFirebase.fromFirebaseUser(user);
    } on FirebaseAuthException catch (e) {
      throw ReauthenticateFailureFirebase.fromCode(e.code);
    } catch (_) {
      throw ReauthenticateFailureFirebase();
    }
  }

  @override
  Future<Account> updateEmail({required String email}) async {
    try {
      await _firebaseAuth.currentUser!.updateEmail(email);
      final user = _firebaseAuth.currentUser;
      return AccountModelFirebase.fromFirebaseUser(user);
    } on FirebaseAuthException catch (e) {
      throw UpdateEmailFailureFirebase.fromCode(e.code);
    } catch (_) {
      throw UpdateEmailFailureFirebase();
    }
  }

  @override
  Future<Account> updatePassword({required String password}) async {
    try {
      await _firebaseAuth.currentUser!.updatePassword(password);
      final user = _firebaseAuth.currentUser;
      return AccountModelFirebase.fromFirebaseUser(user);
    } on FirebaseAuthException catch (e) {
      throw UpdatePasswordFailureFirebase.fromCode(e.code);
    } catch (_) {
      throw UpdatePasswordFailureFirebase();
    }
  }
}
