import 'dart:async';

import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:hl_lieferservice/feature/delivery/bloc/tour_event.dart';
import 'package:hl_lieferservice/feature/delivery/bloc/tour_state.dart';
import 'package:hl_lieferservice/feature/delivery/overview/model/sorting_information.dart';
import 'package:hl_lieferservice/feature/delivery/overview/repository/tour_repository.dart';
import 'package:hl_lieferservice/feature/delivery/overview/service/distance_service.dart';
import 'package:hl_lieferservice/feature/delivery/overview/service/reorder_service.dart';
import 'package:hl_lieferservice/model/tour.dart';
import 'package:hl_lieferservice/widget/operations/bloc/operation_bloc.dart';
import 'package:hl_lieferservice/widget/operations/bloc/operation_event.dart';
import 'package:rxdart/rxdart.dart';

class TourBloc extends Bloc<TourEvent, TourState> {
  OperationBloc opBloc;
  TourRepository tourRepository;
  StreamSubscription? _combinedSubscription;

  TourBloc({required this.opBloc, required this.tourRepository})
    : super(TourInitial()) {
    _combinedSubscription = CombineLatestStream.combine2(
      tourRepository.tour,
      tourRepository.paymentOptions,
      (tour, payments) => {'tour': tour, 'payments': payments},
    ).listen((combined) {
      final tour = combined['tour'] as Tour?;
      final payments = combined['payments'] as List<Payment>;

      if (tour == null) {
        return;
      }

      add(TourUpdated(tour: tour, payments: payments));
    });

    on<LoadTour>(_load);
    on<AssignCarEvent>(_assignCar);
    on<IncrementArticleScanAmount>(_increment);
    on<ScanArticleEvent>(_scan);
    on<HoldDeliveryEvent>(_holdDelivery);
    on<CancelDeliveryEvent>(_cancelDelivery);
    on<ReactivateDeliveryEvent>(_reactivateDelivery);
    on<UnscanArticleEvent>(_unscan);
    on<ResetScanAmountEvent>(_resetAmount);
    on<AddDiscountEvent>(_addDiscount);
    on<RemoveDiscountEvent>(_removeDiscount);
    on<UpdateDiscountEvent>(_updateDiscount);
    on<UpdateDeliveryOptionEvent>(_updateDeliveryOptions);
    on<UpdateSelectedPaymentMethodEvent>(_updatePayment);
    on<FinishDeliveryEvent>(_finishDelivery);
    on<TourUpdated>(_updated);
    on<RequestDeliveryDistanceEvent>(_calculateDistances);
    on<RequestSortingInformationEvent>(_requestSortingInformation);
    on<ReorderDeliveryEvent>(_reorderDelivery);
    on<CarsLoadedEvent>(_carsLoaded);
  }

  @override
  Future<void> close() {
    _combinedSubscription?.cancel();

    return super.close();
  }

  void _carsLoaded(CarsLoadedEvent event, Emitter<TourState> emit) {
    final currentState = state;
    if (currentState is TourLoaded) {
      currentState.tour.driver.cars = event.cars;
      emit(currentState.copyWith());
    }
  }

  void _reorderDelivery(
    ReorderDeliveryEvent event,
    Emitter<TourState> emit,
  ) async {
    final currentState = state;
    if (currentState is TourLoaded) {
      int newPosition = event.newPosition == currentState.sortingInformation.sorting.length ? event.newPosition - 1 : event.newPosition;
      SortingInformation informationOld = currentState.sortingInformation.sorting
          .firstWhere((info) => info.position == event.oldPosition);

      SortingInformation information = currentState
          .sortingInformation
          .sorting
          .firstWhere((info) => info.position == newPosition);

      information.position = event.oldPosition;
      informationOld.position = newPosition;

      await ReorderService().saveSortingInformation(
        currentState.sortingInformation,
      );

      emit(
        currentState.copyWith(
          sortingInformation: currentState.sortingInformation.copyWith(),
        ),
      );
    }
  }

