import 'dart:typed_data';

import 'package:hl_lieferservice/dto/set_article_amount_response.dart';
import 'package:hl_lieferservice/feature/delivery/service/tour_service.dart';
import 'package:hl_lieferservice/model/delivery.dart';
import 'package:hl_lieferservice/model/tour.dart';
import 'package:rxdart/rxdart.dart';

import '../../../../dto/discount_add_response.dart';
import '../../../../dto/discount_remove_response.dart';
import '../../../../dto/discount_update_response.dart';
import '../../../../model/article.dart';
import '../detail/repository/note_repository.dart';
import '../detail/service/notes_service.dart';

enum ScanResult { scanned, alreadyScanned, notFound }

class TourNotFoundException implements Exception {}

class TourRepository {
  TourService service;

  final _tourStream = BehaviorSubject<Tour?>();
  final _paymentOptionsStream = BehaviorSubject<List<Payment>>.seeded([]);

  Stream<Tour?> get tour => _tourStream.stream;

  Stream<List<Payment>> get paymentOptions => _paymentOptionsStream.stream;

  TourRepository({required this.service});

  Future<void> loadTourOfToday(String userId) async {
    _tourStream.add(await service.getTourOfToday(userId));
  }

  Future<void> loadPaymentOptions() async {
    _paymentOptionsStream.add(
      (await service.getPaymentMethods())
          .map((option) => Payment.fromDTO(option))
          .toList(),
    );
  }

  Future<void> assignCar(String deliveryId, String carId) async {
    await service.assignCar(deliveryId, carId);

    final tour = _tourStream.value!;
    final index = tour.deliveries.indexWhere(
      (delivery) => delivery.id == deliveryId,
    );
    tour.deliveries[index].carId = int.parse(carId);

    _tourStream.add(tour);
  }

  Future<ScanResult> scanArticle(
    String deliveryId,
    String carId,
    String articleNumber,
  ) async {
    if (!_tourStream.hasValue) {
      throw TourNotFoundException();
    }

    final tour = _tourStream.value!;

    if (tour.deliveries.any(
      (delivery) => delivery.articles.any(
        (article) => article.articleNumber == articleNumber,
      ),
    )) {
      var delivery = tour.deliveries.firstWhere(
        (delivery) => delivery.id == deliveryId,
      );
      var article = delivery.articles.firstWhere(
        (article) => article.articleNumber == articleNumber,
      );

      await service.scanArticle(article.internalId.toString());

      if (article.scannedAmount < article.amount) {
        article.scannedAmount += 1;
        delivery.carId = int.tryParse(carId) ?? delivery.carId;
        await service.assignCar(deliveryId, carId);
        _tourStream.add(tour);
        return ScanResult.scanned;
      } else {
        return ScanResult.alreadyScanned;
      }
    } else {
      return ScanResult.notFound;
    }
  }

  /// Scan a single BOM component locally.  The server-side `scanArticle` call
  /// for the parent article is deferred until **every** component of the
  /// parent is fully scanned — only then does the parent count as loaded.
  Future<ScanResult> scanComponent(
    String deliveryId,
    String carId,
    String componentArticleNumber,
  ) async {
    if (!_tourStream.hasValue) {
      throw TourNotFoundException();
    }

    final tour = _tourStream.value!;
    final delivery = tour.deliveries.firstWhere(
      (d) => d.id == deliveryId,
    );

    // Locate the parent article and the matching component.
    final parentArticle = delivery.findParentOfComponent(
      componentArticleNumber,
    );
    if (parentArticle == null) return ScanResult.notFound;

    final component = parentArticle.findComponent(componentArticleNumber)!;

    if (component.isFullyScanned) return ScanResult.alreadyScanned;

    // ── Local-only increment ──
    component.scannedAmount += 1;

    // ── When every component is done, sync the parent with the server ──
    if (parentArticle.isFullyScanned) {
      await service.scanArticle(parentArticle.internalId.toString());
      parentArticle.scannedAmount += 1;
      delivery.carId = int.tryParse(carId) ?? delivery.carId;
      await service.assignCar(deliveryId, carId);
    }

    _tourStream.add(tour);
    return ScanResult.scanned;
  }

