import 'dart:async';

import 'package:flutter/foundation.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:uuid/uuid.dart';

import 'package:hl_lieferservice/data/cache/attachment_cache.dart';
import 'package:hl_lieferservice/domain/entity/delivery.dart';
import 'package:hl_lieferservice/domain/entity/delivery_credit.dart';
import 'package:hl_lieferservice/domain/entity/delivery_item.dart';
import 'package:hl_lieferservice/domain/entity/delivery_note.dart';
import 'package:hl_lieferservice/domain/entity/delivery_service_value.dart';
import 'package:hl_lieferservice/domain/entity/scan_intent.dart';
import 'package:hl_lieferservice/domain/entity/scan_progress.dart';
import 'package:hl_lieferservice/domain/entity/tour_details.dart';
import 'package:hl_lieferservice/domain/repository/tour_repository.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/widget/operations/bloc/operation_bloc.dart';
import 'package:hl_lieferservice/widget/operations/bloc/operation_event.dart';

/// Bloc für die heutige Tour. Phase C+D-2: nur Lese-Pfad + Reorder +
/// Car-Assign. Spätere Schreib-Aktionen (Scan/Hold/Cancel/Complete/Notes)
/// landen in einem späteren Schritt **hier** und nicht in einem
/// Parallel-Bloc — der Plan ist ein Bloc pro Aggregat, nicht pro Aktion.
class TourBloc extends Bloc<TourEvent, TourState> {
  TourBloc({
    required this.tourRepository,
    required this.opBloc,
    required this.attachmentCache,
  }) : super(const TourInitial()) {
    on<LoadTour>(_onLoad);
    on<RefreshTour>(_onRefresh);
    on<ReorderDeliveries>(_onReorder);
    on<AssignCarToDelivery>(_onAssignCar);
    on<AssignCarToDeliveries>(_onAssignCarBulk);
    on<ScanItem>(_onScanItem);
    on<UnscanItem>(_onUnscanItem);
    on<HoldItem>(_onHoldItem);
    on<UnholdItem>(_onUnholdItem);
    on<RemoveItem>(_onRemoveItem);
    on<UnremoveItem>(_onUnremoveItem);
    on<CancelDelivery>(_onCancelDelivery);
    on<HoldDelivery>(_onHoldDelivery);
    on<ResumeDelivery>(_onResumeDelivery);
    on<CompleteDelivery>(_onCompleteDelivery);
    on<AddDeliveryNote>(_onAddDeliveryNote);
    on<UpdateDeliveryNote>(_onUpdateDeliveryNote);
    on<DeleteDeliveryNote>(_onDeleteDeliveryNote);
    on<UploadDeliveryNoteImage>(_onUploadDeliveryNoteImage);
    on<SetDeliveryCredit>(_onSetDeliveryCredit);
    on<RemoveDeliveryCredit>(_onRemoveDeliveryCredit);
    on<SetDeliveryServiceValue>(_onSetDeliveryServiceValue);
    on<RemoveDeliveryServiceValue>(_onRemoveDeliveryServiceValue);
  }

  final TourRepository tourRepository;
  final OperationBloc opBloc;

  /// Disk-Cache für Attachment-Vorschauen. Der Bloc nutzt ihn nur fürs
  /// **Pruning**: bei jedem frischen Tour-Load wird der Cache auf die in der
  /// Tour referenzierten Bild-IDs eingedampft, sodass Vorschauen gelöschter
  /// Foto-Notizen lokal verschwinden.
  final AttachmentCache attachmentCache;

  final Uuid _uuid = const Uuid();

  /// Räumt verwaiste Cache-Bilder weg, sobald frische Tour-Daten vorliegen.
  /// Fire-and-forget — Cache-Pflege darf den Load nie verzögern oder
  /// scheitern lassen.
  void _pruneAttachmentCache(TourDetails details) {
    unawaited(attachmentCache.retainOnly(details.referencedAttachmentIds));
  }

  // ─── LoadTour ────────────────────────────────────────────────────────

  Future<void> _onLoad(LoadTour event, Emitter<TourState> emit) async {
    emit(const TourLoading());
    try {
      final details = await tourRepository.getMyTourDetailsOfToday();
      if (details == null) {
        emit(const TourEmpty());
        return;
      }
      emit(TourLoaded(details: details));
      _pruneAttachmentCache(details);
    } catch (e, st) {
      debugPrint('TourBloc.LoadTour fehlgeschlagen: $e\n$st');
      final message = _messageOf(e, 'Tour konnte nicht geladen werden');
      emit(TourLoadFailed(message: message));
      opBloc.add(FailOperation(message: message));
    }
  }

  // ─── RefreshTour ─────────────────────────────────────────────────────

  Future<void> _onRefresh(RefreshTour event, Emitter<TourState> emit) async {
    final current = state;
    if (current is! TourLoaded) {
      // Keine sichtbare Tour zum „weichen" Refreshen — wie ein LoadTour
      // behandeln.
      await _onLoad(const LoadTour(), emit);
      return;
    }

    emit(current.copyWith(isRefreshing: true, refreshError: null));
    try {
      final details = await tourRepository.getMyTourDetailsOfToday();
      if (details == null) {
        emit(const TourEmpty());
        return;
      }
      emit(TourLoaded(details: details));
      _pruneAttachmentCache(details);
    } catch (e, st) {
      debugPrint('TourBloc.RefreshTour fehlgeschlagen: $e\n$st');
      final message = _messageOf(e, 'Tour konnte nicht neu geladen werden');
      // alten Stand sichtbar lassen, Fehler oben mitführen
      final latest = state;
      if (latest is TourLoaded) {
        emit(latest.copyWith(isRefreshing: false, refreshError: message));
      } else {
        emit(TourLoadFailed(message: message));
      }
    }
  }

