// 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:equatable/equatable.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:wyatt_architecture/wyatt_architecture.dart';
import 'package:wyatt_crud_bloc/src/core/enums/where_query_type.dart';
import 'package:wyatt_crud_bloc/src/domain/entities/object_model.dart';
import 'package:wyatt_crud_bloc/src/domain/entities/query.dart';
import 'package:wyatt_crud_bloc/src/domain/usecases/create.dart';
import 'package:wyatt_crud_bloc/src/domain/usecases/delete.dart';
import 'package:wyatt_crud_bloc/src/domain/usecases/delete_all.dart';
import 'package:wyatt_crud_bloc/src/domain/usecases/get.dart';
import 'package:wyatt_crud_bloc/src/domain/usecases/get_all.dart';
import 'package:wyatt_crud_bloc/src/domain/usecases/params/update_parameters.dart';
import 'package:wyatt_crud_bloc/src/domain/usecases/query.dart';
import 'package:wyatt_crud_bloc/src/domain/usecases/update.dart';
import 'package:wyatt_crud_bloc/src/domain/usecases/update_all.dart';

part 'crud_state.dart';

abstract class CrudCubit<Model extends ObjectModel> extends Cubit<CrudState> {

  CrudCubit() : super(CrudInitial());
  Create<Model>? get crudCreate;
  DeleteAll<Model>? get crudDeleteAll;
  Delete<Model>? get crudDelete;
  GetAll<Model>? get crudGetAll;
  Get<Model>? get crudGet;
  Query<Model>? get crudQuery;
  UpdateAll<Model>? get crudUpdateAll;
  Update<Model>? get crudUpdate;

  FutureOr<void> create(Model model) async {
    if (crudCreate != null) {
      final stateCopy = state;
      emit(CrudLoading());
      final result = await crudCreate!.call(model);
      emit(
        result.fold(
          (_) {
            if (stateCopy is CrudLoaded<Model?>) {
              return stateCopy;
            }
            if (stateCopy is CrudListLoaded<Model?>) {
              if (stateCopy.data.isEmpty) {
                return CrudListLoaded<Model?>([model]);
              }
              final List<Model?> lst = stateCopy.data.toList()..add(model);
              return CrudListLoaded<Model?>(lst);
            }
            return const CrudOkReturn();
          },
          (error) => CrudError(error.toString()),
        ),
      );
    }
  }

  FutureOr<void> delete(String id) async {
    if (crudDelete != null) {
      final stateCopy = state;
      emit(CrudLoading());
      final result = await crudDelete!.call(id);
      emit(
        result.fold(
          (_) {
            if (stateCopy is CrudLoaded<Model?>) {
              return stateCopy;
            }
            if (stateCopy is CrudListLoaded<Model?>) {
              return CrudListLoaded<Model?>(
                stateCopy.data.where((element) => element?.id != id).toList(),
              );
            }
            return const CrudOkReturn();
          },
          (error) => CrudError(error.toString()),
        ),
      );
    }
  }

  FutureOr<void> deleteAll() async {
    if (crudDeleteAll != null) {
      final stateCopy = state;
      emit(CrudLoading());
      final result = await crudDeleteAll!.call(null);
      emit(
        result.fold(
          (_) {
            if (stateCopy is CrudLoaded<Model?>) {
              return CrudLoaded<Model?>(null);
            }
            if (stateCopy is CrudListLoaded<Model?>) {
              return CrudListLoaded<Model?>(const []);
            }
            return const CrudOkReturn();
          },
          (error) => CrudError(error.toString()),
        ),
      );
    }
  }

  FutureOr<void> get(String id) async {
    if (crudGet != null) {
      emit(CrudLoading());
      final result = await crudGet!.call(id);
      emit(
        result.fold(
          CrudLoaded<Model?>.new,
          (error) => CrudError(error.toString()),
        ),
      );
    }
  }

  FutureOr<void> getAll() async {
    if (crudGetAll != null) {
      emit(CrudLoading());
      final result = await crudGetAll!.call(null);
      emit(
        result.fold(
          CrudListLoaded<Model?>.new,
          (error) => CrudError(error.toString()),
        ),
      );
    }
  }

  FutureOr<void> query(List<QueryInterface> conditions) async {
    if (crudQuery != null) {
      emit(CrudLoading());
      final result = await crudQuery!.call(conditions);
      emit(
        result.fold(
          CrudListLoaded<Model?>.new,
          (error) => CrudError(error.toString()),
        ),
      );
    }
  }

  FutureOr<void> update(UpdateParameters<Model> param) async {
    if (crudUpdate != null) {
      final stateCopy = state;
      emit(CrudLoading());
      final result = await crudUpdate!.call(param);
      emit(
        await result.foldAsync(
          (_) async {
            if (stateCopy is CrudLoaded<Model?>) {
              if (stateCopy.data?.id == param.id) {
                // Same object, need to update actual stateCopy
                if (crudGet == null) {
                  throw ClientException(
                    'Need to init Get usecase to use update.',
                  );
                }
                final newVersion = await crudGet!.call(param.id);
                if (newVersion.isOk) {
                  return CrudLoaded<Model?>(newVersion.ok);
                }
              }
              return stateCopy;
            }
            if (stateCopy is CrudListLoaded<Model?>) {
              final bool listContains =
                  stateCopy.data.any((element) => element?.id == param.id);
              if (listContains) {
                // Loaded objects contains the modified object.
                if (crudGet == null) {
                  throw ClientException(
                    'Need to init Get usecase to use update.',
                  );
                }
                final newVersion = await crudGet!.call(param.id);
                if (newVersion.isOk) {
                  final newList = stateCopy.data
                      .where(
                        (element) => element?.id != param.id,
                      )
                      .toList();
                  return CrudListLoaded<Model?>(newList + [newVersion.ok]);
                }
              }
              return stateCopy;
            }
            return const CrudOkReturn();
          },
          (error) async => CrudError(error.toString()),
        ),
      );
    }
  }

  FutureOr<void> updateAll(Map<String, Object?> param) async {
    if (crudUpdateAll != null) {
      final stateCopy = state;
      emit(CrudLoading());
      final result = await crudUpdateAll!.call(param);
      emit(
        await result.foldAsync(
          (_) async {
            if (stateCopy is CrudLoaded<Model?>) {
              // Same object, need to update actual stateCopy
              if (crudGet == null) {
                throw ClientException(
                  'Need to init Get usecase to use updateAll.',
                );
              }
              final actualId = stateCopy.data?.id;
              final newVersion = await crudGet!.call(actualId ?? '');
              if (newVersion.isOk) {
                return CrudLoaded<Model?>(newVersion.ok);
              }
              return stateCopy;
            }
            if (stateCopy is CrudListLoaded<Model?>) {
              if (crudQuery == null) {
                throw ClientException(
                  'Need to init Query usecase to use updateAll.',
                );
              }
              // Load all id to retrieve exactly same object
              // (not all because previous stateCopy can be a query result)
              final List<String?> ids = stateCopy.data
                  .map(
                    (e) => e?.id,
                  )
                  .toList();
              final result = await crudQuery!.call([
                WhereQuery(
                  WhereQueryType.whereIn,
                  'id',
                  ids,
                )
              ]);
              if (result.isOk) {
                return CrudListLoaded<Model?>(result.ok ?? []);
              }
              return stateCopy;
            }
            return const CrudOkReturn();
          },
          (error) async => CrudError(error.toString()),
        ),
      );
    }
  }
}