  void _calculateDistances(
    RequestDeliveryDistanceEvent event,
    Emitter<TourState> emit,
  ) async {
    Map<String, double> distances = {};
    opBloc.add(LoadOperation());
    emit(TourRequestingDistances(tour: event.tour, payments: event.payments));

    try {
      for (final delivery in event.tour.deliveries) {
        distances[delivery.id] = await DistanceService.getDistanceByRoad(
          delivery.customer.address.toString(),
        );
      }

      opBloc.add(FinishOperation());
    } catch (e) {
      debugPrint("Fehler beim Berechnen der Distanzen: $e");
      opBloc.add(FailOperation(message: "Fehler beim Berechnen der Distanzen"));
      return;
    } finally {
      // Independent of error state fetch the sorting information
      add(
        RequestSortingInformationEvent(
          tour: event.tour,
          payments: event.payments,
          distances: distances,
        ),
      );
    }
  }

  void _requestSortingInformation(
    RequestSortingInformationEvent event,
    Emitter<TourState> emit,
  ) async {
    try {
      ReorderService service = ReorderService();
      SortingInformationContainer container = SortingInformationContainer(
        sorting: [],
      );

      // Create empty default value if it does not exist yet
      if (!service.orderInformationExist()) {
        await service.initializeTour(event.tour);
      }

      // Populate the container with information. If the file did not exist then it
      // now contains the standard values.
      container = await service.loadSortingInformation();

      bool inconsistent = false;
      for (final delivery in event.tour.deliveries) {
        int info = container.sorting.indexWhere(
          (info) => info.deliveryId == delivery.id,
        );
        int max = container.sorting.fold(0, (acc, element) {
          if (element.position > acc) {
            return element.position;
          }
          return acc;
        });

        // not found, so add it to the list
        if (info == -1) {
          inconsistent = true;
          container.sorting.add(
            SortingInformation(
              deliveryId: delivery.id,
              position: container.sorting.isEmpty ? 0 : max + 1,
            ),
          );
        }
      }

      // if new deliveries were added then save the information with the newly
      // populated container
      if (inconsistent) {
        await service.saveSortingInformation(container);
      }

      emit(
        TourLoaded(
          tour: event.tour,
          paymentOptions: event.payments,
          sortingInformation: container,
          distances: event.distances,
        ),
      );
    } catch (e, st) {
      debugPrint("Fehler beim Lesen der Datei: $e");
      debugPrint("$st");

      opBloc.add(
        FailOperation(
          message:
              "Fehler beim Laden der Sortierung. Es wird ohne Sortierung fortgefahren",
        ),
      );

      SortingInformationContainer container = SortingInformationContainer(
        sorting: [],
      );
      for (final (index, delivery) in event.tour.deliveries.indexed) {
        container.sorting.add(
          SortingInformation(deliveryId: delivery.id, position: index),
        );
      }

      emit(
        TourLoaded(
          tour: event.tour,
          paymentOptions: event.payments,
          sortingInformation: container,
          distances: event.distances,
        ),
      );
    }
  }

  void _updated(TourUpdated event, Emitter<TourState> emit) {
    final currentState = state;
    final tour = event.tour.copyWith();
    final payments =
        event.payments.map((payment) => payment.copyWith()).toList();

    if (currentState is TourLoaded) {
      emit(
        TourLoaded(
          tour: tour,
          paymentOptions: payments,
          distances: Map<String, double>.from(currentState.distances ?? {}),
          sortingInformation: currentState.sortingInformation,
        ),
      );
    }

    // Download distances if tour has previously fetched by API
    if (currentState is TourLoading) {
      add(
        RequestDeliveryDistanceEvent(tour: tour.copyWith(), payments: payments),
      );
    }
  }

  void _reactivateDelivery(
    ReactivateDeliveryEvent event,
    Emitter<TourState> emit,
  ) async {
    final currentState = state;
    if (currentState is TourLoaded) {
      opBloc.add(LoadOperation());
      try {
        await tourRepository.reactivateDelivery(event.deliveryId);

        opBloc.add(FinishOperation());
      } catch (e, st) {
        debugPrint("$e");
        debugPrint("$st");
        opBloc.add(
          FailOperation(message: "Fehler beim Zurückstellen der Lieferung"),
        );
      }
    }
  }

