// 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 'dart:async';

import 'package:rxdart/rxdart.dart';
import 'package:wyatt_architecture/wyatt_architecture.dart';
import 'package:wyatt_authentication_bloc/src/core/constants/form_field.dart';
import 'package:wyatt_authentication_bloc/src/core/constants/form_name.dart';
import 'package:wyatt_authentication_bloc/src/data/models/account_wrapper_model.dart';
import 'package:wyatt_authentication_bloc/src/domain/data_sources/local/authentication_cache_data_source.dart';
import 'package:wyatt_authentication_bloc/src/domain/data_sources/remote/authentication_remote_data_source.dart';
import 'package:wyatt_authentication_bloc/src/domain/entities/account.dart';
import 'package:wyatt_authentication_bloc/src/domain/entities/account_wrapper.dart';
import 'package:wyatt_authentication_bloc/src/domain/repositories/authentication_repository.dart';
import 'package:wyatt_form_bloc/wyatt_form_bloc.dart';
import 'package:wyatt_type_utils/wyatt_type_utils.dart';

typedef OnSignUpSuccess<T> = FutureOrResult<T?> Function(
  AuthenticationRepository<T> repo,
  Account? account,
  WyattForm form,
);

typedef OnAuthChange<T> = FutureOrResult<T?> Function(
  AuthenticationRepository<T> repo,
  Account? account,
);