  // ─── ReorderDeliveries ───────────────────────────────────────────────

  Future<void> _onReorder(
    ReorderDeliveries event,
    Emitter<TourState> emit,
  ) async {
    final current = state;
    if (current is! TourLoaded) return;

    emit(current.copyWith(isPersistingReorder: true, reorderError: null));
    try {
      final updated = await tourRepository.setDeliveryOrder(
        tourId: current.details.tour.id,
        orderedDeliveryIds: event.orderedDeliveryIds,
      );

      final patched = _applySortOrder(current.details, updated);
      emit(TourLoaded(
        details: patched,
        isPersistingReorder: false,
      ));
    } catch (e, st) {
      debugPrint('TourBloc.ReorderDeliveries fehlgeschlagen: $e\n$st');
      final message = _messageOf(
        e,
        'Reihenfolge konnte nicht gespeichert werden',
      );
      final latest = state;
      if (latest is TourLoaded) {
        emit(latest.copyWith(
          isPersistingReorder: false,
          reorderError: message,
        ));
      }
      opBloc.add(FailOperation(message: message));
    }
  }

  TourDetails _applySortOrder(
    TourDetails details,
    Map<String, int> newOrderByDeliveryId,
  ) {
    final next = <Delivery>[];
    for (final d in details.deliveries) {
      final newOrder = newOrderByDeliveryId[d.id];
      next.add(newOrder == null ? d : d.copyWith(sortOrder: newOrder));
    }
    return details.copyWith(deliveries: next);
  }

  // ─── AssignCarToDelivery ─────────────────────────────────────────────

  Future<void> _onAssignCar(
    AssignCarToDelivery event,
    Emitter<TourState> emit,
  ) async {
    final current = state;
    if (current is! TourLoaded) return;

    opBloc.add(StartOperation());
    try {
      final remoteDelivery = await tourRepository.assignCarToDelivery(
        deliveryId: event.deliveryId,
        carId: event.carId,
      );

      // Server schickt die Delivery ohne Items zurück (Phase-1-Endpoint).
      // Wir bauen die lokale Delivery so um, dass alle Stamm-Felder vom
      // Server übernommen werden, Items und sortOrder aus dem lokalen
      // Aggregat erhalten bleiben.
      final localDelivery = current.details.deliveries
          .firstWhere((d) => d.id == event.deliveryId, orElse: () => remoteDelivery);

      final merged = remoteDelivery.copyWith(
        sortOrder: localDelivery.sortOrder,
        items: localDelivery.items,
      );

      emit(current.copyWith(details: current.details.replaceDelivery(merged)));
      opBloc.add(FinishOperation());
    } catch (e, st) {
      debugPrint('TourBloc.AssignCarToDelivery fehlgeschlagen: $e\n$st');
      opBloc.add(FailOperation(
        message: _messageOf(e, 'Fahrzeug-Zuweisung fehlgeschlagen'),
      ));
    }
  }

  // ─── AssignCarToDeliveries (Bulk) ────────────────────────────────────

  /// Sequenzielle Variante von [_onAssignCar]: arbeitet die Liste der
  /// Lieferungs-Ids in der gegebenen Reihenfolge ab, merged jedes
  /// Server-Update mit dem Items-/sortOrder-Stand aus dem lokalen
  /// Aggregat und emittiert genau **einen** State am Ende. Damit ist die
  /// Aktion atomar — entweder sieht das UI alle erfolgreichen Zuweisungen
  /// auf einmal, oder bei einem Fehler die bis dahin erfolgreichen
  /// (Backend hat sie bereits committed) zusammen mit einer Fail-Meldung.
  Future<void> _onAssignCarBulk(
    AssignCarToDeliveries event,
    Emitter<TourState> emit,
  ) async {
    final current = state;
    if (current is! TourLoaded) return;
    if (event.deliveryIds.isEmpty) return;

    opBloc.add(StartOperation());

    var workingDetails = current.details;
    var successCount = 0;
    String? failureMessage;

    for (final deliveryId in event.deliveryIds) {
      try {
        final remote = await tourRepository.assignCarToDelivery(
          deliveryId: deliveryId,
          carId: event.carId,
        );
        final local = workingDetails.deliveries.firstWhere(
          (d) => d.id == deliveryId,
          orElse: () => remote,
        );
        final merged = remote.copyWith(
          sortOrder: local.sortOrder,
          items: local.items,
        );
        workingDetails = workingDetails.replaceDelivery(merged);
        successCount++;
      } catch (e, st) {
        debugPrint(
          'TourBloc.AssignCarToDeliveries[$deliveryId] fehlgeschlagen: $e\n$st',
        );
        failureMessage = _messageOf(e, 'Fahrzeug-Zuweisung fehlgeschlagen');
        break;
      }
    }

    emit(current.copyWith(details: workingDetails));

    if (failureMessage != null) {
      opBloc.add(FailOperation(
        message: successCount == 0
            ? failureMessage
            : '$successCount von ${event.deliveryIds.length} Lieferungen '
                'zugewiesen — $failureMessage',
      ));
    } else {
      opBloc.add(FinishOperation());
    }
  }

  // ─── ScanItem / UnscanItem ───────────────────────────────────────────