  void _holdDelivery(HoldDeliveryEvent event, Emitter<TourState> emit) async {
    final currentState = state;
    if (currentState is TourLoaded) {
      opBloc.add(LoadOperation());
      try {
        await tourRepository.holdDelivery(event.deliveryId);

        opBloc.add(FinishOperation());
      } catch (e, st) {
        debugPrint("$e");
        debugPrint("$st");
        opBloc.add(
          FailOperation(message: "Fehler beim Zurückstellen der Lieferung"),
        );
      }
    }
  }

  void _cancelDelivery(
    CancelDeliveryEvent event,
    Emitter<TourState> emit,
  ) async {
    final currentState = state;
    if (currentState is TourLoaded) {
      opBloc.add(LoadOperation());
      try {
        await tourRepository.cancelDelivery(event.deliveryId);

        opBloc.add(FinishOperation());
      } catch (e, st) {
        debugPrint("$e");
        debugPrint("$st");
        opBloc.add(
          FailOperation(message: "Fehler beim Zurückstellen der Lieferung"),
        );
      }
    }
  }

  void _scan(ScanArticleEvent event, Emitter<TourState> emit) async {
    final currentState = state;
    opBloc.add(LoadOperation());

    if (currentState is TourLoaded) {
      try {
        switch (await tourRepository.scanArticle(
          event.deliveryId,
          event.carId,
          event.articleNumber,
        )) {
          case ScanResult.scanned:
            opBloc.add(FinishOperation(message: 'Artikel gescannt'));
            break;
          case ScanResult.alreadyScanned:
            opBloc.add(
              FailOperation(message: 'Artikel wurde bereits gescannt'),
            );
            break;
          case ScanResult.notFound:
            opBloc.add(
              FailOperation(
                message: 'Artikel ist für keine Lieferung vorgesehen',
              ),
            );
            break;
        }
      } on TourNotFoundException {
        opBloc.add(FailOperation(message: "Fehler beim Scannen des Artikels"));
      }
    }
  }

  Future<void> _increment(
    IncrementArticleScanAmount event,
    Emitter<TourState> emit,
  ) async {
    final currentState = state;

    if (currentState is TourLoaded) {
      opBloc.add(LoadOperation());
      try {
        await tourRepository.scanArticle(
          event.deliveryId,
          event.carId,
          event.internalArticleId,
        );
        opBloc.add(FinishOperation());
      } catch (e, st) {
        debugPrint(st.toString());
        opBloc.add(FailOperation(message: "Fehler beim Scannen des Artikels"));
      }
    }
  }

  Future<void> _assignCar(AssignCarEvent event, Emitter<TourState> emit) async {
    final currentState = state;
    if (currentState is TourLoaded) {
      opBloc.add(LoadOperation());
      try {
        await tourRepository.assignCar(event.deliveryId, event.carId);
        opBloc.add(FinishOperation());
      } catch (e, st) {
        debugPrint(st.toString());
        opBloc.add(
          FailOperation(message: "Fehler beim Zuweisen des Fahrzeugs"),
        );
      }
    }
  }

  Future<void> _load(LoadTour event, Emitter<TourState> emit) async {
    opBloc.add(LoadOperation());
    try {
      emit(TourLoading());
      await tourRepository.loadTourOfToday(event.teamId);
      await tourRepository.loadPaymentOptions();

      opBloc.add(FinishOperation());
    } catch (e) {
      opBloc.add(
        FailOperation(message: "Fehler beim Laden der heutigen Fahrten"),
      );
    }
  }

  void _finishDelivery(
    FinishDeliveryEvent event,
    Emitter<TourState> emit,
  ) async {
    final currentState = state;
    opBloc.add(LoadOperation());

    if (currentState is TourLoaded) {
      try {
        await tourRepository.uploadDriverSignature(
          event.deliveryId,
          event.driverSignature,
        );
        await tourRepository.uploadCustomerSignature(
          event.deliveryId,
          event.customerSignature,
        );

        await tourRepository.finishDelivery(event.deliveryId);

        opBloc.add(FinishOperation());
      } catch (e, st) {
        opBloc.add(FailOperation(message: "Failed to update delivery"));
        debugPrint(st.toString());
      }
    }
  }

