import 'dart:convert';

import 'package:dio/dio.dart';
import 'package:holzleitner_api/holzleitner_api.dart' as api;

import 'package:hl_lieferservice/data/mapper/tour_mapper.dart';
import 'package:hl_lieferservice/domain/entity/address.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_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/tour.dart';
import 'package:hl_lieferservice/domain/entity/tour_details.dart';
import 'package:hl_lieferservice/domain/repository/tour_repository.dart';

/// Implementierung gegen den generierten Dio-Client. Übersetzt
/// `DioException` in [TourRepositoryException]. 401 lassen wir
/// ungefangen durchfliegen — der TokenProvider erkennt 401 separat
/// und meldet `AuthSessionExpired`.
class TourRepositoryImpl implements TourRepository {
  TourRepositoryImpl(this._api);

  final api.HolzleitnerApi _api;

  @override
  Future<TourSummary?> getMyTourSummaryOfToday() async {
    try {
      final response = await _api.getToursApi().listMyToursToday();
      final tours = response.data?.tours;
      if (tours == null || tours.isEmpty) return null;
      // Backend liefert die Liste sortiert; wir nehmen die erste Tour.
      // Der Fahrer hat aktuell nur eine Tour pro Tag — falls sich das
      // ändert, wird hier eine Auswahl-UI nötig.
      return tours.first.toDomain();
    } on DioException catch (e) {
      throw TourRepositoryException(_describe(e, 'Laden der Tour-Übersicht'), e);
    }
  }

  @override
  Future<TourDetails> getTourDetails(String tourId) async {
    try {
      final response = await _api.getToursApi().getTour(tourId: tourId);
      final details = response.data;
      if (details == null) {
        throw const TourRepositoryException(
          'Server lieferte leere Tour-Antwort',
        );
      }
      return details.toDomain();
    } on DioException catch (e) {
      throw TourRepositoryException(_describe(e, 'Laden der Tour'), e);
    }
  }

  @override
  Future<TourDetails?> getMyTourDetailsOfToday() async {
    final summary = await getMyTourSummaryOfToday();
    if (summary == null) return null;
    return getTourDetails(summary.tourId);
  }

  @override
  Future<Map<String, int>> setDeliveryOrder({
    required String tourId,
    required List<String> orderedDeliveryIds,
  }) async {
    try {
      final request = api.SetDeliveryOrderRequest((b) {
        b.deliveryIds.replace(orderedDeliveryIds);
      });
      final response = await _api.getToursApi().setDeliveryOrder(
            tourId: tourId,
            setDeliveryOrderRequest: request,
          );
      final order = response.data?.order;
      if (order == null) {
        throw const TourRepositoryException(
          'Server lieferte leere Reihenfolge-Antwort',
        );
      }
      return {for (final e in order) e.deliveryId: e.sortOrder};
    } on DioException catch (e) {
      throw TourRepositoryException(_describe(e, 'Speichern der Reihenfolge'), e);
    }
  }

  @override
  Future<Delivery> assignCarToDelivery({
    required String deliveryId,
    required String? carId,
  }) async {
    try {
      final request = api.AssignCarRequest((b) {
        // `null` ⇒ Backend hebt die Zuweisung auf. built_value lässt das
        // im Wire als `"carId": null` durchgehen — vom OpenAPI-Schema so
        // gewollt.
        b.carId = carId;
      });
      final response = await _api.getDeliveriesApi().assignCar(
            deliveryId: deliveryId,
            assignCarRequest: request,
          );
      final delivery = response.data?.delivery;
      if (delivery == null) {
        throw const TourRepositoryException(
          'Server lieferte leere Delivery-Antwort',
        );
      }
      // Achtung: Der Endpoint gibt `Delivery` *ohne* Items zurück.
      // Wir bauen hier eine Domain-Delivery mit leerer Item-Liste —
      // der Bloc muss die echten Items aus dem lokalen Aggregat mergen.
      return Delivery(
        id: delivery.id,
        tourId: delivery.tourId,
        customerId: delivery.customerId,
        contactPersonIds: delivery.contactPersonIds.toList(growable: false),
        deliveryAddressSnapshot: delivery.deliveryAddressSnapshot.toDomain(),
        erpBelegartId: delivery.erpBelegartId,
        erpBelegnummer: delivery.erpBelegnummer,
        state: delivery.state.toDomain(),
        stateReason: delivery.stateReason,
        // Stamm-Endpoint kennt `sortOrder` nicht — Bloc behält den Wert.
        sortOrder: 0,
        assignedCarId: delivery.assignedCarId,
        desiredTime: delivery.desiredTime,
        specialAgreements: delivery.specialAgreements,
        items: const [],
        prepaidAmount: delivery.prepaidAmount,
        paymentMethodId: delivery.paymentMethodId,
      );
    } on DioException catch (e) {
      throw TourRepositoryException(_describe(e, 'Fahrzeug-Zuweisung'), e);
    }
  }

