import 'dart:convert';
import 'dart:math';

import 'package:drift/drift.dart';
import 'package:intl/intl.dart';
import 'package:niogu_app/core/database/app_database.dart'
    hide Product, OrderItem;
import 'package:niogu_app/core/database/tables/customer_addresses.dart';
import 'package:niogu_app/core/database/tables/customers.dart';
import 'package:niogu_app/core/database/tables/order_items.dart';
import 'package:niogu_app/core/database/tables/orders.dart';
import 'package:niogu_app/core/database/tables/outlet_inventories.dart';
import 'package:niogu_app/core/database/tables/product_variants.dart';
import 'package:niogu_app/core/database/tables/products.dart';
import 'package:niogu_app/core/database/tables/raw_materials.dart';
import 'package:niogu_app/core/database/tables/receipt_items.dart';
import 'package:niogu_app/core/database/tables/stock_cards.dart';
import 'package:niogu_app/core/database/tables/sync_queues.dart';
import 'package:niogu_app/core/database/tables/transactions.dart';
import 'package:niogu_app/core/database/tables/units.dart';
import 'package:niogu_app/core/entities/custom_activity.dart';
import 'package:niogu_app/core/entities/custom_sync_queue.dart';
import 'package:niogu_app/core/enums/approval_process.dart';
import 'package:niogu_app/core/enums/canceled_by.dart';
import 'package:niogu_app/core/enums/table_type.dart';
import 'package:niogu_app/core/utils/currency_format.dart';
import 'package:niogu_app/core/enums/delivery_type.dart';
import 'package:niogu_app/core/utils/extension_format.dart';
import 'package:niogu_app/core/enums/item_type.dart';
import 'package:niogu_app/core/utils/log_message.dart';
import 'package:niogu_app/core/enums/message_activity_type.dart';
import 'package:niogu_app/core/enums/order_source.dart';
import 'package:niogu_app/core/enums/order_status.dart';
import 'package:niogu_app/core/enums/stock_card_type.dart';
import 'package:niogu_app/core/enums/stock_type.dart';
import 'package:niogu_app/core/enums/sync_status.dart';
import 'package:niogu_app/core/system/system_setting.dart';
import 'package:niogu_app/core/enums/transaction_reference_type.dart';
import 'package:niogu_app/core/enums/user_role.dart';
import 'package:niogu_app/features/goods/products/domain/entities/product.dart';
import 'package:niogu_app/features/goods/raw_materials/domain/entities/raw_material.dart';
import 'package:niogu_app/features/online_store/domain/entities/online_store.dart';
import 'package:niogu_app/features/pos/domain/entities/pos.dart';
import 'package:niogu_app/features/report/transaction/domain/entities/transaction_report.dart';
import 'package:uuid/uuid.dart';

part 'order_dao.g.dart';

@DriftAccessor(
  tables: [
    Customers,
    CustomerAddresses,
    Products,
    Units,
    RawMaterials,
    ProductVariants,
    OutletInventories,
    Orders,
    OrderItems,
    ReceiptItems,
    StockCards,
    Transactions,
    SyncQueues,
  ],
)
class OrderDao extends DatabaseAccessor<AppDatabase> with _$OrderDaoMixin {
  OrderDao(AppDatabase db) : super(db);

  Stream<List<DisplayProductPos>> watchAllDisplayProductPos() async* {
    final outletId = await SystemSetting.getCurrentOutletId();

    final query =
        selectOnly(products).join([
            innerJoin(units, units.localId.equalsExp(products.unitId)),
            innerJoin(
              productVariants,
              productVariants.productId.equalsExp(products.localId),
            ),
            innerJoin(
              outletInventories,
              outletInventories.itemType.equals(ItemType.product_variant.type) &
                  outletInventories.itemId.equalsExp(productVariants.localId),
            ),
          ])
          ..addColumns([
            products.localId,
            productVariants.localId,
            outletInventories.localId,
            products.localImagePath,
            productVariants.localImagePath,
            products.name,
            productVariants.name,
            productVariants.totalSold,
            outletInventories.stockType,
            outletInventories.stock,
            units.name,
            outletInventories.costPrice,
            outletInventories.sellingPrice,
            products.hasVariant,
          ])
          ..orderBy([OrderingTerm.desc(productVariants.totalSold)])
          ..where(products.deletedAt.isNull())
          ..where(productVariants.deletedAt.isNull())
          ..where(
            outletInventories.outletId.equals(outletId!) &
                outletInventories.deletedAt.isNull(),
          );

    yield* query.watch().map((rows) {
      return rows.map((row) {
        final String productId = row.read(products.localId) ?? '';
        final String productVariantId = row.read(productVariants.localId) ?? '';
        final String outletInventoryId =
            row.read(outletInventories.localId) ?? '';
        final dynamic rawImages = row.read(products.localImagePath);

        List<String> productImages = [];

        if (rawImages is String) {
          try {
            productImages = (jsonDecode(rawImages) as List).cast<String>();
          } catch (e) {
            productImages = [];
          }
        } else if (rawImages is List) {
          productImages = (rawImages).cast<String>();
        }

        final bool hasVariant = row.read(products.hasVariant) ?? false;
        final String? imagePath = hasVariant
            ? row.read(productVariants.localImagePath)
            : productImages.lastOrNull;
        final String name = row.read(products.name) ?? '';
        final String variantName = row.read(productVariants.name) ?? '';
        final double currentSold = row.read(productVariants.totalSold) ?? 0.0;
        final StockType stockType = StockType.values.byName(
          (row.read(outletInventories.stockType) ?? ''),
        );
        final double remainingStock = row.read(outletInventories.stock) ?? 0.0;
        final String unitName = row.read(units.name) ?? '';
        final double costPrice = row.read(outletInventories.costPrice) ?? 0.0;
        final double sellingPrice =
            row.read(outletInventories.sellingPrice) ?? 0.0;

        return DisplayProductPos(
          productId: productId,
          productVariantId: productVariantId,
          outletInventoryId: outletInventoryId,
          imagePath: imagePath,
          name: name,
          variantName: variantName,
          currentSold: currentSold,
          stockType: stockType,
          remainingStock: remainingStock,
          unitName: unitName,
          costPrice: costPrice,
          sellingPrice: sellingPrice,
          hasVariant: hasVariant,
        );
      }).toList();
    });
  }

