import 'dart:convert';

import 'package:drift/drift.dart';
import 'package:niogu_app/core/database/app_database.dart'
    hide RawMaterial, Outlet;
import 'package:niogu_app/core/database/tables/outlet_inventories.dart';
import 'package:niogu_app/core/database/tables/outlets.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/units.dart';
import 'package:niogu_app/core/utils/currency_format.dart';
import 'package:niogu_app/core/utils/extension_format.dart';
import 'package:niogu_app/core/enums/item_type.dart';
import 'package:niogu_app/core/system/system_setting.dart';
import 'package:niogu_app/features/goods/raw_materials/domain/entities/raw_material.dart';
import 'package:niogu_app/features/outlets/domain/entities/outlet.dart';

part 'raw_material_dao.g.dart';

@DriftAccessor(
  tables: [
    RawMaterials,
    Units,
    Products,
    ProductVariants,
    ReceiptItems,
    OutletInventories,
    Outlets,
  ],
)
class RawMaterialDao extends DatabaseAccessor<AppDatabase>
    with _$RawMaterialDaoMixin {
  RawMaterialDao(AppDatabase db) : super(db);

  Stream<List<RawMaterial>> watchAllMaterials() async* {
    final outletId = await SystemSetting.getCurrentOutletId();
    final query =
        selectOnly(rawMaterials).join([
            innerJoin(units, units.localId.equalsExp(rawMaterials.unitId)),
            innerJoin(
              receiptItems,
              receiptItems.rawMaterialId.equalsExp(rawMaterials.localId),
            ),
            innerJoin(
              outletInventories,
              outletInventories.itemType.equals(ItemType.raw_material.type) &
                  outletInventories.itemId.equalsExp(rawMaterials.localId),
            ),
          ])
          ..addColumns([
            rawMaterials.localId,
            rawMaterials.name,
            units.name,
            outletInventories.stock,
            outletInventories.purchasePrice,
            receiptItems.deletedAt,
          ])
          ..orderBy([OrderingTerm.desc(rawMaterials.createdAt)])
          ..where(rawMaterials.deletedAt.isNull())
          ..where(
            outletInventories.outletId.equals(outletId!) &
                outletInventories.deletedAt.isNull(),
          );

    yield* query.watch().map((rows) {
      final Map<String, RawMaterial> groupedMap = {};
      final Map<String, int> countProductUsages = {};

      for (final row in rows) {
        final String id = row.read(rawMaterials.localId)!;
        final String name = row.read(rawMaterials.name) ?? '';
        final String remainingStock =
            "${(row.read(outletInventories.stock) ?? 0.0).toStringWithoutTrailingZero()} ${row.read(units.name) ?? ''}";
        final String displayPurchasePrice = CurrencyFormat.formatToIdr(
          (row.read(outletInventories.purchasePrice) ?? 0.0),
          0,
        );

        final deletedAt = row.read(receiptItems.deletedAt);

        countProductUsages.update(
          id,
          (value) => value + (deletedAt == null ? 1 : 0),
          ifAbsent: () => deletedAt == null ? 1 : 0,
        );

        final rawMaterial = RawMaterial(
          id: id,
          name: name,
          remainingStock: remainingStock,
          displayPurchasePrice: displayPurchasePrice,
          countProductUsage: countProductUsages[id]!,
        );

        groupedMap.update(
          id,
          (value) =>
              rawMaterial.copyWith(countProductUsage: countProductUsages[id]),
          ifAbsent: () => rawMaterial,
        );
      }

      return groupedMap.values.toList();
    });
  }

  Future<RawMaterialDetail> getRawMaterialById(
    String id,
    bool isEditing,
  ) async {
    final outletId = await SystemSetting.getCurrentOutletId();
    final materialQuery =
        selectOnly(rawMaterials).join([
            innerJoin(units, units.localId.equalsExp(rawMaterials.unitId)),
            innerJoin(
              outletInventories,
              outletInventories.itemType.equals("raw_material") &
                  outletInventories.itemId.equalsExp(rawMaterials.localId),
            ),
          ])
          ..addColumns([
            rawMaterials.localId,
            rawMaterials.name,
            rawMaterials.unitId,
            outletInventories.localId,
            outletInventories.purchasePrice,
            outletInventories.stock,
          ])
          ..where(rawMaterials.localId.equals(id))
          ..where(rawMaterials.deletedAt.isNull())
          ..where(
            (isEditing
                    ? outletInventories.outletId.equals(outletId!)
                    : const Constant(true)) &
                outletInventories.deletedAt.isNull(),
          )
          ..limit(1);

    final materialRow = await materialQuery.getSingle();

    final String materialId = materialRow.read(rawMaterials.localId) ?? '';
    final String name = materialRow.read(rawMaterials.name) ?? '';
    final String unitId = materialRow.read(rawMaterials.unitId) ?? '';
    final String outletInventoryId =
        materialRow.read(outletInventories.localId) ?? '';
    final double purchasePrice =
        materialRow.read(outletInventories.purchasePrice) ?? 0.0;
    final double stock = materialRow.read(outletInventories.stock) ?? 0.0;

    final receiptItemQuery =
        selectOnly(receiptItems).join([
            innerJoin(
              productVariants,
              productVariants.localId.equalsExp(receiptItems.productVariantId),
            ),
          ])
          ..addColumns([
            receiptItems.localId,
            receiptItems.productVariantId,
            receiptItems.quantity,
          ])
          ..where(
            receiptItems.rawMaterialId.equals(id) &
                receiptItems.deletedAt.isNull(),
          );

    final receiptItemRows = await receiptItemQuery.get();

    final List<SelectedProductUsageMaterial> productUsages = receiptItemRows
        .map((row) {
          final String receiptItemId = row.read(receiptItems.localId) ?? '';
          final String productUsageId =
              row.read(receiptItems.productVariantId) ?? '';
          final double quantity = row.read(receiptItems.quantity) ?? 0.0;

          return SelectedProductUsageMaterial(
            receiptItemId: receiptItemId,
            productUsageId: productUsageId,
            quantity: quantity,
          );
        })
        .toList();

    return RawMaterialDetail(
      id: materialId,
      name: name,
      purchasePrice: purchasePrice,
      stock: stock,
      unitId: unitId,
      outletInventoryId: outletInventoryId,
      productUsages: productUsages,
    );
  }

  Stream<List<ProductUsageMaterial>> watchAllProductUsageMaterials(
    bool isEditing,
  ) async* {
    final outletId = await SystemSetting.getCurrentOutletId();
    final query =
        selectOnly(products).join([
            innerJoin(
              productVariants,
              productVariants.productId.equalsExp(products.localId),
            ),
            innerJoin(
              outletInventories,
              outletInventories.itemType.equals(ItemType.product_variant.type) &
                  outletInventories.itemId.equalsExp(productVariants.localId),
            ),
          ])
          ..addColumns([
            products.localId,
            products.name,
            products.localImagePath,
            products.hasVariant,
            products.deletedAt,
            productVariants.localId,
            productVariants.isProductVariant,
            productVariants.name,
            productVariants.deletedAt,
            outletInventories.deletedAt,
          ])
          ..orderBy([OrderingTerm.desc(products.createdAt)])
          ..where(
            isEditing
                ? outletInventories.outletId.equals(outletId!)
                : outletInventories.outletId.equals(outletId!).not(),
          );

    yield* query.watch().map((rows) {
      final Map<String, ProductUsageMaterial> groupedMap = {};

      for (final row in rows) {
        final productId = row.read(products.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 productDeletedAt = row.read(products.deletedAt);

        final variantDeletedAt = row.read(productVariants.deletedAt);

        final outletInventoryDeletedAt = row.read(outletInventories.deletedAt);

        final productHasBeenDelete = productDeletedAt != null;

        final variantHasBeenDelete =
            (variantDeletedAt != null) || (outletInventoryDeletedAt != null);

        groupedMap.putIfAbsent(
          productId,
          () => ProductUsageMaterial(
            id: productId,
            name: row.read(products.name) ?? '',
            imagePath: productImages.lastOrNull,
            hasVariant: row.read(products.hasVariant) ?? false,
            variants: [],
            hasBeenDelete: productHasBeenDelete,
          ),
        );

        groupedMap[productId]!.variants.add(
          VariantUsageMaterial(
            id: row.read(productVariants.localId)!,
            name: row.read(productVariants.name) ?? '',
            isProductVariant:
                row.read(productVariants.isProductVariant) ?? false,
            hasBeenDelete: variantHasBeenDelete,
          ),
        );
      }

      return groupedMap.values.toList();
    });
  }

  Future<DeleteRawMaterialSnapshot> getRawMaterialSnapshotById(
    String id,
  ) async {
    final receiptItemQuery =
        selectOnly(receiptItems).join([
            innerJoin(
              productVariants,
              productVariants.localId.equalsExp(receiptItems.productVariantId),
            ),
            innerJoin(
              rawMaterials,
              rawMaterials.localId.equalsExp(receiptItems.rawMaterialId),
            ),
          ])
          ..addColumns([
            receiptItems.localId.count(),
            receiptItems.rawMaterialId,
            rawMaterials.name,
          ])
          ..groupBy([receiptItems.rawMaterialId])
          ..where(
            receiptItems.rawMaterialId.equals(id) &
                receiptItems.deletedAt.isNull(),
          )
          ..where(productVariants.deletedAt.isNull())
          ..where(rawMaterials.deletedAt.isNull());

    final receiptItemRow = await receiptItemQuery.getSingle();

    final String rawMaterialId =
        receiptItemRow.read(receiptItems.rawMaterialId) ?? '';
    final String name = receiptItemRow.read(rawMaterials.name) ?? '';
    final int totalProductUsage =
        receiptItemRow.read(receiptItems.localId.count()) ?? 0;

    final outletQuery =
        selectOnly(outlets).join([
            innerJoin(
              outletInventories,
              outletInventories.outletId.equalsExp(outlets.localId),
            ),
          ])
          ..addColumns([outlets.localId, outlets.name, outlets.isMainOutlet])
          ..where(
            outletInventories.itemType.equals("raw_material") &
                outletInventories.itemId.equals(id),
          )
          ..where(outlets.deletedAt.isNull())
          ..where(outletInventories.deletedAt.isNull());

    final outletRows = await outletQuery.get();

    final List<Outlet> outletList = outletRows.map((row) {
      final String id = row.read(outlets.localId) ?? '';
      final String name = row.read(outlets.name) ?? '';
      final bool isMainOutlet = row.read(outlets.isMainOutlet) ?? false;
      return Outlet(
        id: id,
        name: name,
        // email: '',
        // phoneNumber: '',
        isMainOutlet: isMainOutlet,
      );
    }).toList();

    final outletId = await SystemSetting.getCurrentOutletId();

    final outlet = outletList.where((outlet) => outlet.id == outletId).first;

    return DeleteRawMaterialSnapshot(
      rawMaterialId: rawMaterialId,
      name: name,
      totalProductUsage: totalProductUsage,
      currentOutletName: outlet.name,
      isMainOutlet: outlet.isMainOutlet,
      totalOutletCount: outletList.length,
    );
  }

  Future<bool> checkAlreadyRawMaterialByOutlet(String id) async {
    final outletId = await SystemSetting.getCurrentOutletId();

    final query =
        selectOnly(rawMaterials).join([
            innerJoin(
              outletInventories,
              outletInventories.itemType.equals(ItemType.raw_material.type) &
                  outletInventories.itemId.equalsExp(rawMaterials.localId),
            ),
          ])
          ..addColumns([rawMaterials.localId])
          ..where(
            rawMaterials.localId.equals(id) & rawMaterials.deletedAt.isNull(),
          )
          ..where(
            outletInventories.outletId.equals(outletId!) &
                outletInventories.deletedAt.isNull(),
          )
          ..limit(1);

    final result = await query.getSingleOrNull();

    return result != null;
  }

  Stream<List<RawMaterialChoice>> watchAllRawMaterialChoices() async* {
    final query =
        selectOnly(rawMaterials).join([
            innerJoin(
              receiptItems,
              receiptItems.rawMaterialId.equalsExp(rawMaterials.localId),
            ),
          ])
          ..addColumns([
            rawMaterials.localId,
            rawMaterials.name,
            receiptItems.localId.count(),
          ])
          ..groupBy([rawMaterials.localId])
          ..where(rawMaterials.deletedAt.isNull())
          ..where(receiptItems.deletedAt.isNull());

    yield* query.watch().map((rows) {
      return rows.map((row) {
        return RawMaterialChoice(
          id: row.read(rawMaterials.localId)!,
          name: row.read(rawMaterials.name) ?? '',
          countProductUsage: row.read(receiptItems.localId.count()) ?? 0,
        );
      }).toList();
    });
  }
}