  @override
  Future<Delivery> cancelDelivery({
    required String deliveryId,
    required String reason,
  }) async {
    try {
      final request = api.CancelDeliveryRequest((b) => b..reason = reason);
      final response = await _api.getDeliveriesApi().cancel(
            deliveryId: deliveryId,
            cancelDeliveryRequest: request,
          );
      return _liftDeliveryStub(response.data?.delivery, 'Abbruch');
    } on DioException catch (e) {
      throw TourRepositoryException(_describe(e, 'Lieferung abbrechen'), e);
    }
  }

  @override
  Future<Delivery> holdDelivery({
    required String deliveryId,
    required String reason,
  }) async {
    try {
      final request = api.HoldDeliveryRequest((b) => b..reason = reason);
      final response = await _api.getDeliveriesApi().hold(
            deliveryId: deliveryId,
            holdDeliveryRequest: request,
          );
      return _liftDeliveryStub(response.data?.delivery, 'Pausieren');
    } on DioException catch (e) {
      throw TourRepositoryException(_describe(e, 'Lieferung pausieren'), e);
    }
  }

  @override
  Future<Delivery> resumeDelivery({required String deliveryId}) async {
    try {
      final response =
          await _api.getDeliveriesApi().resume(deliveryId: deliveryId);
      return _liftDeliveryStub(response.data?.delivery, 'Fortsetzen');
    } on DioException catch (e) {
      throw TourRepositoryException(_describe(e, 'Lieferung fortsetzen'), e);
    }
  }

  @override
  Future<Delivery> completeDelivery({
    required String deliveryId,
    required List<int> customerSignaturePng,
    required List<int> driverSignaturePng,
    required bool receiptConfirmed,
    required bool notesAcknowledged,
    required List<String> acknowledgedNoteIds,
    String? paymentMethodId,
    String? actorCarId,
    bool paymentCollected = false,
  }) async {
    // multipart/form-data: zwei Signatur-PNGs + ein JSON-Feld mit den
    // Bestätigungen. Direkt über die Dio-Instanz, weil der dart-dio-Generator
    // für multipart keinen typisierten Body erzeugt (wie beim Bild-Upload).
    try {
      final acknowledgements = <String, dynamic>{
        'receiptConfirmed': receiptConfirmed,
        'notesAcknowledged': notesAcknowledged,
        'acknowledgedNoteIds': acknowledgedNoteIds,
        'paymentCollected': paymentCollected,
        if (paymentMethodId != null) 'paymentMethodId': paymentMethodId,
        if (actorCarId != null) 'authorCarId': actorCarId,
      };
      final form = FormData.fromMap({
        'customer_signature': MultipartFile.fromBytes(
          customerSignaturePng,
          filename: 'customer_signature.png',
          contentType: DioMediaType.parse('image/png'),
        ),
        'driver_signature': MultipartFile.fromBytes(
          driverSignaturePng,
          filename: 'driver_signature.png',
          contentType: DioMediaType.parse('image/png'),
        ),
        'acknowledgements': jsonEncode(acknowledgements),
      });
      final response = await _api.dio.post<Map<String, dynamic>>(
        '/deliveries/$deliveryId/complete',
        data: form,
      );
      final delivery = response.data?['delivery'] as Map<String, dynamic>?;
      if (delivery == null) {
        throw const TourRepositoryException(
          'Server lieferte leere Delivery-Antwort beim Abschließen',
        );
      }
      return _deliveryStubFromJson(delivery);
    } on DioException catch (e) {
      throw TourRepositoryException(_describe(e, 'Lieferung abschließen'), e);
    }
  }