  Future<String> _generateOrderNumber() async {
    final now = DateTime.now();

    final formatter = DateFormat('yyMMdd');

    final dateCode = formatter.format(now);

    final tenantCode = await SystemSetting.getTenantCode();

    final chars = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789';

    final randomPart = List.generate(
      3,
      (index) => chars[Random().nextInt(chars.length)],
    ).join();

    final prefix = 'INVC-$tenantCode-$dateCode-';

    final lastOrder =
        await (select(orders)
              ..where((t) => t.orderNumber.like('$prefix%'))
              ..orderBy([(t) => OrderingTerm.desc(t.orderNumber)])
              ..limit(1))
            .getSingleOrNull();

    int nextSequence = 1;

    if (lastOrder != null) {
      final parts = lastOrder.orderNumber.split('-');
      if (parts.isNotEmpty) {
        final lastPart = parts.last;
        if (lastPart.length >= 5) {
          final lastSeqStr = lastPart.substring(lastPart.length - 5);
          nextSequence = int.parse(lastSeqStr) + 1;
        }
      }
    }

    // Format baru: INVC - [TenantCode] - [Tanggal] - [Random3] [Urutan5]
    // Contoh: INVC-ZL6B25KQ-260328-XYZ00001
    final finalSequence =
        '$randomPart${nextSequence.toString().padLeft(5, '0')}';

    return '$prefix$finalSequence';
  }

  Future<List<UpdateStockRawMaterial>> _getProductUseMaterials(
    String productVariantId,
    double productQuantity,
  ) async {
    final outletId = await SystemSetting.getCurrentOutletId();

    final query =
        selectOnly(rawMaterials).join([
            innerJoin(units, units.localId.equalsExp(rawMaterials.unitId)),
            innerJoin(
              outletInventories,
              outletInventories.itemType.equals(ItemType.raw_material.type) &
                  outletInventories.itemId.equalsExp(rawMaterials.localId),
            ),
            innerJoin(
              receiptItems,
              outletInventories.itemType.equals(ItemType.raw_material.type) &
                  receiptItems.rawMaterialId.equalsExp(
                    outletInventories.itemId,
                  ),
            ),
          ])
          ..addColumns([
            rawMaterials.name,
            units.name,
            outletInventories.localId,
            outletInventories.itemId,
            outletInventories.stock,
            outletInventories.purchasePrice,
            receiptItems.productVariantId,
            receiptItems.rawMaterialId,
            receiptItems.quantity,
          ])
          ..where(
            receiptItems.productVariantId.equals(productVariantId) &
                receiptItems.quantity.isBiggerThanValue(0.0) &
                receiptItems.deletedAt.isNull(),
          )
          ..where(outletInventories.outletId.equals(outletId!))
          ..where(outletInventories.deletedAt.isNull());

    final results = await query.get();

    final Map<String, UpdateStockRawMaterial> groupedMap = {
      for (final row in results)
        row.read(outletInventories.localId)!: UpdateStockRawMaterial(
          outletInventoryId: row.read(outletInventories.localId)!,
          productVariantId: row.read(receiptItems.productVariantId) ?? '',
          rawMaterialId: row.read(receiptItems.rawMaterialId) ?? '',
          name: row.read(rawMaterials.name) ?? '',
          unit: row.read(units.name) ?? '',
          productQuantity: productQuantity,
          stock: row.read(outletInventories.stock) ?? 0.0,
          quantity: row.read(receiptItems.quantity) ?? 0.0,
          purchasePrice: row.read(outletInventories.purchasePrice) ?? 0.0,
        ),
    };

    return groupedMap.values.toList();
  }