  Future<void> unscan(
    String deliveryId,
    String articleId,
    int newAmount,
    String reason,
  ) async {
    if (!_tourStream.hasValue) {
      throw TourNotFoundException();
    }

    Tour tour = _tourStream.value!;
    String? noteId = await service.unscanArticle(articleId, newAmount, reason);
    Article article = tour.deliveries
        .firstWhere((delivery) => delivery.id == deliveryId)
        .articles
        .firstWhere((article) => article.internalId == int.parse(articleId));

    article.removeNoteId = noteId;
    article.scannedRemovedAmount += newAmount;
    article.scannedAmount -= newAmount;

    _tourStream.add(tour);
  }

  Future<void> resetScan(String articleId, String deliveryId) async {
    if (!_tourStream.hasValue) {
      throw TourNotFoundException();
    }

    Tour tour = _tourStream.value!;
    await service.resetScannedArticleAmount(articleId);

    Article article = tour.deliveries
        .firstWhere((delivery) => delivery.id == deliveryId)
        .articles
        .firstWhere((article) => article.internalId == int.parse(articleId));

    article.removeNoteId = null;
    article.scannedRemovedAmount = 0;
    article.scannedAmount = article.amount;

    _tourStream.add(tour);
  }

  Future<void> uploadDriverSignature(
    String deliveryId,
    Uint8List signature,
  ) async {
    NoteRepository noteRepository = NoteRepository(service: NoteService());
    await noteRepository.addNamedImage(
      deliveryId,
      signature,
      "delivery_${deliveryId}_signature_driver.jpg",
    );
  }

  Future<void> uploadCustomerSignature(
    String deliveryId,
    Uint8List signature,
  ) async {
    NoteRepository noteRepository = NoteRepository(service: NoteService());
    await noteRepository.addNamedImage(
      deliveryId,
      signature,
      "delivery_${deliveryId}_signature_customer.jpg",
    );
  }

  Future<void> addDiscount(String deliveryId, String reason, int value) async {
    if (!_tourStream.hasValue) {
      throw TourNotFoundException();
    }

    Tour tour = _tourStream.value!;
    DiscountAddResponseDTO response = await service.addDiscount(
      deliveryId,
      value,
      reason,
    );

    Delivery delivery = tour.deliveries.firstWhere(
      (delivery) => delivery.id == deliveryId,
    );
    Article discountArticle = Article.fromDTO(response.values.article);
    delivery.totalNetValue = response.values.receipt.net;
    delivery.totalGrossValue = response.values.receipt.gross;

    delivery.discount = Discount(
      article: Article.fromDTO(response.values.article),
      note: response.values.note.noteDescription,
      noteId: response.values.note.rowId,
    );
    delivery.articles = [
      ...delivery.articles,
      discountArticle,
    ];

    _tourStream.add(tour);
  }

  Future<void> removeDiscount(String deliveryId) async {
    if (!_tourStream.hasValue) {
      throw TourNotFoundException();
    }

    Tour tour = _tourStream.value!;
    DiscountRemoveResponseDTO response = await service.removeDiscount(
      deliveryId,
    );

    Delivery delivery = tour.deliveries.firstWhere(
      (delivery) => delivery.id == deliveryId,
    );
    delivery.articles =
        delivery.articles
            .where(
              (article) =>
                  article.internalId != delivery.discount?.article.internalId,
            )
            .toList();

    delivery.discount = null;
    delivery.totalGrossValue = response.receipt.gross;
    delivery.totalNetValue = response.receipt.net;

    _tourStream.add(tour);
  }