  /// Hebt einen Delivery-Stub (Stamm-Endpoint-Response **ohne** Items)
  /// in die Domain. Aufrufer muss anschließend `copyWith(items: ..., sortOrder: ...)`
  /// aus dem lokalen Aggregat mergen — der Bloc-Handler kümmert sich darum.
  Delivery _liftDeliveryStub(api.Delivery? stub, String operation) {
    if (stub == null) {
      throw TourRepositoryException(
        'Server lieferte leere Delivery-Antwort beim $operation',
      );
    }
    return Delivery(
      id: stub.id,
      tourId: stub.tourId,
      customerId: stub.customerId,
      contactPersonIds: stub.contactPersonIds.toList(growable: false),
      deliveryAddressSnapshot: stub.deliveryAddressSnapshot.toDomain(),
      erpBelegartId: stub.erpBelegartId,
      erpBelegnummer: stub.erpBelegnummer,
      state: stub.state.toDomain(),
      stateReason: stub.stateReason,
      sortOrder: 0,
      assignedCarId: stub.assignedCarId,
      desiredTime: stub.desiredTime,
      specialAgreements: stub.specialAgreements,
      items: const [],
      prepaidAmount: stub.prepaidAmount,
      paymentMethodId: stub.paymentMethodId,
    );
  }

  @override
  Future<DeliveryNote> addDeliveryNote({
    required String deliveryId,
    String? text,
    String? imageAttachment,
    String? creditDeliveryItemId,
    bool isAmountCreditNote = false,
  }) async {
    try {
      final request = api.CreateDeliveryNoteRequest((b) {
        if (text != null) b.text = text;
        if (imageAttachment != null) b.imageAttachment = imageAttachment;
        if (creditDeliveryItemId != null) {
          b.creditDeliveryItemId = creditDeliveryItemId;
        }
        b.isAmountCreditNote = isAmountCreditNote;
      });
      final response = await _api.getDeliveriesApi().createNote(
            deliveryId: deliveryId,
            createDeliveryNoteRequest: request,
          );
      final note = response.data?.note;
      if (note == null) {
        throw const TourRepositoryException(
          'Server lieferte leere Notiz-Antwort',
        );
      }
      return note.toDomain();
    } on DioException catch (e) {
      throw TourRepositoryException(_describe(e, 'Notiz anlegen'), e);
    }
  }

  @override
  Future<DeliveryNote> updateDeliveryNote({
    required String deliveryId,
    required String noteId,
    String? text,
    String? imageAttachment,
  }) async {
    try {
      final request = api.UpdateDeliveryNoteRequest((b) {
        if (text != null) b.text = text;
        if (imageAttachment != null) b.imageAttachment = imageAttachment;
      });
      final response = await _api.getDeliveriesApi().updateNote(
            deliveryId: deliveryId,
            noteId: noteId,
            updateDeliveryNoteRequest: request,
          );
      final note = response.data?.note;
      if (note == null) {
        throw const TourRepositoryException(
          'Server lieferte leere Notiz-Antwort',
        );
      }
      return note.toDomain();
    } on DioException catch (e) {
      throw TourRepositoryException(_describe(e, 'Notiz aktualisieren'), e);
    }
  }

  @override
  Future<void> deleteDeliveryNote({
    required String deliveryId,
    required String noteId,
  }) async {
    try {
      await _api.getDeliveriesApi().deleteNote(
            deliveryId: deliveryId,
            noteId: noteId,
          );
    } on DioException catch (e) {
      throw TourRepositoryException(_describe(e, 'Notiz löschen'), e);
    }
  }