  Future<List<UpdateStockRawMaterial>> getProductUseMaterials(
    String productVariantId,
    double productQuantity,
  ) async {
    return await _getProductUseMaterials(productVariantId, productQuantity);
  }

  Future<bool> checkRawMaterialStockSufficient(
    String productVariantId,
    double productQuantity,
  ) async {
    final rawMaterials = await _getProductUseMaterials(
      productVariantId,
      productQuantity,
    );

    final Map<String, double> runningStockTracker = {};

    for (final rawMaterial in rawMaterials) {
      final double currentStockBase =
          runningStockTracker.containsKey(rawMaterial.outletInventoryId)
          ? runningStockTracker[rawMaterial.outletInventoryId]!
          : rawMaterial.stock;

      final double deduction =
          rawMaterial.productQuantity * rawMaterial.quantity;

      final double stockAfterDeduction = currentStockBase - deduction;

      runningStockTracker[rawMaterial.outletInventoryId] = stockAfterDeduction;

      if (stockAfterDeduction <= 0) return false;
    }

    return true;
  }

  Future<void> addSale({
    CustomersCompanion? customer,
    CustomerAddressesCompanion? customerAddress,
    required OrdersCompanion order,
    required List<OrderItemsCompanion> orderItem,
    required List<UpdateTotalSold> totalSolds,
    required List<UpdateStockProduct> stockProducts,
    required List<StockCardsCompanion> cardProducts,
  }) async {
    try {
      await transaction(() async {
        final now = DateTime.now();

        final orderNumber = await _generateOrderNumber();

        await batch((batch) async {
          if (customer != null) {
            batch.insert(customers, customer);

            CustomSyncQueue.addToSyncQueue(
              type: TableType.customers,
              id: customer.localId.value,
              action: SyncAction.create,
              batch: batch,
              syncQueues: syncQueues,
              data: {
                'uuid': customer.localId.value,
                'outlet_id': customer.outletId.value,
                'name': customer.name.value,
                'phone_number': customer.phoneNumber.value,
                'source': customer.source.value,
                'created_at': now.toIso8601String(),
                'updated_at': now.toIso8601String(),
              },
            );

            if (customerAddress != null) {
              batch.insert(customerAddresses, customerAddress);
            }
          }

          batch.insert(orders, order.copyWith(orderNumber: Value(orderNumber)));

          CustomSyncQueue.addToSyncQueue(
            type: TableType.orders,
            id: order.localId.value,
            action: SyncAction.create,
            batch: batch,
            syncQueues: syncQueues,
            data: {
              'uuid': order.localId.value,
              'order_number': orderNumber,
              'outlet_id': order.outletId.value,
              'outlet_name_snapshot': order.outletNameSnapshot.value,
              'customer_id': order.customerId.value,
              'customer_name_snapshot': order.customerNameSnapshot.value,
              'customer_phone_number_snapshot':
                  order.customerPhoneNumberSnapshot.value,
              'user_id': order.userId.value,
              'admin_name_snapshot': order.adminNameSnapshot.value,
              'source': order.source.value,
              'order_status': order.orderStatus.value,
              'total_order': order.totalOrder.value,
              'discount': order.discount.value,
              'tax': order.tax.value,
              'total_amount': order.totalAmount.value,
              'amount_paid': order.amountPaid.value,
              'change_amount': order.changeAmount.value,
              'notes': order.notes.value,
              'payment_proof_url': order.paymentProofPath.value,
              'customer_address_snapshot': order.customerAddressSnapshot.value,
              'created_at': now.toIso8601String(),
              'updated_at': now.toIso8601String(),
            },
          );

          final String category = order.source.value == 'pos'
              ? 'Penjualan Kasir'
              : 'Penjualan Toko Online';

          final transaction = TransactionsCompanion(
            localId: Value(const Uuid().v7().toString()),
            outletId: order.outletId,
            userId: order.userId,
            referenceType: Value(TransactionReferenceType.orders.type),
            referenceId: order.localId,
            referenceNumber: Value(orderNumber),
            stakeholder: order.customerNameSnapshot,
            transactionType: Value(TransactionType.income.type),
            category: Value(category),
            amount: order.totalAmount,
            note: order.notes,
          );

          batch.insert(transactions, transaction);

          CustomSyncQueue.addToSyncQueue(
            type: TableType.transactions,
            id: transaction.localId.value,
            action: SyncAction.create,
            batch: batch,
            syncQueues: syncQueues,
            data: {
              'uuid': transaction.localId.value,
              'outlet_id': transaction.outletId.value,
              'user_id': transaction.userId.value,
              'reference_type': transaction.referenceType.value,
              'reference_id': transaction.referenceId.value,
              'reference_number': transaction.referenceNumber.value,
              'stakeholder': transaction.stakeholder.value,
              'transaction_type': transaction.transactionType.value,
              'category': transaction.category.value,
              'amount': transaction.amount.value,
              'note': transaction.note.value,
              'created_at': now.toIso8601String(),
              'updated_at': now.toIso8601String(),
            },
          );

          for (final item in orderItem) {
            batch.insert(orderItems, item);

            CustomSyncQueue.addToSyncQueue(
              type: TableType.order_items,
              id: item.localId.value,
              action: SyncAction.create,
              batch: batch,
              syncQueues: syncQueues,
              data: {
                'uuid': item.localId.value,
                'order_id': item.orderId.value,
                'product_variant_id': item.productVariantId.value,
                'quantity': item.quantity.value,
                'product_image_url_snapshot': item.productImageSnapshot.value,
                'product_name_snapshot': item.productNameSnapshot.value,
                'product_varian_name_snapshot':
                    item.productVariantNameSnapshot.value,
                'selling_price_snapshot': item.sellingPriceSnapshot.value,
                'subtotal': item.subtotal.value,
                'created_at': now.toIso8601String(),
                'updated_at': now.toIso8601String(),
              },
            );
          }

          for (final item in totalSolds) {
            batch.update(
              productVariants,
              ProductVariantsCompanion(
                totalSold: Value(item.qtySold + item.currentSold),
                updatedAt: Value(now),
              ),
              where: (tbl) => tbl.localId.equals(item.id),
            );

            CustomSyncQueue.addToSyncQueue(
              type: TableType.product_variants,
              id: item.id,
              action: SyncAction.update,
              batch: batch,
              syncQueues: syncQueues,
              data: {
                'uuid': item.id,
                'qty_sold': item.qtySold,
                'updated_at': now.toIso8601String(),
              },
            );
          }

          final List<UpdateStockRawMaterial> productUseMaterials = [];

          for (final stockProduct in stockProducts) {
            if (stockProduct.stockType == StockType.fixed) {
              batch.update(
                outletInventories,
                OutletInventoriesCompanion(
                  stock: Value(stockProduct.stock - stockProduct.quantity),
                  syncStatus: Value(SyncStatus.dirty.status),
                  updatedAt: Value(now),
                ),
                where: (inv) {
                  return inv.localId.equals(stockProduct.outletInventoryId);
                },
              );
            }
            productUseMaterials.addAll(
              await _getProductUseMaterials(
                stockProduct.productVariantId,
                stockProduct.quantity,
              ),
            );
          }

          final Map<String, double> runningStockTracker = {};

          final List<StockCardsCompanion> cardRawMaterials = [];

          final userId = await SystemSetting.getCurrentUserId();

          if (productUseMaterials.isNotEmpty) {
            for (final productUseMaterial in productUseMaterials) {
              final double currentStockBase =
                  runningStockTracker.containsKey(
                    productUseMaterial.outletInventoryId,
                  )
                  ? runningStockTracker[productUseMaterial.outletInventoryId]!
                  : productUseMaterial.stock;

              final double deduction =
                  productUseMaterial.productQuantity *
                  productUseMaterial.quantity;

              final double stockAfterDeduction = currentStockBase - deduction;

              runningStockTracker[productUseMaterial.outletInventoryId] =
                  stockAfterDeduction;

              batch.update(
                outletInventories,
                OutletInventoriesCompanion(
                  stock: Value(stockAfterDeduction),
                  syncStatus: Value(SyncStatus.dirty.status),
                  updatedAt: Value(now),
                ),
                where: (inv) {
                  return inv.localId.equals(
                    productUseMaterial.outletInventoryId,
                  );
                },
              );

              cardRawMaterials.add(
                StockCardsCompanion(
                  localId: Value(Uuid().v7().toString()),
                  outletInventoryId: Value(
                    productUseMaterial.outletInventoryId,
                  ),
                  userId: Value(userId!),
                  type: Value(StockCardType.sale.type),
                  qtyChange: Value(deduction),
                  currentStock: Value(stockAfterDeduction),
                  stockType: Value(StockType.fixed.type),
                  costPrice: Value(productUseMaterial.purchasePrice),
                  referenceId: order.localId,
                  syncStatus: Value(SyncStatus.dirty.status),
                ),
              );
            }
          }

          if (cardProducts.isNotEmpty) {
            for (final card in cardProducts) {
              batch.insert(stockCards, card);
              CustomSyncQueue.addToSyncQueue(
                type: TableType.stock_cards,
                id: card.localId.value,
                action: SyncAction.create,
                batch: batch,
                syncQueues: syncQueues,
                data: {
                  'uuid': card.localId.value,
                  'outlet_inventory_id': card.outletInventoryId.value,
                  'user_id': card.userId.value,
                  'type': card.type.value,
                  'qty_change': card.qtyChange.value,
                  'current_stock': card.currentStock.value,
                  'stock_type': card.stockType.value,
                  'cost_price': card.costPrice.value,
                  'reference_id': card.referenceId.value,
                  'created_at': now.toIso8601String(),
                },
              );
            }
          }

          if (cardRawMaterials.isNotEmpty) {
            for (final card in cardRawMaterials) {
              batch.insert(stockCards, card);
              CustomSyncQueue.addToSyncQueue(
                type: TableType.stock_cards,
                id: card.localId.value,
                action: SyncAction.create,
                batch: batch,
                syncQueues: syncQueues,
                data: {
                  'uuid': card.localId.value,
                  'outlet_inventory_id': card.outletInventoryId.value,
                  'user_id': card.userId.value,
                  'type': card.type.value,
                  'qty_change': card.qtyChange.value,
                  'current_stock': card.currentStock.value,
                  'stock_type': card.stockType.value,
                  'cost_price': card.costPrice.value,
                  'reference_id': card.referenceId.value,
                  'created_at': now.toIso8601String(),
                },
              );
            }
          }
        });
      });
    } catch (e, st) {
      LogMessage.log.e(e.toString(), error: e, stackTrace: st);
      rethrow;
    }
  }