  /// Plus-Eins-Scan: lokal sofort hochzählen, anschließend Backend
  /// bestätigen lassen. Bei `rejected` zurückrollen, bei `duplicate`
  /// still bleiben (Server-/Client-State stimmen schon überein).
  Future<void> _onScanItem(ScanItem event, Emitter<TourState> emit) async {
    final current = state;
    if (current is! TourLoaded) return;

    final beforeItem = _findItem(current.details, event.deliveryItemId);
    if (beforeItem == null) {
      opBloc.add(FailOperation(
        message: 'Artikel nicht in der aktuellen Tour gefunden',
      ));
      return;
    }
    if (beforeItem.isDone) {
      // Defensive: UI sollte „done" gar nicht zum Scan zulassen. Falls doch,
      // hier still skippen statt einen rejected vom Server zu provozieren.
      opBloc.add(FailOperation(message: 'Artikel ist bereits vollständig'));
      return;
    }

    // Manuelle Bestätigung verbucht die GANZE Restmenge auf einmal; der
    // reguläre (Barcode-)Scan zählt um +1 hoch.
    final remaining =
        beforeItem.requiredQuantity - beforeItem.scanProgress.scannedQuantity;
    final delta = event.manual ? remaining : 1;
    final newQuantity = beforeItem.scanProgress.scannedQuantity + delta;

    // ── Optimistisches Update ──
    final optimisticItem = beforeItem.copyWith(
      scanProgress: beforeItem.scanProgress.copyWith(
        scannedQuantity: newQuantity,
        lastUpdatedAt: DateTime.now(),
        status: newQuantity >= beforeItem.requiredQuantity
            ? ScanStatus.done
            : ScanStatus.inProgress,
      ),
    );
    emit(current.copyWith(
      details: _replaceItem(current.details, optimisticItem),
    ));

    // ── Server-Apply ──
    // Bei manueller Bestätigung schickt die App die Restmenge als `quantity`
    // mit (Backend verbucht sie als ein Scan-Event, `manual=true`).
    final intent = ScanIntent(
      clientScanId: _uuid.v4(),
      clientScannedAt: DateTime.now(),
      deliveryItemId: event.deliveryItemId,
      action: ScanAction.scan,
      actorCarId: event.actorCarId,
      quantity: event.manual ? remaining : null,
      manual: event.manual,
    );
    await _applyAndReconcile(emit, intent, beforeItem);
  }

  Future<void> _onUnscanItem(
    UnscanItem event,
    Emitter<TourState> emit,
  ) async {
    final current = state;
    if (current is! TourLoaded) return;

    final beforeItem = _findItem(current.details, event.deliveryItemId);
    if (beforeItem == null) return;
    if (beforeItem.scanProgress.scannedQuantity == 0) {
      opBloc.add(FailOperation(
        message: 'Artikel hat keine Scans zum Zurücknehmen',
      ));
      return;
    }

    final optimisticItem = beforeItem.copyWith(
      scanProgress: beforeItem.scanProgress.copyWith(
        scannedQuantity: beforeItem.scanProgress.scannedQuantity - 1,
        lastUpdatedAt: DateTime.now(),
        status: ScanStatus.inProgress,
      ),
    );
    emit(current.copyWith(
      details: _replaceItem(current.details, optimisticItem),
    ));

    final intent = ScanIntent(
      clientScanId: _uuid.v4(),
      clientScannedAt: DateTime.now(),
      deliveryItemId: event.deliveryItemId,
      action: ScanAction.unscan,
      actorCarId: event.actorCarId,
      reason: event.reason,
    );
    await _applyAndReconcile(emit, intent, beforeItem);
  }

  Future<void> _onHoldItem(HoldItem event, Emitter<TourState> emit) async {
    final current = state;
    if (current is! TourLoaded) return;

    final beforeItem = _findItem(current.details, event.deliveryItemId);
    if (beforeItem == null) return;

    final optimisticItem = beforeItem.copyWith(
      scanProgress: beforeItem.scanProgress.copyWith(
        status: ScanStatus.held,
        heldReason: event.reason,
        lastUpdatedAt: DateTime.now(),
      ),
    );
    emit(current.copyWith(
      details: _replaceItem(current.details, optimisticItem),
    ));

    final intent = ScanIntent(
      clientScanId: _uuid.v4(),
      clientScannedAt: DateTime.now(),
      deliveryItemId: event.deliveryItemId,
      action: ScanAction.hold,
      actorCarId: event.actorCarId,
      reason: event.reason,
    );
    await _applyAndReconcile(emit, intent, beforeItem);
  }

  Future<void> _onUnholdItem(
    UnholdItem event,
    Emitter<TourState> emit,
  ) async {
    final current = state;
    if (current is! TourLoaded) return;

    final beforeItem = _findItem(current.details, event.deliveryItemId);
    if (beforeItem == null) return;

    // Beim Unhold lokal zurück auf in_progress; `done` wird beim nächsten
    // Server-Refresh ggf. neu gesetzt, falls die scannedQuantity schon
    // requiredQuantity erreicht hatte.
    final optimisticItem = beforeItem.copyWith(
      scanProgress: ScanProgress(
        status: beforeItem.scanProgress.scannedQuantity >=
                beforeItem.requiredQuantity
            ? ScanStatus.done
            : ScanStatus.inProgress,
        scannedQuantity: beforeItem.scanProgress.scannedQuantity,
        lastUpdatedAt: DateTime.now(),
        // heldReason explizit weglassen — Backend löscht ihn auch.
      ),
    );
    emit(current.copyWith(
      details: _replaceItem(current.details, optimisticItem),
    ));

    final intent = ScanIntent(
      clientScanId: _uuid.v4(),
      clientScannedAt: DateTime.now(),
      deliveryItemId: event.deliveryItemId,
      action: ScanAction.unhold,
      actorCarId: event.actorCarId,
    );
    await _applyAndReconcile(emit, intent, beforeItem);
  }