  @override
  Future<DeliveryNote> uploadDeliveryNoteImage({
    required String deliveryId,
    required String filename,
    required String mime,
    required List<int> bytes,
  }) async {
    // Bewusst direkt über die Dio-Instanz statt über den generierten Client:
    // der dart-dio-Generator erzeugt für multipart/form-data keinen
    // typisierten Body-Parameter. Der `HolzleitnerAuthInterceptor` an der
    // Dio-Instanz hängt den Bearer-Token automatisch an.
    try {
      final form = FormData.fromMap({
        'file': MultipartFile.fromBytes(
          bytes,
          filename: filename,
          contentType: DioMediaType.parse(mime),
        ),
      });
      final response = await _api.dio.post<Map<String, dynamic>>(
        '/deliveries/$deliveryId/notes/image',
        data: form,
      );
      final note = (response.data?['note']) as Map<String, dynamic>?;
      if (note == null) {
        throw const TourRepositoryException(
          'Server lieferte leere Notiz-Antwort beim Bild-Upload',
        );
      }
      return _noteFromJson(note);
    } on DioException catch (e) {
      throw TourRepositoryException(_describe(e, 'Bild hochladen'), e);
    }
  }

  @override
  Future<DeliveryCredit?> setDeliveryCredit({
    required String deliveryId,
    required String clientEventId,
    required int amountCents,
    required String reason,
    String? actorCarId,
  }) async {
    try {
      final request = api.DeliveryCreditEventRequest((b) {
        b
          ..clientEventId = clientEventId
          ..action = api.CreditAction.set_
          ..amountCents = amountCents
          ..reason = reason;
        if (actorCarId != null) b.authorCarId = actorCarId;
      });
      final response = await _api.getDeliveriesApi().applyCredit(
            deliveryId: deliveryId,
            deliveryCreditEventRequest: request,
          );
      return response.data?.credit?.toDomain();
    } on DioException catch (e) {
      throw TourRepositoryException(_describe(e, 'Gutschrift setzen'), e);
    }
  }

  @override
  Future<DeliveryCredit?> removeDeliveryCredit({
    required String deliveryId,
    required String clientEventId,
    String? actorCarId,
  }) async {
    try {
      final request = api.DeliveryCreditEventRequest((b) {
        b
          ..clientEventId = clientEventId
          ..action = api.CreditAction.remove;
        if (actorCarId != null) b.authorCarId = actorCarId;
      });
      final response = await _api.getDeliveriesApi().applyCredit(
            deliveryId: deliveryId,
            deliveryCreditEventRequest: request,
          );
      return response.data?.credit?.toDomain();
    } on DioException catch (e) {
      throw TourRepositoryException(_describe(e, 'Gutschrift entfernen'), e);
    }
  }

  @override
  Future<DeliveryServiceValue> setDeliveryService({
    required String deliveryId,
    required String serviceId,
    bool? boolValue,
    int? numericValue,
    String? actorCarId,
  }) async {
    try {
      final request = api.SetDeliveryServiceRequest((b) {
        if (boolValue != null) b.boolValue = boolValue;
        if (numericValue != null) b.numericValue = numericValue;
        if (actorCarId != null) b.authorCarId = actorCarId;
      });
      final response = await _api.getDeliveriesApi().setService(
            deliveryId: deliveryId,
            serviceId: serviceId,
            setDeliveryServiceRequest: request,
          );
      final value = response.data?.value;
      if (value == null) {
        throw const TourRepositoryException(
          'Server lieferte leeren Service-Wert',
        );
      }
      return value.toDomain();
    } on DioException catch (e) {
      throw TourRepositoryException(_describe(e, 'Service setzen'), e);
    }
  }

  @override
  Future<void> removeDeliveryService({
    required String deliveryId,
    required String serviceId,
  }) async {
    try {
      await _api.getDeliveriesApi().deleteServiceValue(
            deliveryId: deliveryId,
            serviceId: serviceId,
          );
    } on DioException catch (e) {
      throw TourRepositoryException(_describe(e, 'Service entfernen'), e);
    }
  }

