import 'dart:convert';

import 'package:drift/drift.dart';
import 'package:niogu_app/core/database/app_database.dart' hide Product, Outlet;
import 'package:niogu_app/core/database/tables/categories.dart';
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/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/core/enums/stock_type.dart';
import 'package:niogu_app/features/outlets/domain/entities/outlet.dart';
import 'package:niogu_app/features/goods/products/domain/entities/product.dart';

part 'product_dao.g.dart';

@DriftAccessor(
  tables: [
    Products,
    Categories,
    Units,
    ProductVariants,
    OutletInventories,
    Outlets,
  ],
)
class ProductDao extends DatabaseAccessor<AppDatabase> with _$ProductDaoMixin {
  ProductDao(AppDatabase db) : super(db);

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

    final masterQuery = selectOnly(productVariants)
      ..addColumns([productVariants.productId, productVariants.localId.count()])
      ..groupBy([productVariants.productId])
      ..where(productVariants.deletedAt.isNull());

    final masterSub = Subquery(masterQuery, 'master_sub');

    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),
            ),
            innerJoin(
              masterSub,
              masterSub
                  .ref(productVariants.productId)
                  .equalsExp(products.localId),
            ),
          ])
          ..addColumns([
            products.localId,
            products.localImagePath,
            products.name,
            units.name,
            products.hasVariant,
            productVariants.localId.count(),
            masterSub.ref(productVariants.localId.count()),
            productVariants.isProductVariant,
            outletInventories.stockType,
            outletInventories.stock.sum(),
            outletInventories.sellingPrice.min(),
          ])
          ..groupBy([products.localId])
          ..orderBy([OrderingTerm.desc(products.createdAt)])
          ..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 id = row.read(products.localId) ?? '';
        final String name = row.read(products.name) ?? '';
        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 bool isProductVariant =
            row.read(productVariants.isProductVariant) ?? false;

        final int variantCount = hasVariant && isProductVariant
            ? row.read(productVariants.localId.count()) ?? 0
            : 0;

        final int newVariantCount = hasVariant && isProductVariant
            ? (row.read(masterSub.ref(productVariants.localId.count())) ?? 0)
            : 0;

        final String stockType = row.read(outletInventories.stockType) ?? '';

        final String remainingStock =
            "${(row.read(outletInventories.stock.sum()) ?? 0.0).toStringWithoutTrailingZero()} ${row.read(units.name) ?? ''}";

        final String displaySellingPrice = CurrencyFormat.formatToIdr(
          (row.read(outletInventories.sellingPrice.min()) ?? 0.0),
          0,
        );

        return Product(
          id: id,
          name: name,
          productImages: productImages,
          hasVariant: hasVariant,
          variantCount: variantCount,
          newVariantCount: newVariantCount - variantCount,
          stockType: stockType,
          remainingStock: remainingStock,
          displaySellingPrice: displaySellingPrice,
        );
      }).toList();
    });
  }

  Future<ProductDetail> getProductById(String id, bool isEditing) async {
    final productQuery = selectOnly(products)
      ..addColumns([
        products.localId,
        products.localImagePath,
        products.name,
        products.categoryId,
        products.hasVariant,
        products.unitId,
      ])
      ..where(products.localId.equals(id) & products.deletedAt.isNull())
      ..limit(1);

    final outletId = await SystemSetting.getCurrentOutletId();

    final productVariantQuery =
        selectOnly(productVariants).join([
            innerJoin(
              outletInventories,
              outletInventories.itemType.equals(ItemType.product_variant.type) &
                  outletInventories.itemId.equalsExp(productVariants.localId),
            ),
          ])
          ..addColumns([
            productVariants.localId,
            outletInventories.localId,
            productVariants.localImagePath,
            productVariants.serverImageUrl,
            productVariants.name,
            outletInventories.costPrice,
            outletInventories.sellingPrice,
            outletInventories.stockType,
            outletInventories.stock,
          ])
          ..where(
            productVariants.productId.equals(id) &
                productVariants.deletedAt.isNull(),
          )
          ..where(
            (isEditing
                    ? outletInventories.outletId.equals(outletId!)
                    : const Constant(true)) &
                outletInventories.deletedAt.isNull(),
          );

    final productRow = await productQuery.getSingle();

    final String localId = productRow.read(products.localId) ?? '';
    final String name = productRow.read(products.name) ?? '';
    final dynamic rawImages = productRow.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 String categoryId = productRow.read(products.categoryId) ?? '';
    final bool hasVariant = productRow.read(products.hasVariant) ?? false;
    final String unitId = productRow.read(products.unitId) ?? '';

    final productVariantRows = await productVariantQuery.get();

    List<ProductVariantDetail> variants = productVariantRows.map((row) {
      final String productVariantId = row.read(productVariants.localId) ?? '';
      final String outletInventoryId =
          row.read(outletInventories.localId) ?? '';
      final String image = row.read(productVariants.localImagePath) ?? '';
      final String name = row.read(productVariants.name) ?? '';
      final double costPrice = row.read(outletInventories.costPrice) ?? 0.0;
      final double sellingPrice =
          row.read(outletInventories.sellingPrice) ?? 0.0;
      final StockType stockType = StockType.values.byName(
        (row.read(outletInventories.stockType) ?? ''),
      );
      final double stock = row.read(outletInventories.stock) ?? 0.0;

      return ProductVariantDetail(
        productVariantId: productVariantId,
        outletInventoryId: outletInventoryId,
        image: image,
        name: name,
        costPrice: costPrice,
        sellingPrice: sellingPrice,
        stockType: stockType,
        stock: stock,
      );
    }).toList();

    return ProductDetail(
      localId: localId,
      productImages: productImages,
      name: name,
      categoryId: categoryId,
      hasVariant: hasVariant,
      variants: variants,
      unitId: unitId,
    );
  }

  Future<List<ProductWithVariant>> getNewVariants(String id) async {
    final outletId = await SystemSetting.getCurrentOutletId();
    final query =
        selectOnly(productVariants).join([
            innerJoin(
              outletInventories,
              outletInventories.itemType.equals(ItemType.product_variant.type) &
                  outletInventories.itemId.equalsExp(productVariants.localId),
            ),
          ])
          ..addColumns([
            productVariants.localId,
            outletInventories.localId,
            productVariants.localImagePath,
            productVariants.name,
            outletInventories.costPrice,
            outletInventories.sellingPrice,
            outletInventories.stockType,
            outletInventories.stock,
          ])
          ..where(
            productVariants.productId.equals(id) &
                productVariants.deletedAt.isNull(),
          )
          ..where(
            outletInventories.outletId.equals(outletId!).not() &
                outletInventories.deletedAt.isNull(),
          );

    final results = await query.get();

    return results.map((row) {
      final String productVariantId = row.read(productVariants.localId) ?? '';
      final String image = row.read(productVariants.localImagePath) ?? '';
      final String name = row.read(productVariants.name) ?? '';
      final double costPrice = row.read(outletInventories.costPrice) ?? 0.0;
      final double sellingPrice =
          row.read(outletInventories.sellingPrice) ?? 0.0;
      final double stock = row.read(outletInventories.stock) ?? 0.0;

      return ProductWithVariant(
        productVariantId: productVariantId,
        imagePath: image,
        name: name,
        stock: stock,
        costPrice: costPrice,
        sellingPrice: sellingPrice,
      );
    }).toList();
  }

  Future<DeleteProductSnapshot> getDeleteProductSnapshotById(String id) async {
    final productQuery = selectOnly(products)
      ..addColumns([products.localId, products.name, products.localImagePath])
      ..where(products.localId.equals(id) & products.deletedAt.isNull())
      ..limit(1);

    final productVariantQuery =
        selectOnly(productVariants).join([
            innerJoin(
              outletInventories,
              outletInventories.itemType.equals(ItemType.product_variant.type) &
                  outletInventories.itemId.equalsExp(productVariants.localId),
            ),
            innerJoin(
              outlets,
              outlets.localId.equalsExp(outletInventories.outletId),
            ),
          ])
          ..addColumns([
            productVariants.totalSold,
            outlets.localId,
            outlets.name,
            outlets.isMainOutlet,
          ])
          ..where(
            productVariants.productId.equals(id) &
                productVariants.deletedAt.isNull(),
          )
          ..where(outlets.deletedAt.isNull())
          ..where(outletInventories.deletedAt.isNull());

    final productRow = await productQuery.getSingle();

    final productVariantRows = await productVariantQuery.get();

    final String productId = productRow.read(products.localId) ?? '';
    final String name = productRow.read(products.name) ?? '';
    final dynamic rawImages = productRow.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 String? imagePath = productImages.isNotEmpty
        ? productImages.last
        : null;

    double totalSold = 0.0;

    final Map<String, Outlet> outletUses = {};

    for (final row in productVariantRows) {
      final String id = row.read(outlets.localId) ?? '';

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

      final bool isMainOutlet = row.read(outlets.isMainOutlet) ?? false;

      outletUses.putIfAbsent(
        id,
        () => Outlet(id: id, name: name, isMainOutlet: isMainOutlet),
      );

      totalSold += row.read(productVariants.totalSold) ?? 0.0;
    }

    final outletId = await SystemSetting.getCurrentOutletId();

    final currentOutlet = outletUses.values
        .where((outlet) => outlet.id == outletId)
        .first;

    return DeleteProductSnapshot(
      productId: productId,
      imagePath: imagePath,
      name: name,
      totalSold: totalSold,
      currentOutletName: currentOutlet.name,
      isMainOutlet: currentOutlet.isMainOutlet,
      totalOutletCount: outletUses.length,
    );
  }

  Future<bool> checkAlreadyProductByOutlet(String id) async {
    final productQuery = selectOnly(products)
      ..addColumns([products.localId])
      ..where(products.localId.equals(id) & products.deletedAt.isNull())
      ..limit(1);

    final outletId = await SystemSetting.getCurrentOutletId();

    final productVariantQuery =
        selectOnly(productVariants).join([
            innerJoin(
              outletInventories,
              outletInventories.itemType.equals(ItemType.product_variant.type) &
                  outletInventories.itemId.equalsExp(productVariants.localId),
            ),
          ])
          ..addColumns([productVariants.localId, outletInventories.outletId])
          ..where(
            productVariants.productId.equals(id) &
                productVariants.deletedAt.isNull(),
          )
          ..where(
            outletInventories.outletId.equals(outletId!) &
                outletInventories.deletedAt.isNull(),
          );

    final resultProduct = await productQuery.getSingleOrNull();

    final resultProductVariants = await productVariantQuery.get();

    return resultProduct != null && resultProductVariants.isNotEmpty;
  }

  Stream<List<ProductChoice>> watchAllProductChoices() async* {
    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.localImagePath,
            products.name,
            products.hasVariant,
          ])
          ..where(products.deletedAt.isNull())
          ..where(productVariants.deletedAt.isNull())
          ..where(outletInventories.deletedAt.isNull());

    yield* query.watch().map((rows) {
      final Map<String, ProductChoice> 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>();
        }

        String? imagePath;

        if (productImages.isNotEmpty) {
          imagePath = productImages.last;
        }

        groupedMap.putIfAbsent(
          productId,
          () => ProductChoice(
            id: productId,
            imagePath: imagePath,
            name: row.read(products.name) ?? '',
            hasVariant: row.read(products.hasVariant) ?? false,
          ),
        );
      }

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