  Future<void> _onRemoveItem(
    RemoveItem event,
    Emitter<TourState> emit,
  ) async {
    final current = state;
    if (current is! TourLoaded) return;

    final beforeItem = _findItem(current.details, event.deliveryItemId);
    if (beforeItem == null) return;

    // Optimistische Mengen-Gutschrift: `quantity == null` heißt „ganze
    // Restmenge". Status kippt erst auf `removed`, wenn voll gutgeschrieben.
    final before = beforeItem.scanProgress;
    final remaining = beforeItem.requiredQuantity - before.creditedQuantity;
    final n = event.quantity ?? remaining;
    final newCredited =
        (before.creditedQuantity + n).clamp(0, beforeItem.requiredQuantity);
    final fully = newCredited >= beforeItem.requiredQuantity;

    final optimisticItem = beforeItem.copyWith(
      scanProgress: before.copyWith(
        creditedQuantity: newCredited,
        status: fully ? ScanStatus.removed : before.status,
        lastUpdatedAt: DateTime.now(),
      ),
    );
    emit(current.copyWith(
      details: _replaceItem(current.details, optimisticItem),
    ));

    final intent = ScanIntent(
      clientScanId: _uuid.v4(),
      clientScannedAt: DateTime.now(),
      deliveryItemId: event.deliveryItemId,
      action: ScanAction.remove,
      actorCarId: event.actorCarId,
      reason: event.reason,
      quantity: event.quantity,
    );
    final applied = await _applyAndReconcile(emit, intent, beforeItem);

    // Oberartikel voll entfernt → seine Komponenten lokal ebenfalls als
    // entfernt zeigen. Das Backend hat sie in derselben Transaktion bereits
    // cascade-entfernt; hier ziehen wir nur die UI nach (der Scan-Response
    // liefert nur den State der einen Position). Erst nach bestätigtem
    // `applied`, damit ein abgelehnter Versuch die Komponenten nicht fälschlich
    // ausblendet.
    if (applied && fully) {
      final s = state;
      if (s is TourLoaded) {
        final parentNr =
            s.details.articleOf(beforeItem.articleId)?.articleNumber;
        Delivery? delivery;
        for (final d in s.details.deliveries) {
          if (d.id == beforeItem.deliveryId) {
            delivery = d;
            break;
          }
        }
        if (parentNr != null && delivery != null) {
          var details = s.details;
          for (final it in delivery.items) {
            if (it.parentArtikelNr == parentNr &&
                it.belegzeilenNr == beforeItem.belegzeilenNr &&
                it.scanProgress.status != ScanStatus.removed) {
              details = _replaceItem(
                details,
                it.copyWith(
                  scanProgress: it.scanProgress.copyWith(
                    creditedQuantity: it.requiredQuantity,
                    status: ScanStatus.removed,
                    lastUpdatedAt: DateTime.now(),
                  ),
                ),
              );
            }
          }
          emit(s.copyWith(details: details));
        }
      }
    }

    // Grund der Gutschrift zusätzlich als Notiz festhalten — aber nur, wenn
    // das Entfernen wirklich durchging (kein Note-Eintrag für abgelehnte
    // Versuche) und der Aufrufer das angefordert hat (Gutschrift-Flow).
    if (applied && event.saveReasonAsNote) {
      final articleName =
          current.details.articleOf(beforeItem.articleId)?.name ??
              'Artikel';
      add(AddDeliveryNote(
        deliveryId: beforeItem.deliveryId,
        text: 'Gutschrift – $articleName ($n×): ${event.reason}',
        // Verknüpfung zur Belegzeile → beim Unremove gezielt löschbar.
        creditDeliveryItemId: beforeItem.id,
      ));
    }
  }

  Future<void> _onUnremoveItem(
    UnremoveItem event,
    Emitter<TourState> emit,
  ) async {
    final current = state;
    if (current is! TourLoaded) return;

    final beforeItem = _findItem(current.details, event.deliveryItemId);
    if (beforeItem == null) return;

    // Optimistische Rücknahme der Gutschrift. `quantity == null` = alles
    // zurück. Stand die Zeile auf `removed` (voll gutgeschrieben), kommt sie
    // nach Scan-Menge zurück auf `done`/`in_progress`; bei Teil-Rücknahme
    // bleibt der Status. `heldReason` wird gelöscht (analog Backend).
    final before = beforeItem.scanProgress;
    final n = event.quantity ?? before.creditedQuantity;
    final newCredited =
        (before.creditedQuantity - n).clamp(0, beforeItem.requiredQuantity);
    final newStatus = before.status == ScanStatus.removed
        ? (before.scannedQuantity >= beforeItem.requiredQuantity
            ? ScanStatus.done
            : ScanStatus.inProgress)
        : before.status;

    final optimisticItem = beforeItem.copyWith(
      scanProgress: ScanProgress(
        status: newStatus,
        scannedQuantity: before.scannedQuantity,
        creditedQuantity: newCredited,
        lastUpdatedAt: DateTime.now(),
      ),
    );
    emit(current.copyWith(
      details: _replaceItem(current.details, optimisticItem),
    ));

    final intent = ScanIntent(
      clientScanId: _uuid.v4(),
      clientScannedAt: DateTime.now(),
      deliveryItemId: event.deliveryItemId,
      action: ScanAction.unremove,
      actorCarId: event.actorCarId,
      quantity: event.quantity,
    );
    final applied = await _applyAndReconcile(emit, intent, beforeItem);

    // Spiegelbild zur Notiz-Erzeugung beim Entfernen: nach erfolgreicher
    // Rücknahme die zugehörigen Gutschrift-Notizen (mit Bezug auf genau
    // diese Belegzeile) wieder löschen. Aus der aktuellsten Notizliste
    // gelesen, damit auch nach einem Tour-Reload die richtigen IDs erwischt
    // werden.
    if (applied) {
      final latest = state;
      if (latest is TourLoaded) {
        final linked = latest.details
            .notesOf(beforeItem.deliveryId)
            .where((note) => note.creditDeliveryItemId == event.deliveryItemId)
            .toList(growable: false);
        for (final note in linked) {
          add(DeleteDeliveryNote(
            deliveryId: beforeItem.deliveryId,
            noteId: note.id,
          ));
        }
      }
    }
  }