  Future<OrderDetail> getOrderDetail(String orderId) async {
    final query =
        selectOnly(orders).join([
            innerJoin(orderItems, orderItems.orderId.equalsExp(orders.localId)),
          ])
          ..addColumns([
            orders.localId,
            orders.orderNumber,
            orders.createdAt,
            orders.source,
            orders.orderStatus,
            orders.outletNameSnapshot,
            orders.adminNameSnapshot,
            orders.customerNameSnapshot,
            orders.customerPhoneNumberSnapshot,
            orders.customerAddressSnapshot,
            orders.notes,
            orderItems.productNameSnapshot,
            orderItems.productVariantNameSnapshot,
            orderItems.sellingPriceSnapshot,
            orderItems.quantity,
            orderItems.subtotal,
            orders.totalOrder,
            orders.discount,
            orders.tax,
            orders.totalAmount,
            orders.amountPaid,
            orders.changeAmount,
            orders.paymentProofPath,
          ])
          ..where(orders.localId.equals(orderId));
    final results = await query.get();

    final Map<String, OrderDetail> groupedMap = {};

    for (final row in results) {
      final String orderId = row.read(orders.localId) ?? '';

      final String orderNumber = (row.read(orders.orderNumber) ?? '')
          .toUniqueNumberCleaner();

      final DateTime orderDate = row.read(orders.createdAt) ?? DateTime.now();

      final OrderSource orderSource = OrderSource.values.byName(
        (row.read(orders.source) ?? ''),
      );

      final String status = row.read(orders.orderStatus) ?? '';

      final OrderStatus orderStatus = OrderStatus.values.byName(status);

      final String outletName = row.read(orders.outletNameSnapshot) ?? '';

      final String userName = row.read(orders.adminNameSnapshot) ?? '';

      final String? customerName = row.read(orders.customerNameSnapshot);

      final String? customerPhone = row.read(
        orders.customerPhoneNumberSnapshot,
      );

      final dynamic rawCustomerAddress = row.read(
        orders.customerAddressSnapshot,
      );

      Map<String, dynamic> customerAddressSnapshot = {};

      if (rawCustomerAddress is String) {
        try {
          customerAddressSnapshot = (jsonDecode(rawCustomerAddress) as Map)
              .cast<String, dynamic>();
        } catch (e) {
          customerAddressSnapshot = {};
          LogMessage.log.e(e.toString(), error: e);
        }
      } else if (rawCustomerAddress is Map) {
        customerAddressSnapshot = (rawCustomerAddress).cast<String, dynamic>();
      }

      String? customerAddress;

      if (customerAddressSnapshot.isNotEmpty) {
        customerAddress = customerAddressSnapshot["full_address"] as String?;
      }

      final String? note = row.read(orders.notes);

      final double totalOrder = row.read(orders.totalOrder) ?? 0.0;

      final double discount = row.read(orders.discount) ?? 0.0;

      final double tax = row.read(orders.tax) ?? 0.0;

      final double totalAmount = row.read(orders.totalAmount) ?? 0.0;

      final double payAmount = row.read(orders.amountPaid) ?? 0.0;

      final double changeAmount = row.read(orders.changeAmount) ?? 0.0;

      final String? paymentProofPath = row.read(orders.paymentProofPath);

      groupedMap.putIfAbsent(orderId, () {
        return OrderDetail(
          orderId: orderId,
          orderNumber: orderNumber,
          orderDate: orderDate,
          orderSource: orderSource,
          orderStatus: orderStatus,
          outletName: outletName,
          userName: userName,
          customerName: customerName,
          customerPhone: customerPhone,
          customerAddress: customerAddress,
          note: note,
          items: [],
          totalOrder: totalOrder,
          discount: discount,
          tax: tax,
          totalAmount: totalAmount,
          payAmount: payAmount,
          changeAmount: changeAmount,
          paymentProofPath: paymentProofPath,
        );
      });

      final String name = row.read(orderItems.productNameSnapshot) ?? '';

      final String? variantName = row.read(
        orderItems.productVariantNameSnapshot,
      );

      final double sellingPrice =
          row.read(orderItems.sellingPriceSnapshot) ?? 0.0;

      final double quantity = row.read(orderItems.quantity) ?? 0.0;

      final double subtotal = row.read(orderItems.subtotal) ?? 0.0;

      groupedMap[orderId]!.items.add(
        OrderItem(
          name: name,
          variantName: variantName,
          sellingPrice: sellingPrice,
          quantity: quantity,
          subtotal: subtotal,
        ),
      );
    }

    return groupedMap.values.single;
  }