  void _updatePayment(
    UpdateSelectedPaymentMethodEvent event,
    Emitter<TourState> emit,
  ) async {
    try {
      opBloc.add(LoadOperation());
      await tourRepository.updatePayment(event.deliveryId, event.payment);
      opBloc.add(FinishOperation());
    } catch (e, st) {
      debugPrint(st.toString());
      opBloc.add(
        FailOperation(message: "Fehler beim Aktualisieren des Betrags"),
      );
    }
  }

  void _updateDeliveryOptions(
    UpdateDeliveryOptionEvent event,
    Emitter<TourState> emit,
  ) async {
    try {
      opBloc.add(LoadOperation());
      await tourRepository.updateOption(
        event.deliveryId,
        event.key,
        event.value,
      );
      opBloc.add(FinishOperation());
    } catch (e, st) {
      debugPrint("$st");
      opBloc.add(
        FailOperation(message: "Fehler beim Aktualisieren der Optionen"),
      );
    }
  }

  void _updateDiscount(
    UpdateDiscountEvent event,
    Emitter<TourState> emit,
  ) async {
    opBloc.add(LoadOperation());

    try {
      opBloc.add(FinishOperation());
      await tourRepository.updateDiscount(
        event.deliveryId,
        event.reason,
        event.value,
      );
      opBloc.add(FinishOperation());
    } catch (e, st) {
      debugPrint(
        "Fehler beim Hinzufügen eins Discounts zur Lieferung: ${event.deliveryId}:",
      );
      debugPrint("$e");
      debugPrint("$st");

      opBloc.add(
        FailOperation(message: "Fehler beim Hinzufügen des Discounts: $e"),
      );
    }
  }

  void _removeDiscount(
    RemoveDiscountEvent event,
    Emitter<TourState> emit,
  ) async {
    opBloc.add(LoadOperation());

    try {
      await tourRepository.removeDiscount(event.deliveryId);
      opBloc.add(FinishOperation());
    } catch (e, st) {
      debugPrint(
        "Fehler beim Löschen des Discounts der Lieferung: ${event.deliveryId}:",
      );
      debugPrint("$e");
      debugPrint("$st");

      opBloc.add(
        FailOperation(message: "Fehler beim Löschen des Discounts: $e"),
      );
    }
  }

  void _addDiscount(AddDiscountEvent event, Emitter<TourState> emit) async {
    opBloc.add(LoadOperation());

    try {
      await tourRepository.addDiscount(
        event.deliveryId,
        event.reason,
        event.value,
      );

      opBloc.add(FinishOperation());
    } catch (e, st) {
      debugPrint(
        "Fehler beim Hinzufügen eins Discounts zur Lieferung: ${event.deliveryId}:",
      );
      debugPrint("$e");
      debugPrint("$st");

      opBloc.add(
        FailOperation(message: "Fehler beim Hinzufügen des Discounts: $e"),
      );
    }
  }

  void _unscan(UnscanArticleEvent event, Emitter<TourState> emit) async {
    opBloc.add(LoadOperation());

    try {
      await tourRepository.unscan(
        event.deliveryId,
        event.articleId,
        event.newAmount,
        event.reason,
      );
      opBloc.add(FinishOperation());
    } catch (e, st) {
      debugPrint("Fehler beim Unscan des Artikels: ${event.articleId}:");
      debugPrint("$e");
      debugPrint("$st");

      opBloc.add(FailOperation(message: "Fehler beim Unscan des Artikels: $e"));
    }
  }

  void _resetAmount(ResetScanAmountEvent event, Emitter<TourState> emit) async {
    opBloc.add(LoadOperation());

    try {
      await tourRepository.resetScan(event.articleId, event.deliveryId);

      opBloc.add(FinishOperation());
    } catch (e, st) {
      debugPrint("Fehler beim Unscan des Artikels: ${event.articleId}:");
      debugPrint("$e");
      debugPrint("$st");

      opBloc.add(FailOperation(message: "Fehler beim Zurücksetzen: $e"));
    }
  }
}