  // ─── Betrags-Gutschrift ──────────────────────────────────────────────

  /// Tauscht die Gutschrift einer Lieferung im Aggregat aus (`null` = entfernt).
  TourDetails _withCredit(
    TourDetails details,
    String deliveryId,
    DeliveryCredit? credit,
  ) {
    final next = Map<String, DeliveryCredit>.from(details.creditsByDeliveryId);
    if (credit == null) {
      next.remove(deliveryId);
    } else {
      next[deliveryId] = credit;
    }
    return details.copyWith(creditsByDeliveryId: next);
  }

  /// Löscht alle Betrags-Gutschrift-Notizen einer Lieferung am Backend
  /// (best-effort — die Notiz ist Beiwerk, die Gutschrift selbst ist führend).
  Future<void> _deleteAmountCreditNotes(
    String deliveryId,
    Iterable<DeliveryNote> notes,
  ) async {
    for (final note in notes.where((n) => n.isAmountCreditNote)) {
      try {
        await tourRepository.deleteDeliveryNote(
          deliveryId: deliveryId,
          noteId: note.id,
        );
      } catch (e, st) {
        debugPrint('TourBloc: Betrags-Gutschrift-Notiz löschen fehlgeschlagen: $e\n$st');
      }
    }
  }

  /// State-Update: entfernt alle Betrags-Gutschrift-Notizen der Lieferung aus
  /// der Notiz-Map und hängt optional [newNote] an. So bleibt pro Lieferung
  /// genau eine aktuelle Grund-Notiz.
  TourDetails _replaceAmountCreditNotes(
    TourDetails details,
    String deliveryId,
    DeliveryNote? newNote,
  ) {
    final nextNotes = Map<String, List<DeliveryNote>>.from(
      details.notesByDeliveryId,
    );
    final existing = nextNotes[deliveryId] ?? const <DeliveryNote>[];
    final kept = existing.where((n) => !n.isAmountCreditNote).toList();
    if (newNote != null) kept.add(newNote);
    nextNotes[deliveryId] = kept;
    return details.copyWith(notesByDeliveryId: nextNotes);
  }

  Future<void> _onSetDeliveryCredit(
    SetDeliveryCredit event,
    Emitter<TourState> emit,
  ) async {
    final current = state;
    if (current is! TourLoaded) return;

    final previous = current.details.creditOf(event.deliveryId);
    // Bestehende Grund-Notizen merken (zum Ersetzen nach Erfolg).
    final existingNotes = current.details.notesOf(event.deliveryId);
    // Optimistisch sofort anzeigen.
    emit(current.copyWith(
      details: _withCredit(
        current.details,
        event.deliveryId,
        DeliveryCredit(
          deliveryId: event.deliveryId,
          amountCents: event.amountCents,
          reason: event.reason,
        ),
      ),
    ));

    // Sichtbares „Request läuft"-Feedback (blockierender Spinner) + Erfolgs-/
    // Fehler-SnackBar über den OperationBloc.
    opBloc.add(StartOperation(message: 'Gutschrift wird gespeichert …'));
    try {
      final result = await tourRepository.setDeliveryCredit(
        deliveryId: event.deliveryId,
        clientEventId: _uuid.v4(),
        amountCents: event.amountCents,
        reason: event.reason,
        actorCarId: event.actorCarId,
      );

      // Grund als Notiz festhalten (wie beim Artikel-Entfernen). Alte
      // Betrags-Gutschrift-Notizen ersetzen, damit pro Lieferung genau eine
      // aktuelle Grund-Notiz steht. Best-effort — schlägt es fehl, bleibt die
      // Gutschrift trotzdem gesetzt.
      DeliveryNote? newNote;
      try {
        await _deleteAmountCreditNotes(event.deliveryId, existingNotes);
        newNote = await tourRepository.addDeliveryNote(
          deliveryId: event.deliveryId,
          text: 'Betrags-Gutschrift – ${event.amountCents ~/ 100} €: ${event.reason}',
          isAmountCreditNote: true,
        );
      } catch (e, st) {
        debugPrint('TourBloc: Betrags-Gutschrift-Notiz anlegen fehlgeschlagen: $e\n$st');
      }

      // Server ist autoritativ — den zurückgelieferten Stand übernehmen.
      final latest = state;
      if (latest is TourLoaded) {
        var details = _withCredit(latest.details, event.deliveryId, result);
        details =
            _replaceAmountCreditNotes(details, event.deliveryId, newNote);
        emit(latest.copyWith(details: details));
      }
      opBloc.add(FinishOperation(message: 'Gutschrift gespeichert'));
    } catch (e, st) {
      debugPrint('TourBloc.SetDeliveryCredit fehlgeschlagen: $e\n$st');
      final latest = state;
      if (latest is TourLoaded) {
        emit(latest.copyWith(
          details: _withCredit(latest.details, event.deliveryId, previous),
        ));
      }
      opBloc.add(FailOperation(
        message: _messageOf(e, 'Gutschrift konnte nicht gespeichert werden'),
      ));
    }
  }