  Future<int> cancelOrder(String orderId) async {
    return await transaction(() async {
      final now = DateTime.now();

      final rowsAffected =
          (await update(orders)
                ..where((order) => order.localId.equals(orderId)))
              .write(
                OrdersCompanion(
                  orderStatus: Value(OrderStatus.canceled.status),
                  syncStatus: Value(SyncStatus.dirty.status),
                  updatedAt: Value(now),
                ),
              );

      final payload = SyncPayload(
        table: TableType.orders.type,
        localId: orderId,
        action: SyncAction.update.action,
        data: {
          'uuid': orderId,
          'order_status': OrderStatus.canceled.status,
          'updated_at': now.toIso8601String(),
        },
      ).toJson();

      await into(syncQueues).insert(
        SyncQueuesCompanion(
          tableType: Value(TableType.orders.type),
          tableLocalId: Value(orderId),
          action: Value(SyncAction.update.action),
          status: Value(SyncStatus.dirty.status),
          payload: Value(payload),
        ),
      );

      return rowsAffected;
    });
  }

  Future<int> updateAlreadyReadOrderOnline(String orderId) async {
    return transaction(() async {
      final now = DateTime.now();

      final rowsAffected =
          (await update(orders)
                ..where((order) => order.localId.equals(orderId)))
              .write(
                OrdersCompanion(
                  alreadyRead: Value(false),
                  syncStatus: Value(SyncStatus.dirty.status),
                  updatedAt: Value(now),
                ),
              );

      final payload = SyncPayload(
        table: TableType.orders.type,
        localId: orderId,
        action: SyncAction.update.action,
        data: {
          'uuid': orderId,
          'already_read': false,
          'updated_at': now.toIso8601String(),
        },
      ).toJson();

      await into(syncQueues).insert(
        SyncQueuesCompanion(
          tableType: Value(TableType.orders.type),
          tableLocalId: Value(orderId),
          action: Value(SyncAction.update.action),
          status: Value(SyncStatus.dirty.status),
          payload: Value(payload),
        ),
      );

      return rowsAffected;
    });
  }

