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/repository/process_repository.dart';
import 'package:hl_lieferservice/feature/delivery/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/feature/delivery/util.dart';
import 'package:hl_lieferservice/model/tour.dart';
import 'package:hl_lieferservice/feature/authentication/bloc/auth_bloc.dart';
import 'package:hl_lieferservice/feature/authentication/bloc/auth_event.dart';
import 'package:hl_lieferservice/feature/authentication/exceptions.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;
  AuthBloc authBloc;
  TourRepository tourRepository;
  ProcessRepository processRepository;
  StreamSubscription? _combinedSubscription;

  TourBloc({
    required this.opBloc,
    required this.authBloc,
    required this.tourRepository,
    ProcessRepository? processRepository,
  })  : processRepository = processRepository ?? ProcessRepository(),
        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<UnassignDeliveryEvent>(_unassignDelivery);
    on<IncrementArticleScanAmount>(_increment);
    on<ScanArticleEvent>(_scan);
    on<ScanComponentEvent>(_scanComponent);
    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<ReplaceSortingEvent>(_replaceSorting);
    on<ConfirmSortingEvent>(_confirmSorting);
    on<EnsureSortingForCarEvent>(_ensureSortingForCar);
    on<CarsLoadedEvent>(_carsLoaded);
    on<SetArticleAmountEvent>(_setArticleAmount);
  }

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

  void _handleError(Object e, String fallbackMessage) {
    if (e is UserUnauthorized) {
      authBloc.add(SessionExpiredEvent());
    } else {
      opBloc.add(FailOperation(message: fallbackMessage));
    }
  }

  void _setArticleAmount(
    SetArticleAmountEvent event,
    Emitter<TourState> emit,
  ) async {
    final currentState = state;
    if (currentState is TourLoaded) {
      opBloc.add(StartOperation());
      try {
        await tourRepository.setArticleAmount(
          event.deliveryId,
          event.articleId,
          event.amount,
          event.reason,
        );
        opBloc.add(FinishOperation());
      } catch (e, st) {
        debugPrint("$e $st");
        _handleError(e, "Fehler beim Ändern der Menge des Artikels");
      }
    }
  }

  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) {
      Map<String, List<String>> container = {...currentState.sortingInformation};

      List<String> reorderedList = reorderList(
        container[event.carId.toString()] ?? [],
        event.oldPosition,
        event.newPosition,
      );

      container[event.carId.toString()] = reorderedList;
      await ReorderService().saveSortingInformation(container);

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

  Future<void> _ensureSortingForCar(
    EnsureSortingForCarEvent event,
    Emitter<TourState> emit,
  ) async {
    final currentState = state;
    if (currentState is! TourLoaded) return;

    // Ein-Auto-Teams: alle Tour-Lieferungen sind die eigenen — die
    // Zuordnung erfolgt erst beim Scannen.
    // Mehr-Auto-Teams: nach dem Auswahl-Schritt sind die Lieferungen
    // bereits per `assignCar` mit der eigenen carId verknüpft; wir
    // sortieren dann ausschließlich die eigenen.
    final cars = currentState.tour.driver.cars;
    final allTourIds = cars.length >= 2
        ? currentState.tour.deliveries
            .where((d) => d.carId?.toString() == event.carId)
            .map((d) => d.id)
            .toList(growable: false)
        : currentState.tour.deliveries
            .map((d) => d.id)
            .toList(growable: false);
    final existing =
        currentState.sortingInformation[event.carId] ?? const <String>[];

    // Bestehende Reihenfolge beibehalten (nur Einträge, die noch in der Tour
    // sind), dann fehlende Tour-Lieferungen hinten anhängen.
    final allIdsSet = allTourIds.toSet();
    final seen = <String>{};
    final merged = <String>[];
    for (final id in existing) {
      if (allIdsSet.contains(id) && seen.add(id)) merged.add(id);
    }
    for (final id in allTourIds) {
      if (seen.add(id)) merged.add(id);
    }

    if (merged.length == existing.length &&
        _listEquals(merged, existing.toList())) {
      // Nichts zu tun — Bucket ist bereits konsistent.
      return;
    }

    final container = {...currentState.sortingInformation};
    container[event.carId] = merged;

    await ReorderService().saveSortingInformation(container);
    emit(currentState.copyWith(sortingInformation: container));
  }

  bool _listEquals(List<String> a, List<String> b) {
    if (a.length != b.length) return false;
    for (int i = 0; i < a.length; i++) {
      if (a[i] != b[i]) return false;
    }
    return true;
  }

  Future<void> _replaceSorting(
    ReplaceSortingEvent event,
    Emitter<TourState> emit,
  ) async {
    final currentState = state;
    if (currentState is TourLoaded) {
      await ReorderService().saveSortingInformation(
        event.newSortingInformation,
      );
      emit(
        currentState.copyWith(
          sortingInformation: event.newSortingInformation,
        ),
      );
    }
  }

  Future<void> _confirmSorting(
    ConfirmSortingEvent event,
    Emitter<TourState> emit,
  ) async {
    final currentState = state;
    if (currentState is! TourLoaded) return;

    emit(
      currentState.copyWith(
        isPersistingSorting: true,
        clearSortingPersistError: true,
      ),
    );

    final orderedIds =
        currentState.sortingInformation[event.carId] ?? const <String>[];

    try {
      await processRepository.persistDeliveryOrder(
        carId: event.carId,
        orderedDeliveryIds: orderedIds,
      );

      // Hinweis: Der eigentliche Phasen-Wechsel auf `beladen` läuft über
      // den [PhaseBloc], angestoßen vom UI-Listener in DeliverySortPage.
      // So bleibt der Phasen-State zentral und der TourBloc unabhängig.

      // Re-read the latest state — earlier emits or upstream tour updates
      // may have replaced it while the await was in flight.
      final latest = state;
      if (latest is TourLoaded) {
        emit(latest.copyWith(isPersistingSorting: false));
      }
    } catch (e, st) {
      debugPrint("Fehler beim Persistieren der Sortierung: $e $st");
      final latest = state;
      if (latest is TourLoaded) {
        emit(
          latest.copyWith(
            isPersistingSorting: false,
            sortingPersistError:
                "Reihenfolge konnte nicht gespeichert werden. Bitte erneut versuchen.",
          ),
        );
      }
    }
  }

  void _calculateDistances(
    RequestDeliveryDistanceEvent event,
    Emitter<TourState> emit,
  ) async {
    Map<String, double> distances = {};

    for (final delivery in event.tour.deliveries) {
      try {
        distances[delivery.id] = await DistanceService.getDistanceByRoad(
          delivery.customer.address.toString(),
        );
      } catch (e, st) {
        debugPrint("Fehler beim Laden der Distanz: $e");
        debugPrint("$st");
        distances[delivery.id] = double.nan;
      }
    }

    final currentState = state;
    if (currentState is TourLoaded) {
      emit(currentState.copyWith(distances: distances));
    }
  }

  void _requestSortingInformation(
    RequestSortingInformationEvent event,
    Emitter<TourState> emit,
  ) async {
    Map<String, List<String>> container = {};

    try {
      ReorderService service = ReorderService();

      // 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[delivery.carId.toString()]!.indexWhere(
          (id) => id == delivery.id,
        );

        // not found, so add it to the list
        if (info == -1) {
          inconsistent = true;
          container[delivery.carId.toString()]!.add(delivery.id);
        }
      }

      // if new deliveries were added then save the information with the newly
      // populated container
      if (inconsistent) {
        await service.saveSortingInformation(container);
      }
    } 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",
        ),
      );

      // fill the container without sorting information
      for (final delivery in event.tour.deliveries) {
        container[delivery.carId.toString()]!.add(delivery.id);
      }
    }

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

    add(RequestDeliveryDistanceEvent(tour: event.tour));
  }

  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,
          pendingScanRequests: currentState.pendingScanRequests,
          isPersistingSorting: currentState.isPersistingSorting,
          sortingPersistError: currentState.sortingPersistError,
        ),
      );
    }

    if (currentState is TourLoading) {
      add(
        RequestSortingInformationEvent(tour: tour.copyWith(), payments: payments),
      );
    }
  }

  void _reactivateDelivery(
    ReactivateDeliveryEvent event,
    Emitter<TourState> emit,
  ) async {
    final currentState = state;
    if (currentState is TourLoaded) {
      opBloc.add(StartOperation());
      try {
        await tourRepository.reactivateDelivery(event.deliveryId);
        opBloc.add(FinishOperation());
      } catch (e, st) {
        debugPrint("$e $st");
        _handleError(e, "Fehler beim Zurückstellen der Lieferung");
      }
    }
  }

  void _holdDelivery(HoldDeliveryEvent event, Emitter<TourState> emit) async {
    final currentState = state;
    if (currentState is TourLoaded) {
      opBloc.add(StartOperation());
      try {
        await tourRepository.holdDelivery(event.deliveryId);
        opBloc.add(FinishOperation());
      } catch (e, st) {
        debugPrint("$e $st");
        _handleError(e, "Fehler beim Zurückstellen der Lieferung");
      }
    }
  }

  void _cancelDelivery(
    CancelDeliveryEvent event,
    Emitter<TourState> emit,
  ) async {
    final currentState = state;
    if (currentState is TourLoaded) {
      opBloc.add(StartOperation());
      try {
        await tourRepository.cancelDelivery(event.deliveryId);
        opBloc.add(FinishOperation());
      } catch (e, st) {
        debugPrint("$e $st");
        _handleError(e, "Fehler beim Stornieren der Lieferung");
      }
    }
  }

  void _bumpPendingScans(Emitter<TourState> emit, int delta) {
    final currentState = state;
    if (currentState is TourLoaded) {
      final next = (currentState.pendingScanRequests + delta).clamp(0, 1 << 30);
      emit(currentState.copyWith(pendingScanRequests: next));
    }
  }

  void _scanComponent(
    ScanComponentEvent event,
    Emitter<TourState> emit,
  ) async {
    final currentState = state;

    if (currentState is TourLoaded) {
      _bumpPendingScans(emit, 1);
      try {
        switch (await tourRepository.scanComponent(
          event.deliveryId,
          event.carId,
          event.componentArticleNumber,
        )) {
          case ScanResult.scanned:
            opBloc.add(FinishOperation(message: 'Komponente gescannt'));
            break;
          case ScanResult.alreadyScanned:
            opBloc.add(
              FailOperation(message: 'Komponente wurde bereits gescannt'),
            );
            break;
          case ScanResult.notFound:
            opBloc.add(
              FailOperation(
                message: 'Komponente ist für keine Lieferung vorgesehen',
              ),
            );
            break;
        }
      } catch (e, st) {
        debugPrint("FEHLER beim Scannen einer Komponente: $e $st");
        _handleError(e, "Fehler beim Scannen der Komponente");
      } finally {
        _bumpPendingScans(emit, -1);
      }
    }
  }

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

    if (currentState is TourLoaded) {
      _bumpPendingScans(emit, 1);
      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;
        }
      } catch (e, st) {
        debugPrint("FEHLER beim Scannen eines Artikels: $e $st");
        _handleError(e, "Fehler beim Scannen des Artikels");
      } finally {
        _bumpPendingScans(emit, -1);
      }
    }
  }

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

    if (currentState is TourLoaded) {
      _bumpPendingScans(emit, 1);
      try {
        await tourRepository.scanArticle(
          event.deliveryId,
          event.carId,
          event.internalArticleId,
        );
      } catch (e, st) {
        debugPrint("$e $st");
        _handleError(e, "Fehler beim Scannen des Artikels");
      } finally {
        _bumpPendingScans(emit, -1);
      }
    }
  }

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

  Future<void> _unassignDelivery(
    UnassignDeliveryEvent event,
    Emitter<TourState> emit,
  ) async {
    final currentState = state;
    if (currentState is! TourLoaded) return;

    opBloc.add(StartOperation());
    try {
      // Stub-Aufruf — solange kein echter Endpoint existiert, gibt es
      // hier kein Tour-Update; das lokale Modell behält den alten carId-
      // Wert. Mit dem echten Endpoint wird die Tour anschließend über
      // den tourRepository-Stream aktualisiert (analog zu assignCar).
      await processRepository.unassignDeliveryFromCar(
        deliveryId: event.deliveryId,
      );
      opBloc.add(FinishOperation());
    } catch (e, st) {
      debugPrint("$e $st");
      _handleError(e, "Fehler beim Freigeben der Lieferung");
    }
  }

  Future<void> _load(LoadTour event, Emitter<TourState> emit) async {
    try {
      emit(TourLoading());
      await tourRepository.loadTourOfToday(event.teamId);
      await tourRepository.loadPaymentOptions();
    } catch (e) {
      if (e is UserUnauthorized) {
        authBloc.add(SessionExpiredEvent());
        return;
      }
      emit(TourLoadingFailed());
      opBloc.add(FailOperation(message: "Fehler beim Laden der heutigen Fahrten"));
    }
  }

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

    if (currentState is TourLoaded) {
      opBloc.add(StartOperation(message: "Lieferung wird abgeschlossen…"));
      try {
        await tourRepository.uploadDriverSignature(
          event.deliveryId,
          event.driverSignature,
        );
        await tourRepository.uploadCustomerSignature(
          event.deliveryId,
          event.customerSignature,
        );

        await tourRepository.finishDelivery(event.deliveryId);
        opBloc.add(FinishOperation(message: "Lieferung abgeschlossen"));
      } catch (e, st) {
        debugPrint("$e $st");
        _handleError(e, "Fehler beim Abschließen der Lieferung");
      }
    }
  }

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

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

  void _updateDiscount(
    UpdateDiscountEvent event,
    Emitter<TourState> emit,
  ) async {
    opBloc.add(StartOperation());
    try {
      await tourRepository.updateDiscount(
        event.deliveryId,
        event.reason,
        event.value,
      );
      opBloc.add(FinishOperation());
    } catch (e, st) {
      debugPrint("Fehler beim Aktualisieren des Discounts: $e $st");
      _handleError(e, "Fehler beim Aktualisieren des Discounts");
    }
  }

  void _removeDiscount(
    RemoveDiscountEvent event,
    Emitter<TourState> emit,
  ) async {
    opBloc.add(StartOperation());
    try {
      await tourRepository.removeDiscount(event.deliveryId);
      opBloc.add(FinishOperation());
    } catch (e, st) {
      debugPrint("Fehler beim Löschen des Discounts: $e $st");
      _handleError(e, "Fehler beim Löschen des Discounts");
    }
  }

  void _addDiscount(AddDiscountEvent event, Emitter<TourState> emit) async {
    opBloc.add(StartOperation());
    try {
      await tourRepository.addDiscount(
        event.deliveryId,
        event.reason,
        event.value,
      );
      opBloc.add(FinishOperation());
    } catch (e, st) {
      debugPrint("Fehler beim Hinzufügen des Discounts: $e $st");
      _handleError(e, "Fehler beim Hinzufügen des Discounts");
    }
  }

  void _unscan(UnscanArticleEvent event, Emitter<TourState> emit) async {
    opBloc.add(StartOperation());
    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}: $e $st");
      _handleError(e, "Fehler beim Unscan des Artikels");
    }
  }

  void _resetAmount(ResetScanAmountEvent event, Emitter<TourState> emit) async {
    opBloc.add(StartOperation());
    try {
      await tourRepository.resetScan(event.articleId, event.deliveryId);
      opBloc.add(FinishOperation());
    } catch (e, st) {
      debugPrint("Fehler beim Zurücksetzen Artikel ${event.articleId}: $e $st");
      _handleError(e, "Fehler beim Zurücksetzen");
    }
  }
}