  Future<void> _onRemoveDeliveryCredit(
    RemoveDeliveryCredit event,
    Emitter<TourState> emit,
  ) async {
    final current = state;
    if (current is! TourLoaded) return;

    final previous = current.details.creditOf(event.deliveryId);
    if (previous == null) return; // nichts zu entfernen

    final existingNotes = current.details.notesOf(event.deliveryId);

    emit(current.copyWith(
      details: _withCredit(current.details, event.deliveryId, null),
    ));

    opBloc.add(StartOperation(message: 'Gutschrift wird entfernt …'));
    try {
      await tourRepository.removeDeliveryCredit(
        deliveryId: event.deliveryId,
        clientEventId: _uuid.v4(),
        actorCarId: event.actorCarId,
      );

      // Spiegelbild zum Setzen: zugehörige Grund-Notiz(en) wieder löschen.
      await _deleteAmountCreditNotes(event.deliveryId, existingNotes);
      final latest = state;
      if (latest is TourLoaded) {
        emit(latest.copyWith(
          details: _replaceAmountCreditNotes(
            latest.details,
            event.deliveryId,
            null,
          ),
        ));
      }
      opBloc.add(FinishOperation(message: 'Gutschrift entfernt'));
    } catch (e, st) {
      debugPrint('TourBloc.RemoveDeliveryCredit fehlgeschlagen: $e\n$st');
      final latest = state;
      if (latest is TourLoaded) {
        emit(latest.copyWith(
          details: _withCredit(latest.details, event.deliveryId, previous),
        ));
      }
      opBloc.add(FailOperation(
        message: _messageOf(e, 'Gutschrift konnte nicht entfernt werden'),
      ));
    }
  }

  // ─── Services (Phase 4) ──────────────────────────────────────────────

  /// Tauscht den Service-Wert einer Lieferung im Aggregat aus
  /// (`null` = entfernt).
  TourDetails _withServiceValue(
    TourDetails details,
    String deliveryId,
    String serviceId,
    DeliveryServiceValue? value,
  ) {
    final outer = Map<String, Map<String, DeliveryServiceValue>>.from(
      details.serviceValuesByDeliveryId,
    );
    final inner = Map<String, DeliveryServiceValue>.from(
      outer[deliveryId] ?? const <String, DeliveryServiceValue>{},
    );
    if (value == null) {
      inner.remove(serviceId);
    } else {
      inner[serviceId] = value;
    }
    outer[deliveryId] = inner;
    return details.copyWith(serviceValuesByDeliveryId: outer);
  }

  Future<void> _onSetDeliveryServiceValue(
    SetDeliveryServiceValue event,
    Emitter<TourState> emit,
  ) async {
    final current = state;
    if (current is! TourLoaded) return;

    final previous =
        current.details.serviceValueOf(event.deliveryId, event.serviceId);
    emit(current.copyWith(
      details: _withServiceValue(
        current.details,
        event.deliveryId,
        event.serviceId,
        DeliveryServiceValue(
          deliveryId: event.deliveryId,
          serviceId: event.serviceId,
          boolValue: event.boolValue,
          numericValue: event.numericValue,
        ),
      ),
    ));

    opBloc.add(StartOperation(message: 'Checkliste-Eintrag wird gespeichert …'));
    try {
      final result = await tourRepository.setDeliveryService(
        deliveryId: event.deliveryId,
        serviceId: event.serviceId,
        boolValue: event.boolValue,
        numericValue: event.numericValue,
        actorCarId: event.actorCarId,
      );
      final latest = state;
      if (latest is TourLoaded) {
        emit(latest.copyWith(
          details: _withServiceValue(
            latest.details,
            event.deliveryId,
            event.serviceId,
            result,
          ),
        ));
      }
      opBloc.add(FinishOperation(message: 'Checkliste-Eintrag gespeichert'));
    } catch (e, st) {
      debugPrint('TourBloc.SetDeliveryServiceValue fehlgeschlagen: $e\n$st');
      final latest = state;
      if (latest is TourLoaded) {
        emit(latest.copyWith(
          details: _withServiceValue(
            latest.details,
            event.deliveryId,
            event.serviceId,
            previous,
          ),
        ));
      }
      opBloc.add(FailOperation(
        message: _messageOf(e, 'Checkliste-Eintrag konnte nicht gespeichert werden'),
      ));
    }
  }

  Future<void> _onRemoveDeliveryServiceValue(
    RemoveDeliveryServiceValue event,
    Emitter<TourState> emit,
  ) async {
    final current = state;
    if (current is! TourLoaded) return;

    final previous =
        current.details.serviceValueOf(event.deliveryId, event.serviceId);
    if (previous == null) return;

    emit(current.copyWith(
      details: _withServiceValue(
        current.details,
        event.deliveryId,
        event.serviceId,
        null,
      ),
    ));

    opBloc.add(StartOperation(message: 'Checkliste-Eintrag wird entfernt …'));
    try {
      await tourRepository.removeDeliveryService(
        deliveryId: event.deliveryId,
        serviceId: event.serviceId,
      );
      opBloc.add(FinishOperation(message: 'Checkliste-Eintrag entfernt'));
    } catch (e, st) {
      debugPrint('TourBloc.RemoveDeliveryServiceValue fehlgeschlagen: $e\n$st');
      final latest = state;
      if (latest is TourLoaded) {
        emit(latest.copyWith(
          details: _withServiceValue(
            latest.details,
            event.deliveryId,
            event.serviceId,
            previous,
          ),
        ));
      }
      opBloc.add(FailOperation(
        message: _messageOf(e, 'Checkliste-Eintrag konnte nicht entfernt werden'),
      ));
    }
  }