  Stream<MessageActivity?> watchOrderOnlineHasNotReview() async* {
    final userRole = await SystemSetting.getCurrentUserRole();

    final outletId = await SystemSetting.getCurrentOutletId();

    final query = selectOnly(orders)
      ..addColumns([
        orders.localId,
        orders.orderNumber,
        orders.customerNameSnapshot,
        orders.totalAmount,
      ])
      ..where(
        orders.source.equals(OrderSource.online.source) &
            orders.alreadyRead.equals(true) &
            (orders.orderStatus.equals(OrderStatus.pending_payment.status) |
                orders.orderStatus.equals(
                  OrderStatus.pending_confirmation.status,
                )) &
            (userRole == UserRole.admin
                ? orders.outletId.equals(outletId!)
                : const Constant(true)),
      )
      ..limit(1);

    yield* query.watchSingle().map((row) {
      final id = row.read(orders.localId);

      if (id != null) {
        return MessageActivity(
          type: MessageActivityType.new_online_order,
          messages: [
            id,
            row.read(orders.orderNumber)!,
            row.read(orders.customerNameSnapshot)!,
            CurrencyFormat.formatToIdr(
              (row.read(orders.totalAmount) ?? 0.0),
              0,
            ),
          ],
        );
      }

      return null;
    });
  }

  Stream<int> watchNumberOfNeedToBeProcessed() async* {
    final userRole = await SystemSetting.getCurrentUserRole();

    final outletId = await SystemSetting.getCurrentOutletId();

    final query = selectOnly(orders)
      ..addColumns([orders.localId.count()])
      ..groupBy([orders.source, orders.orderStatus])
      ..orderBy([OrderingTerm.desc(orders.createdAt)])
      ..where(orders.source.equals(OrderSource.online.source))
      ..where(
        orders.orderStatus.equals(OrderStatus.pending_payment.status) |
            orders.orderStatus.equals(OrderStatus.pending_confirmation.status),
      )
      ..where(
        userRole == UserRole.admin
            ? orders.outletId.equals(outletId!)
            : const Constant(true),
      );

    yield* query.watch().map((rows) {
      return rows.fold(0, (sum, row) {
        return sum + (row.read(orders.localId.count()) ?? 0);
      });
    });
  }

