// Copyright (C) 2023 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 'dart:async';

import 'package:equatable/equatable.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:wyatt_architecture/wyatt_architecture.dart';
import 'package:wyatt_authentication_bloc/src/core/enums/authentication_status.dart';
import 'package:wyatt_authentication_bloc/src/core/utils/custom_routine.dart';
import 'package:wyatt_authentication_bloc/src/domain/entities/entities.dart';
import 'package:wyatt_authentication_bloc/src/domain/repositories/authentication_repository.dart';
import 'package:wyatt_type_utils/wyatt_type_utils.dart';

part 'authentication_state.dart';

/// {@template authentication_cubit}
/// Abstract authentication cubit class needs to be implemented in application.
///
/// This cubit is in charge of managing the global authentication state of
/// the application.
///
/// Its here you can override every callbacks and add your custom logic.
/// {@endtemplate}
abstract class AuthenticationCubit<Data>
    extends Cubit<AuthenticationState<Data>> {
  /// {@macro authentication_cubit}
  AuthenticationCubit({
    required this.authenticationRepository,
  }) : super(const AuthenticationState.unknown()) {
    _init();
  }

  /// The authentication repository.
  final AuthenticationRepository<Data> authenticationRepository;

  /// The latest session.
  AuthenticationSession<Data>? _latestSession;

  /// Method that is called when the cubit is initialized.
  Future<void> _init() async {
    /// Setup listeners.
    _listenForAuthenticationChanges();

    /// Check if there is a cached account.
    await authenticationRepository.checkForCachedAccount();
  }

  void _listenForAuthenticationChanges() {
    authenticationRepository.sessionStream().asyncMap((session) async {
      final event = session.latestEvent;

      /// If the session is signed in from cache.
      if (event is SignedInFromCacheEvent) {
        /// Call the custom routine.
        final customRoutineResult = await onSignInFromCache(session);

        if (customRoutineResult.isOk) {
          final account = event.account;
          final sessionData = customRoutineResult.ok;

          final signedInSession = AuthenticationSession.fromEvent(
            SignedInFromCacheEvent(account: account),
            data: sessionData,
          );

          return signedInSession;
        }
      }
      return session;
    }).listen((session) async {
      /// Save the latest session.
      _latestSession = session;

      /// If there is an account: emit authenticated state.
      if (session.account != null) {
        emit(AuthenticationState<Data>.authenticated(session));
        return;
      }

      /// If there is no account: emit unauthenticated state.
      emit(AuthenticationState<Data>.unauthenticated());

      return;
    });
  }

  /// {@macro refresh}
  FutureOr<void> refresh() async => CustomRoutine<Account, Data?>(
        routine: authenticationRepository.refresh,
        attachedLogic: onRefresh,
        onError: addError,
        onSuccess: (result, data) => authenticationRepository.addSession(
          AuthenticationSession.fromEvent(
            RefreshedEvent(account: result),
            data: data,
          ),
        ),
      ).call();

  /// {@macro reauthenticate}
  FutureOr<void> reauthenticate() async => CustomRoutine<Account, Data?>(
        routine: authenticationRepository.reauthenticate,
        attachedLogic: onReauthenticate,
        onError: addError,
        onSuccess: (result, data) => authenticationRepository.addSession(
          AuthenticationSession.fromEvent(
            ReauthenticatedEvent(account: result),
            data: data,
          ),
        ),
      ).call();

  /// {@macro signout}
  FutureOr<void> signOut() async => CustomRoutine<void, void>(
        routine: authenticationRepository.signOut,
        attachedLogic: (routineResult) => onSignOut(),
        onError: addError,
        onSuccess: (result, data) => authenticationRepository.addSession(
          const AuthenticationSession(
            latestEvent: SignedOutEvent(),
          ),
        ),
      ).call();

  /// {@macro delete}
  FutureOr<void> delete() async => CustomRoutine<void, void>(
        routine: authenticationRepository.delete,
        attachedLogic: (routineResult) => onDelete(),
        onError: addError,
        onSuccess: (result, data) => authenticationRepository.addSession(
          const AuthenticationSession(
            latestEvent: DeletedEvent(),
          ),
        ),
      ).call();

  /// Returns latest session.
  ///
  /// Contains latest event and latest session data (account + extra data)
  AuthenticationSession<Data>? currentSession() => _latestSession;

  /// This callback is triggered when the user is automaticcaly logged in from
  /// the cache.
  ///
  /// For example: when the user is sign in from the Firebase cache.
  FutureOrResult<Data?> onSignInFromCache(AuthenticationSession<Data> session);

  /// This callback is triggered when the account is refreshed.
  ///
  /// For example: when the access token is refreshed.
  FutureOrResult<Data?> onRefresh(Result<Account, AppException> result);

  /// This callback is triggered when the account is re-authenticated
  ///
  /// For example: when the user is logged in and sign in
  /// from an another provider
  FutureOrResult<Data?> onReauthenticate(Result<Account, AppException> result);

  /// This callback is triggered when the user is logged out.
  ///
  /// For example: when the user clicks on the logout button.
  FutureOrResult<void> onSignOut();

  /// This callback is triggered when the current account is deleted from
  /// the remote.
  ///
  /// For example: when the user wants to delete his account from Firebase
  FutureOrResult<void> onDelete();
}