  /// Liefert `true`, wenn der Scan serverseitig angewendet (oder als
  /// `duplicate` akzeptiert) wurde, `false` bei Ablehnung/Fehler. Aufrufer,
  /// die an den Erfolg eine Folgeaktion knüpfen (z. B. die Gutschrift-Notiz),
  /// werten den Rückgabewert aus; die übrigen ignorieren ihn.
  Future<bool> _applyAndReconcile(
    Emitter<TourState> emit,
    ScanIntent intent,
    DeliveryItem before,
  ) async {
    try {
      final outcomes = await tourRepository.applyScans([intent]);
      final outcome = outcomes[intent.clientScanId];
      if (outcome == null) {
        // Server hat keinen Eintrag für unseren Scan zurückgegeben —
        // defensiv als rejected behandeln.
        _rollback(emit, before, 'Unklare Server-Antwort');
        return false;
      }
      switch (outcome.status) {
        case ScanOutcomeStatus.applied:
        case ScanOutcomeStatus.duplicate:
          // Erfolg. Optimistisches Update bleibt stehen. (Bei `applied`
          // könnten wir das Server-`newScanState` zurückmischen; das
          // sparen wir uns, weil unsere lokale Inkrement-Logik exakt
          // dem Backend-Verhalten entspricht.)
          return true;
        case ScanOutcomeStatus.rejected:
          _rollback(emit, before, outcome.reason ?? 'Scan abgelehnt');
          return false;
      }
    } catch (e, st) {
      debugPrint('TourBloc.applyScans fehlgeschlagen: $e\n$st');
      _rollback(emit, before, _messageOf(e, 'Scan fehlgeschlagen'));
      return false;
    }
  }

  void _rollback(Emitter<TourState> emit, DeliveryItem before, String reason) {
    final current = state;
    if (current is! TourLoaded) return;
    emit(current.copyWith(
      details: _replaceItem(current.details, before),
    ));
    opBloc.add(FailOperation(message: reason));
  }

  // Lookups/Mutationen am Aggregat ---------------------------------------

  DeliveryItem? _findItem(TourDetails details, String itemId) {
    for (final d in details.deliveries) {
      for (final it in d.items) {
        if (it.id == itemId) return it;
      }
    }
    return null;
  }

  TourDetails _replaceItem(TourDetails details, DeliveryItem updated) {
    final nextDeliveries = <Delivery>[];
    for (final d in details.deliveries) {
      if (d.id != updated.deliveryId) {
        nextDeliveries.add(d);
        continue;
      }
      final nextItems = <DeliveryItem>[];
      for (final it in d.items) {
        nextItems.add(it.id == updated.id ? updated : it);
      }
      nextDeliveries.add(d.copyWith(items: nextItems));
    }
    return details.copyWith(deliveries: nextDeliveries);
  }

  // ─── Delivery-Lifecycle ──────────────────────────────────────────────

  Future<void> _onCancelDelivery(
    CancelDelivery event,
    Emitter<TourState> emit,
  ) async {
    await _runLifecycle(
      emit: emit,
      deliveryId: event.deliveryId,
      operationLabel: 'Lieferung abbrechen',
      apply: () => tourRepository.cancelDelivery(
        deliveryId: event.deliveryId,
        reason: event.reason,
      ),
    );
  }

  Future<void> _onHoldDelivery(
    HoldDelivery event,
    Emitter<TourState> emit,
  ) async {
    await _runLifecycle(
      emit: emit,
      deliveryId: event.deliveryId,
      operationLabel: 'Lieferung pausieren',
      apply: () => tourRepository.holdDelivery(
        deliveryId: event.deliveryId,
        reason: event.reason,
      ),
    );
  }

  Future<void> _onResumeDelivery(
    ResumeDelivery event,
    Emitter<TourState> emit,
  ) async {
    await _runLifecycle(
      emit: emit,
      deliveryId: event.deliveryId,
      operationLabel: 'Lieferung fortsetzen',
      apply: () =>
          tourRepository.resumeDelivery(deliveryId: event.deliveryId),
    );
  }

  Future<void> _onCompleteDelivery(
    CompleteDelivery event,
    Emitter<TourState> emit,
  ) async {
    await _runLifecycle(
      emit: emit,
      deliveryId: event.deliveryId,
      operationLabel: 'Lieferung abschließen',
      apply: () => tourRepository.completeDelivery(
        deliveryId: event.deliveryId,
        customerSignaturePng: event.customerSignaturePng,
        driverSignaturePng: event.driverSignaturePng,
        receiptConfirmed: event.receiptConfirmed,
        notesAcknowledged: event.notesAcknowledged,
        acknowledgedNoteIds: event.acknowledgedNoteIds,
        paymentMethodId: event.paymentMethodId,
        actorCarId: event.actorCarId,
        paymentCollected: event.paymentCollected,
      ),
    );
  }

  /// Gemeinsamer Pfad für die drei Lifecycle-Operationen: globaler
  /// Loading-Overlay an, Server-Call, lokales Aggregat mit der neuen
  /// Stamm-Delivery mergen (Items/sortOrder aus dem aktuellen State
  /// behalten), Overlay aus. Konservativ — kein optimistisches Update,
  /// weil das selten genug passiert und der Fahrer eh bewusst auf den
  /// Reason-Dialog reagiert.
  Future<void> _runLifecycle({
    required Emitter<TourState> emit,
    required String deliveryId,
    required String operationLabel,
    required Future<Delivery> Function() apply,
  }) async {
    final current = state;
    if (current is! TourLoaded) return;

    opBloc.add(StartOperation());
    try {
      final remote = await apply();
      final local = current.details.deliveries
          .firstWhere((d) => d.id == deliveryId, orElse: () => remote);
      final merged = remote.copyWith(
        sortOrder: local.sortOrder,
        items: local.items,
      );
      emit(current.copyWith(
        details: current.details.replaceDelivery(merged),
      ));
      opBloc.add(FinishOperation());
    } catch (e, st) {
      debugPrint('TourBloc.$operationLabel fehlgeschlagen: $e\n$st');
      opBloc.add(FailOperation(
        message: _messageOf(e, '$operationLabel fehlgeschlagen'),
      ));
    }
  }

  // ─── AddDeliveryNote ─────────────────────────────────────────────────