  Future<void> updateDiscount(
    String deliveryId,
    String? reason,
    int? value,
  ) async {
    if (!_tourStream.hasValue) {
      throw TourNotFoundException();
    }

    Tour tour = _tourStream.value!;
    DiscountUpdateResponseDTO response = await service.updateDiscount(
      deliveryId,
      reason,
      value,
    );

    Delivery delivery = tour.deliveries.firstWhere(
      (delivery) => delivery.id == deliveryId,
    );
    if (response.values?.receipt != null) {
      delivery.totalNetValue = response.values!.receipt.net;
      delivery.totalGrossValue = response.values!.receipt.gross;
    }

    String discountArticleNumber = delivery.discount!.article.articleNumber;
    delivery.discount = Discount(
      article:
          response.values?.article != null
              ? Article.fromDTO(response.values!.article)
              : delivery.discount!.article,
      note:
          response.values?.note != null
              ? response.values!.note.noteDescription
              : delivery.discount!.note,
      noteId:
          response.values?.note != null
              ? response.values!.note.rowId
              : delivery.discount!.noteId,
    );

    delivery.articles = [
      ...delivery.articles.where(
        (article) => article.articleNumber != discountArticleNumber,
      ),
      delivery.discount!.article,
    ];

    _tourStream.add(tour);
  }

  Future<void> reactivateDelivery(String deliveryId) async {
    await _changeState(deliveryId, DeliveryState.ongoing);
  }

  Future<void> holdDelivery(String deliveryId) async {
    await _changeState(deliveryId, DeliveryState.onhold);
  }

  Future<void> cancelDelivery(String deliveryId) async {
    await _changeState(deliveryId, DeliveryState.canceled);
  }

  Future<void> finishDelivery(String deliveryId) async {
    await _changeState(deliveryId, DeliveryState.finished);
    Delivery delivery = _tourStream.value!.deliveries.firstWhere(
      (delivery) => delivery.id == deliveryId,
    );

    await _updateDelivery(delivery);
    await service.finishDelivery(deliveryId);
  }

  Future<void> setArticleAmount(
    String deliveryId,
    String articleId,
    int amount,
    String? reason,
  ) async {
    if (!_tourStream.hasValue) {
      throw TourNotFoundException();
    }

    try {
      SetArticleAmountResponseDTO dto = await service.setArticleAmount(
        deliveryId,
        articleId,
        amount,
        reason,
      );

      Delivery delivery = _tourStream.value!.deliveries.firstWhere(
        (delivery) => delivery.id == deliveryId,
      );
      Article article = delivery.articles.firstWhere(
        (article) => article.internalId == int.parse(articleId),
      );

      article.amount = amount;
      article.removeNoteId = dto.noteId;
      _tourStream.add(_tourStream.value);
    } catch (_) {
      rethrow;
    }
  }

  Future<void> _changeState(String deliveryId, DeliveryState state) async {
    if (!_tourStream.hasValue) {
      throw TourNotFoundException();
    }

    Tour tour = _tourStream.value!;
    Delivery delivery = tour.deliveries.firstWhere(
      (delivery) => delivery.id == deliveryId,
    );

    delivery.state = state;
    await _updateDelivery(delivery);

    _tourStream.add(tour);
  }

  Future<void> _updateDelivery(Delivery newDelivery) async {
    if (!_tourStream.hasValue) {
      throw TourNotFoundException();
    }

    await service.updateDelivery(newDelivery);
  }

  Future<void> updatePayment(String deliveryId, Payment payment) async {
    if (!_tourStream.hasValue) {
      throw TourNotFoundException();
    }

    Tour tour = _tourStream.value!;
    Delivery delivery = tour.deliveries.firstWhere(
      (delivery) => delivery.id == deliveryId,
    );

    delivery.payment = payment;
    await _updateDelivery(delivery);

    _tourStream.add(tour);
  }

  Future<void> updateOption(
    String deliveryId,
    String key,
    dynamic value,
  ) async {
    if (!_tourStream.hasValue) {
      throw TourNotFoundException();
    }

    Tour tour = _tourStream.value!;
    Delivery delivery = tour.deliveries.firstWhere(
      (delivery) => delivery.id == deliveryId,
    );

    delivery.options =
        delivery.options.map((option) {
          if (option.key == key) {
            if (option.numerical) {
              return option.copyWith(value: value);
            } else {
              return option.copyWith(value: value == true ? "1" : "0");
            }
          }

          return option;
        }).toList();

    await _updateDelivery(delivery);

    _tourStream.add(tour);
  }
}