  /// Mappt das rohe Note-JSON (camelCase) der Upload-Antwort in die Domain.
  /// Eigene Mini-Deserialisierung, weil dieser Pfad nicht über den
  /// generierten Client (mit built_value) läuft.
  /// Baut aus der rohen JSON-Map (Stamm-Endpoint-Response **ohne** Items)
  /// eine Domain-Delivery. Wie [_liftDeliveryStub], aber für die direkten
  /// Dio-Calls (multipart), die keinen typisierten Body liefern. Aufrufer
  /// merged Items/sortOrder aus dem lokalen Aggregat.
  Delivery _deliveryStubFromJson(Map<String, dynamic> j) {
    final snap = j['deliveryAddressSnapshot'] as Map<String, dynamic>;
    return Delivery(
      id: j['id'] as String,
      tourId: j['tourId'] as String,
      customerId: j['customerId'] as String,
      contactPersonIds:
          (j['contactPersonIds'] as List).cast<String>().toList(growable: false),
      deliveryAddressSnapshot: Address(
        street: snap['street'] as String,
        houseNumber: snap['houseNumber'] as String,
        postalCode: snap['postalCode'] as String,
        city: snap['city'] as String,
        country: snap['country'] as String,
      ),
      erpBelegartId: (j['erpBelegartId'] as num).toInt(),
      erpBelegnummer: j['erpBelegnummer'] as String,
      state: _deliveryStateFromWire(j['state'] as String),
      stateReason: j['stateReason'] as String?,
      sortOrder: 0,
      assignedCarId: j['assignedCarId'] as String?,
      desiredTime: j['desiredTime'] as String?,
      specialAgreements: j['specialAgreements'] as String?,
      items: const [],
      prepaidAmount: (j['prepaidAmount'] as num).toDouble(),
      paymentMethodId: j['paymentMethodId'] as String,
    );
  }

  DeliveryState _deliveryStateFromWire(String value) {
    switch (value) {
      case 'active':
        return DeliveryState.active;
      case 'held':
        return DeliveryState.held;
      case 'canceled':
        return DeliveryState.canceled;
      case 'completed':
        return DeliveryState.completed;
      default:
        throw TourRepositoryException('Unbekannter DeliveryState: $value');
    }
  }

  DeliveryNote _noteFromJson(Map<String, dynamic> j) => DeliveryNote(
        id: j['id'] as String,
        deliveryId: j['deliveryId'] as String,
        text: j['text'] as String?,
        imageAttachment: j['imageAttachment'] as String?,
        authorPersonalnummer: (j['authorPersonalnummer'] as num).toInt(),
        authorCarId: j['authorCarId'] as String?,
        creditDeliveryItemId: j['creditDeliveryItemId'] as String?,
        isAmountCreditNote: (j['isAmountCreditNote'] as bool?) ?? false,
        createdAt: DateTime.parse(j['createdAt'] as String),
      );

  @override
  Future<Map<String, ScanOutcome>> applyScans(List<ScanIntent> intents) async {
    if (intents.isEmpty) return const {};
    try {
      final request = api.ApplyScansRequest((b) {
        b.scans.replace(intents.map((i) => i.toWire()));
      });
      final response = await _api.getScansApi().applyScans(
            applyScansRequest: request,
          );
      final results = response.data?.results;
      if (results == null) {
        throw const TourRepositoryException(
          'Server lieferte leere Scan-Antwort',
        );
      }
      return {for (final r in results) r.clientScanId: r.toDomain()};
    } on DioException catch (e) {
      throw TourRepositoryException(_describe(e, 'Scans anwenden'), e);
    }
  }

  String _describe(DioException e, String operation) {
    final status = e.response?.statusCode;
    final body = e.response?.data;
    if (status == 400 && body is Map && body['message'] != null) {
      return '$operation fehlgeschlagen: ${body['message']}';
    }
    if (status == 401) return 'Sitzung abgelaufen';
    if (status == 403) return 'Keine Berechtigung';
    if (status == 404) return 'Tour oder Lieferung nicht gefunden';
    return '$operation fehlgeschlagen (HTTP ${status ?? 'unbekannt'})';
  }
}