  /// Schreibt eine Notiz an einer Lieferung und hängt sie in das lokale
  /// Tour-Aggregat ein. Bewusst kein optimistisches Update — die UI zeigt
  /// einen kurzen Loader und bekommt erst danach die Notiz mit echter
  /// Server-Id zurück; das vermeidet Geister-Notizen, wenn der Call kippt.
  Future<void> _onAddDeliveryNote(
    AddDeliveryNote event,
    Emitter<TourState> emit,
  ) async {
    final current = state;
    if (current is! TourLoaded) return;

    opBloc.add(StartOperation());
    try {
      final note = await tourRepository.addDeliveryNote(
        deliveryId: event.deliveryId,
        text: event.text,
        imageAttachment: event.imageAttachment,
        creditDeliveryItemId: event.creditDeliveryItemId,
      );
      final nextNotes = Map<String, List<DeliveryNote>>.from(
        current.details.notesByDeliveryId,
      );
      final existing = nextNotes[event.deliveryId] ?? const <DeliveryNote>[];
      // Backend liefert die Liste pro Lieferung nach `createdAt` sortiert —
      // wir hängen die neue Notiz hinten an, weil sie als jüngste Notiz
      // automatisch am Ende landet.
      nextNotes[event.deliveryId] = [...existing, note];
      emit(current.copyWith(
        details: current.details.copyWith(notesByDeliveryId: nextNotes),
      ));
      opBloc.add(FinishOperation());
    } catch (e, st) {
      debugPrint('TourBloc.AddDeliveryNote fehlgeschlagen: $e\n$st');
      opBloc.add(FailOperation(
        message: _messageOf(e, 'Notiz konnte nicht gespeichert werden'),
      ));
    }
  }

  // ─── UpdateDeliveryNote ──────────────────────────────────────────────

  Future<void> _onUpdateDeliveryNote(
    UpdateDeliveryNote event,
    Emitter<TourState> emit,
  ) async {
    final current = state;
    if (current is! TourLoaded) return;

    opBloc.add(StartOperation());
    try {
      final updated = await tourRepository.updateDeliveryNote(
        deliveryId: event.deliveryId,
        noteId: event.noteId,
        text: event.text,
        imageAttachment: event.imageAttachment,
      );
      final nextNotes = Map<String, List<DeliveryNote>>.from(
        current.details.notesByDeliveryId,
      );
      final list = List<DeliveryNote>.of(
        nextNotes[event.deliveryId] ?? const <DeliveryNote>[],
      );
      final idx = list.indexWhere((n) => n.id == event.noteId);
      if (idx != -1) {
        list[idx] = updated;
      }
      nextNotes[event.deliveryId] = list;
      emit(current.copyWith(
        details: current.details.copyWith(notesByDeliveryId: nextNotes),
      ));
      opBloc.add(FinishOperation());
    } catch (e, st) {
      debugPrint('TourBloc.UpdateDeliveryNote fehlgeschlagen: $e\n$st');
      opBloc.add(FailOperation(
        message: _messageOf(e, 'Notiz konnte nicht geändert werden'),
      ));
    }
  }

  // ─── DeleteDeliveryNote ──────────────────────────────────────────────

  Future<void> _onDeleteDeliveryNote(
    DeleteDeliveryNote event,
    Emitter<TourState> emit,
  ) async {
    final current = state;
    if (current is! TourLoaded) return;

    opBloc.add(StartOperation());
    try {
      await tourRepository.deleteDeliveryNote(
        deliveryId: event.deliveryId,
        noteId: event.noteId,
      );
      final nextNotes = Map<String, List<DeliveryNote>>.from(
        current.details.notesByDeliveryId,
      );
      final list = List<DeliveryNote>.of(
        nextNotes[event.deliveryId] ?? const <DeliveryNote>[],
      )..removeWhere((n) => n.id == event.noteId);
      if (list.isEmpty) {
        nextNotes.remove(event.deliveryId);
      } else {
        nextNotes[event.deliveryId] = list;
      }
      emit(current.copyWith(
        details: current.details.copyWith(notesByDeliveryId: nextNotes),
      ));
      opBloc.add(FinishOperation());
    } catch (e, st) {
      debugPrint('TourBloc.DeleteDeliveryNote fehlgeschlagen: $e\n$st');
      opBloc.add(FailOperation(
        message: _messageOf(e, 'Notiz konnte nicht gelöscht werden'),
      ));
    }
  }

  // ─── UploadDeliveryNoteImage ─────────────────────────────────────────

  Future<void> _onUploadDeliveryNoteImage(
    UploadDeliveryNoteImage event,
    Emitter<TourState> emit,
  ) async {
    final current = state;
    if (current is! TourLoaded) return;

    opBloc.add(StartOperation());
    try {
      final note = await tourRepository.uploadDeliveryNoteImage(
        deliveryId: event.deliveryId,
        filename: event.filename,
        mime: event.mime,
        bytes: event.bytes,
      );
      final nextNotes = Map<String, List<DeliveryNote>>.from(
        current.details.notesByDeliveryId,
      );
      final existing = nextNotes[event.deliveryId] ?? const <DeliveryNote>[];
      nextNotes[event.deliveryId] = [...existing, note];
      emit(current.copyWith(
        details: current.details.copyWith(notesByDeliveryId: nextNotes),
      ));
      opBloc.add(FinishOperation());
    } catch (e, st) {
      debugPrint('TourBloc.UploadDeliveryNoteImage fehlgeschlagen: $e\n$st');
      opBloc.add(FailOperation(
        message: _messageOf(e, 'Bild konnte nicht hochgeladen werden'),
      ));
    }
  }

  // ─── Helpers ─────────────────────────────────────────────────────────

  String _messageOf(Object e, String fallback) {
    if (e is TourRepositoryException) return e.message;
    return fallback;
  }
}