  Stream<List<OnlineOrder>> watchAllOnlineOrders({
    String? outletId,
    DateTime? startDate,
    DateTime? endDate,
  }) async* {
    final query =
        selectOnly(orders).join([
            innerJoin(orderItems, orderItems.orderId.equalsExp(orders.localId)),
            innerJoin(
              productVariants,
              productVariants.localId.equalsExp(orderItems.productVariantId),
            ),
            innerJoin(
              products,
              products.localId.equalsExp(productVariants.productId),
            ),
          ])
          ..addColumns([
            orders.localId,
            productVariants.localImagePath,
            orders.orderNumber,
            orders.orderStatus,
            orders.isCancellation,
            orders.cancellationAccepted,
            orders.approvalProccess,
            orderItems.localId.count(),
            orders.totalAmount,
            orders.createdAt,
            products.hasVariant,
            products.localImagePath,
            productVariants.localImagePath,
          ])
          ..groupBy([orders.localId])
          ..orderBy([OrderingTerm.desc(orders.createdAt)])
          ..where(orders.source.equals(OrderSource.online.source))
          ..where(
            outletId != null
                ? orders.outletId.equals(outletId)
                : const Constant(true),
          )
          ..where(
            (startDate != null && endDate != null)
                ? orders.createdAt.isBiggerOrEqualValue(startDate) &
                      orders.createdAt.isSmallerThanValue(
                        endDate.add(const Duration(days: 1)),
                      )
                : const Constant(true),
          );

    yield* query.watch().map((rows) {
      return rows.map((row) {
        final dynamic rawImages = row.read(products.localImagePath);

        List<String> productImages = [];

        if (rawImages is String) {
          try {
            productImages = (jsonDecode(rawImages) as List).cast<String>();
          } catch (e) {
            productImages = [];
          }
        } else if (rawImages is List) {
          productImages = (rawImages).cast<String>();
        }

        final bool hasVariant = row.read(products.hasVariant) ?? false;

        final String? displayImage = hasVariant
            ? row.read(productVariants.localImagePath)
            : productImages.lastOrNull;

        return OnlineOrder(
          id: row.read(orders.localId)!,
          displayImage: displayImage,
          orderNumber: (row.read(orders.orderNumber) ?? '')
              .toUniqueNumberCleaner(),
          orderStatus: OrderStatus.values.byName(
            (row.read(orders.orderStatus) ?? ''),
          ),
          isCancellation: row.read(orders.isCancellation) ?? false,
          cancellationAccepted: row.read(orders.cancellationAccepted) ?? false,
          process: ApprovalProcess.values.byName(
            (row.read(orders.approvalProccess) ?? 'done'),
          ),
          itemCount: row.read(orderItems.localId.count()) ?? 0,
          orderTotal: row.read(orders.totalAmount) ?? 0.0,
          orderDate: row.read(orders.createdAt) ?? DateTime.now(),
        );
      }).toList();
    });
  }

  Future<OnlineOrderDetail> getOnlineOrderDetailById(String orderId) async {
    final query =
        selectOnly(orders).join([
            innerJoin(orderItems, orderItems.orderId.equalsExp(orders.localId)),
          ])
          ..addColumns([
            orders.localId,
            orders.outletNameSnapshot,
            orders.orderNumber,
            orders.createdAt,
            orders.notes,
            orders.orderStatus,
            orders.isCancellation,
            orders.cancellationAccepted,
            orders.approvalProccess,
            orders.deliveryType,
            orders.customerNameSnapshot,
            orders.customerPhoneNumberSnapshot,
            orders.customerAddressSnapshot,
            orders.deliveryPrefrence,
            orders.paymentMethod,
            orders.paymentProofPath,
            orders.shippingCost,
            orderItems.quantity,
            orderItems.productImageSnapshot,
            orderItems.productNameSnapshot,
            orderItems.productVariantNameSnapshot,
            orderItems.sellingPriceSnapshot,
            orderItems.subtotal,
            orders.totalOrder,
            orders.totalAmount,
          ])
          ..where(orders.localId.equals(orderId));

    final results = await query.get();

    final Map<String, OnlineOrderDetail> groupedMap = {};

    for (final row in results) {
      final orderId = row.read(orders.localId)!;

      final outletName = row.read(orders.outletNameSnapshot) ?? '';

      final orderNumber = (row.read(orders.orderNumber) ?? '')
          .toUniqueNumberCleaner();

      final orderDate = row.read(orders.createdAt) ?? DateTime.now();

      final notes = row.read(orders.notes);

      final orderStatus = OrderStatus.values.byName(
        (row.read(orders.orderStatus) ?? ''),
      );

      final isCancellation = row.read(orders.isCancellation) ?? false;

      final cancellationAccepted =
          row.read(orders.cancellationAccepted) ?? false;

      final process = ApprovalProcess.values.byName(
        (row.read(orders.approvalProccess) ?? 'done'),
      );

      final deliveryType = DeliveryType.values.byName(
        (row.read(orders.deliveryType) ?? ''),
      );

      final dynamic rawCustomerAddress = row.read(
        orders.customerAddressSnapshot,
      );

      Map<String, dynamic> customerAddressSnapshot = {};

      if (rawCustomerAddress is String) {
        try {
          customerAddressSnapshot = (jsonDecode(rawCustomerAddress) as Map)
              .cast<String, dynamic>();
        } catch (e) {
          customerAddressSnapshot = {};
          LogMessage.log.e(e.toString(), error: e);
        }
      } else if (rawCustomerAddress is Map) {
        customerAddressSnapshot = (rawCustomerAddress).cast<String, dynamic>();
      }

      String? customerAddress;

      if (customerAddressSnapshot.isNotEmpty) {
        customerAddress = customerAddressSnapshot["full_address"] as String?;
      }

      final deliveryInfo = DeliveryInfo(
        name: row.read(orders.customerNameSnapshot) ?? '',
        phoneNumber: row.read(orders.customerPhoneNumberSnapshot) ?? '',
        fullAddress: customerAddress ?? '',
        preference: row.read(orders.deliveryPrefrence) ?? '',
      );

      final String paymentMethod = row.read(orders.paymentMethod) ?? '';

      final paymentProof = row.read(orders.paymentProofPath) ?? '';

      final totalOrder = row.read(orders.totalOrder) ?? 0.0;

      final deliveryFee = row.read(orders.shippingCost) ?? 0.0;

      final totalAmount = row.read(orders.totalAmount) ?? 0.0;

      groupedMap.putIfAbsent(
        orderId,
        () => OnlineOrderDetail(
          id: orderId,
          outletName: outletName,
          orderNumber: orderNumber,
          orderDate: orderDate,
          notes: notes,
          orderStatus: orderStatus,
          isCancellation: isCancellation,
          cancellationAccepted: cancellationAccepted,
          process: process,
          deliveryType: deliveryType,
          deliveryInfo: deliveryInfo,
          paymentMethod: paymentMethod,
          paymentProof: paymentProof,
          items: [],
          totalOrder: totalOrder,
          deliveryFee: deliveryFee,
          totalAmount: totalAmount,
        ),
      );

      groupedMap[orderId]!.items.add(
        OnlineOrderItem(
          imagePath: row.read(orderItems.productImageSnapshot),
          name: row.read(orderItems.productNameSnapshot) ?? '',
          variantName: row.read(orderItems.productVariantNameSnapshot),
          sellingPrice: row.read(orderItems.sellingPriceSnapshot) ?? 0.0,
          quantity: row.read(orderItems.quantity) ?? 0.0,
          subtotal: row.read(orderItems.subtotal) ?? 0.0,
        ),
      );
    }

    return groupedMap.values.single;
  }