class AuthenticationRepositoryImpl<T extends Object>
    extends AuthenticationRepository<T> {
  AuthenticationRepositoryImpl({
    required AuthenticationCacheDataSource<T> authenticationCacheDataSource,
    required AuthenticationRemoteDataSource authenticationRemoteDataSource,
    FormRepository? formRepository,
    // ignore: strict_raw_type
    List<FormInput>? extraSignUpInputs,
    FormInputValidator<String?, ValidationError>? customEmailValidator,
    FormInputValidator<String?, ValidationError>? customPasswordValidator,
    OnSignUpSuccess<T>? onSignUpSuccess,
    OnAuthChange<T>? onAuthChange,
  })  : _authenticationLocalDataSource = authenticationCacheDataSource,
        _authenticationRemoteDataSource = authenticationRemoteDataSource,
        _onSignUpSuccess = onSignUpSuccess,
        _onAccountChanges = onAuthChange {
    _formRepository = formRepository ?? FormRepositoryImpl();
    if (formRepository != null) {
      return;
    }
    _formRepository
      ..registerForm(
        WyattFormImpl(
          [
            FormInput(
              AuthFormField.email,
              customEmailValidator ?? const Email.pure(),
            ),
            FormInput(
              AuthFormField.password,
              customPasswordValidator ?? const Password.pure(),
            )
          ],
          name: AuthFormName.signInForm,
        ),
      )
      ..registerForm(
        WyattFormImpl(
          [
            FormInput(
              AuthFormField.email,
              customEmailValidator ?? const Email.pure(),
            ),
            FormInput(
              AuthFormField.password,
              customPasswordValidator ?? const Password.pure(),
            ),
            ...extraSignUpInputs ?? []
          ],
          name: AuthFormName.signUpForm,
        ),
      );
  }
  final AuthenticationCacheDataSource<T> _authenticationLocalDataSource;
  final AuthenticationRemoteDataSource _authenticationRemoteDataSource;

  late FormRepository _formRepository;

  final OnSignUpSuccess<T>? _onSignUpSuccess;

  final OnAuthChange<T>? _onAccountChanges;
  final StreamController<FutureOrResult<AccountWrapper<T>>> _signUpStream =
      StreamController();

  bool _pause = false;

  @override
  FormRepository get formRepository => _formRepository;

  @override
  FutureOrResult<Account> signInWithEmailAndPassword({
    required String email,
    required String password,
  }) =>
      Result.tryCatchAsync<Account, AppException, AppException>(
        () async {
          final account =
              await _authenticationRemoteDataSource.signInWithEmailAndPassword(
            email: email,
            password: password,
          );
          await _authenticationLocalDataSource.storeAccount(account);
          return account;
        },
        (error) => error,
      );

  @override
  FutureOrResult<void> signOut() =>
      Result.tryCatchAsync<void, AppException, AppException>(
        () async {
          await _authenticationRemoteDataSource.signOut();
          await _authenticationLocalDataSource.destroy();
        },
        (error) => error,
      );

  @override
  FutureOrResult<Account> signUp({
    required String email,
    required String password,
  }) =>
      Result.tryCatchAsync<Account, AppException, AppException>(
        () async {
          _pause = true;
          final account = await _authenticationRemoteDataSource.signUp(
            email: email,
            password: password,
          );
          await _authenticationLocalDataSource.storeAccount(account);
          if (_onSignUpSuccess.isNotNull) {
            final dataResult = await _onSignUpSuccess!.call(
              this,
              account,
              _formRepository.accessForm(AuthFormName.signUpForm).clone(),
            );
            await dataResult.foldAsync(
              (data) async {
                await _authenticationLocalDataSource.storeData(data);
                _signUpStream
                    .add(Future.value(Ok(AccountWrapperModel(account, data))));
              },
              (error) async => error,
            );
          }
          _pause = false;
          return account;
        },
        (error) {
          _pause = false;
          return error;
        },
      );

  @override
  FutureOrResult<void> destroyCache() =>
      Result.tryCatchAsync<void, AppException, AppException>(
        _authenticationLocalDataSource.destroy,
        (error) => error,
      );

  @override
  FutureOrResult<AccountWrapper<T>> getCache() =>
      Result.tryCatchAsync<AccountWrapper<T>, AppException, AppException>(
        _authenticationLocalDataSource.load,
        (error) => error,
      );

  @override
  FutureOrResult<Account> getAccount() =>
      Result.tryCatchAsync<Account, AppException, AppException>(
        _authenticationLocalDataSource.loadAccount,
        (error) => error,
      );

  @override
  FutureOrResult<void> setAccount(
    Account account,
  ) =>
      Result.tryCatchAsync<void, AppException, AppException>(
        () async {
          await _authenticationLocalDataSource.storeAccount(account);
        },
        (error) => error,
      );

  @override
  FutureOrResult<T> getData() =>
      Result.tryCatchAsync<T, AppException, AppException>(
        _authenticationLocalDataSource.loadData,
        (error) => error,
      );

  @override
  FutureOrResult<void> setData(
    T? data,
  ) =>
      Result.tryCatchAsync<void, AppException, AppException>(
        () async {
          await _authenticationLocalDataSource.storeData(data);
        },
        (error) => error,
      );

  @override
  FutureOrResult<String> getIdentityToken() =>
      Result.tryCatchAsync<String, AppException, AppException>(
        _authenticationRemoteDataSource.getIdentityToken,
        (error) => error,
      );

  @override
  Stream<FutureOrResult<AccountWrapper<T>>> streamAccount() => MergeStream([
        _signUpStream.stream.asBroadcastStream(),
        _authenticationRemoteDataSource.streamAccount().map((account) async {
          if (_onAccountChanges.isNotNull && !_pause) {
            final dataResult = await _onAccountChanges!.call(this, account);
            return dataResult.map((data) {
              _authenticationLocalDataSource.storeData(data);
              return AccountWrapperModel(account, data);
            });
          }
          return Ok<AccountWrapperModel<T>, AppException>(
            AccountWrapperModel<T>(account, null),
          );
        })
      ]);

  @override
  FutureOrResult<void> confirmPasswordReset({
    required String code,
    required String newPassword,
  }) =>
      Result.tryCatchAsync<void, AppException, AppException>(
        () async {
          await _authenticationRemoteDataSource.confirmPasswordReset(
            code: code,
            newPassword: newPassword,
          );
        },
        (error) => error,
      );

  @override
  FutureOrResult<void> sendEmailVerification() =>
      Result.tryCatchAsync<void, AppException, AppException>(
        () async {
          await _authenticationRemoteDataSource.sendEmailVerification();
        },
        (error) => error,
      );

  @override
  FutureOrResult<void> sendPasswordResetEmail({required String email}) =>
      Result.tryCatchAsync<void, AppException, AppException>(
        () async {
          await _authenticationRemoteDataSource.sendPasswordResetEmail(
            email: email,
          );
        },
        (error) => error,
      );

  @override
  FutureOrResult<Account> signInAnonymously() =>
      Result.tryCatchAsync<Account, AppException, AppException>(
        () async {
          final account =
              await _authenticationRemoteDataSource.signInAnonymously();
          return account;
        },
        (error) => error,
      );

  @override
  FutureOrResult<Account> signInWithGoogle() =>
      Result.tryCatchAsync<Account, AppException, AppException>(
        () async {
          final account =
              await _authenticationRemoteDataSource.signInWithGoogle();
          return account;
        },
        (error) => error,
      );

  @override
  FutureOrResult<bool> verifyPasswordResetCode({required String code}) =>
      Result.tryCatchAsync<bool, AppException, AppException>(
        () async {
          final response = await _authenticationRemoteDataSource
              .verifyPasswordResetCode(code: code);
          return response;
        },
        (error) => error,
      );

  @override
  FutureOrResult<void> refresh() =>
      Result.tryCatchAsync<void, AppException, AppException>(
        () async {
          await _authenticationRemoteDataSource.refresh();
        },
        (error) => error,
      );

  @override
  FutureOrResult<Account> reauthenticateWithCredential() =>
      Result.tryCatchAsync<Account, AppException, AppException>(
        () async {
          final account = await _authenticationRemoteDataSource
              .reauthenticateWithCredential();
          return account;
        },
        (error) => error,
      );

  @override
  FutureOrResult<Account> updateEmail({required String email}) =>
      Result.tryCatchAsync<Account, AppException, AppException>(
        () async {
          final account =
              await _authenticationRemoteDataSource.updateEmail(email: email);
          return account;
        },
        (error) => error,
      );

  @override
  FutureOrResult<Account> updatePassword({required String password}) =>
      Result.tryCatchAsync<Account, AppException, AppException>(
        () async {
          final account = await _authenticationRemoteDataSource.updatePassword(
            password: password,
          );
          return account;
        },
        (error) => error,
      );
}