  Stream<ViewOrderStatus> watchOrderStatusById(String orderId) async* {
    final query = selectOnly(orders)
      ..addColumns([
        orders.isCancellation,
        orders.cancellationAccepted,
        orders.approvalProccess,
        orders.orderStatus,
        orders.canceledBy,
      ])
      ..where(orders.localId.equals(orderId))
      ..limit(1);

    yield* query.watchSingle().map((row) {
      final orderStatus = OrderStatus.values.byName(
        (row.read(orders.orderStatus) ?? ''),
      );

      final cBay = row.read(orders.canceledBy);

      CanceledBy? canceledBy;

      if (orderStatus == OrderStatus.canceled &&
          cBay != null &&
          cBay.isNotEmpty) {
        canceledBy = CanceledBy.values.byName(cBay);
      }

      return ViewOrderStatus(
        isCancellation: row.read(orders.isCancellation) ?? false,
        cancellationAccepted: row.read(orders.cancellationAccepted) ?? false,
        process: ApprovalProcess.values.byName(
          (row.read(orders.approvalProccess) ?? 'done'),
        ),
        orderStatus: orderStatus,
        canceledBy: canceledBy,
      );
    });
  }

  Future<int> updateOrder({
    required String orderId,
    required OrderStatus orderStatus,
    required bool cancellationAccepted,
    ApprovalProcess? process,
    CanceledBy? canceledBy,
  }) async {
    return transaction(() async {
      final now = DateTime.now();

      final rowsAffected =
          (await update(orders)
                ..where((order) => order.localId.equals(orderId)))
              .write(
                OrdersCompanion(
                  orderStatus: Value(orderStatus.status),
                  cancellationAccepted: Value(cancellationAccepted),
                  approvalProccess: Value.absentIfNull(process?.process),
                  canceledBy: Value.absentIfNull(canceledBy?.type),
                  syncStatus: Value(SyncStatus.dirty.status),
                  updatedAt: Value(now),
                ),
              );

      final payload = SyncPayload(
        table: TableType.orders.type,
        localId: orderId,
        action: SyncAction.update.action,
        data: {
          'uuid': orderId,
          'order_status': orderStatus.status,
          'cancellation_accepted': cancellationAccepted,
          'approval_proccess': process?.process,
          'canceledBy': canceledBy?.type,
          'updated_at': now.toIso8601String(),
        },
      ).toJson();

      await into(syncQueues).insert(
        SyncQueuesCompanion(
          tableType: Value(TableType.orders.type),
          tableLocalId: Value(orderId),
          action: Value(SyncAction.update.action),
          status: Value(SyncStatus.dirty.status),
          payload: Value(payload),
        ),
      );

      return rowsAffected;
    });
  }
}
