import 'dart:convert';

import 'package:drift/drift.dart';
import 'package:niogu_app/core/database/app_database.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/product_likes.dart';
import 'package:niogu_app/core/database/tables/product_reviews.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/enums/order_source.dart';
import 'package:niogu_app/features/online_store/domain/entities/online_store.dart';

part 'product_review_dao.g.dart';

@DriftAccessor(
  tables: [
    Customers,
    Units,
    Products,
    ProductVariants,
    ProductReviews,
    ProductLikes,
    Orders,
    OrderItems,
  ],
)
class ProductReviewDao extends DatabaseAccessor<AppDatabase>
    with _$ProductReviewDaoMixin {
  ProductReviewDao(AppDatabase db) : super(db);

  Stream<List<DisplayProductReview>> watchAllProductReviews() async* {
    final ratingAvg = productVariants.averageRating.avg();
    final ratingQuery = selectOnly(productVariants)
      ..addColumns([productVariants.productId, ratingAvg])
      ..groupBy([productVariants.productId]);

    final ratingSub = Subquery(ratingQuery, 'rating_sub');

    final onlineSold = CaseWhenExpression<double>(
      cases: [
        CaseWhen(
          orders.source.equals(OrderSource.online.source),
          then: orderItems.quantity,
        ),
      ],
      orElse: const Constant(0.0),
    ).sum();

    final posSold = CaseWhenExpression<double>(
      cases: [
        CaseWhen(
          orders.source.equals(OrderSource.pos.source),
          then: orderItems.quantity,
        ),
      ],
      orElse: const Constant(0.0),
    ).sum();

    final salesQuery =
        selectOnly(productVariants).join([
            leftOuterJoin(
              orderItems,
              orderItems.productVariantId.equalsExp(productVariants.localId),
            ),
            leftOuterJoin(orders, orders.localId.equalsExp(orderItems.orderId)),
          ])
          ..addColumns([productVariants.productId, onlineSold, posSold])
          ..groupBy([productVariants.productId]);

    final salesSub = Subquery(salesQuery, 'sales_sub');

    final query =
        selectOnly(products).join([
          innerJoin(units, units.localId.equalsExp(products.unitId)),

          leftOuterJoin(
            ratingSub,
            ratingSub
                .ref(productVariants.productId)
                .equalsExp(products.localId),
          ),

          leftOuterJoin(
            salesSub,
            salesSub.ref(productVariants.productId).equalsExp(products.localId),
          ),
        ])..addColumns([
          products.localId,
          products.localImagePath,
          products.name,
          units.name,
          ratingSub.ref(ratingAvg),
          salesSub.ref(onlineSold),
          salesSub.ref(posSold),
        ]);

    yield* query.watch().map((rows) {
      return rows.map((row) {
        final rating = row.read(ratingSub.ref(ratingAvg)) ?? 0.0;
        final online = row.read(salesSub.ref(onlineSold)) ?? 0.0;
        final pos = row.read(salesSub.ref(posSold)) ?? 0.0;

        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>();
        }

        return DisplayProductReview(
          id: row.read(products.localId)!,
          imagePath: productImages.lastOrNull,
          name: row.read(products.name) ?? '',
          unit: row.read(units.name) ?? '',
          averageRating: rating,
          totalSoldInOnlineStore: online,
          totalSoldInPos: pos,
        );
      }).toList();
    });
  }

  Stream<DetailProductReview> watchDetailProductReview(
    String productId,
  ) async* {
    final fiveStar = CaseWhenExpression<int>(
      cases: [
        CaseWhen(productReviews.rating.equals(5), then: const Constant(1)),
      ],
      orElse: const Constant(0),
    ).sum();

    final fourStar = CaseWhenExpression<int>(
      cases: [
        CaseWhen(productReviews.rating.equals(4), then: const Constant(1)),
      ],
      orElse: const Constant(0),
    ).sum();

    final threeStar = CaseWhenExpression<int>(
      cases: [
        CaseWhen(productReviews.rating.equals(3), then: const Constant(1)),
      ],
      orElse: const Constant(0),
    ).sum();

    final twoStar = CaseWhenExpression<int>(
      cases: [
        CaseWhen(productReviews.rating.equals(2), then: const Constant(1)),
      ],
      orElse: const Constant(0),
    ).sum();

    final oneStar = CaseWhenExpression<int>(
      cases: [
        CaseWhen(productReviews.rating.equals(1), then: const Constant(1)),
      ],
      orElse: const Constant(0),
    ).sum();

    final avgRating = productVariants.averageRating.avg();
    final totalRating = productVariants.totalRating.sum();

    final totalLike = productLikes.localId.count();

    final likeQuery = selectOnly(productLikes)
      ..addColumns([productLikes.productId, totalLike])
      ..where(productLikes.productId.equals(productId))
      ..where(productLikes.isLike.equals(true));

    final likeSub = Subquery(likeQuery, 'like_sub');

    final query =
        select(products).join([
            innerJoin(
              productVariants,
              productVariants.productId.equalsExp(products.localId),
            ),
            leftOuterJoin(
              productReviews,
              productReviews.productVariantId.equalsExp(
                productVariants.localId,
              ),
            ),
            leftOuterJoin(
              likeSub,
              likeSub.ref(productLikes.productId).equalsExp(products.localId),
            ),
          ])
          ..addColumns([
            products.localImagePath,
            avgRating,
            totalRating,
            fiveStar,
            fourStar,
            threeStar,
            twoStar,
            oneStar,
            likeSub.ref(totalLike),
          ])
          ..groupBy([products.localId])
          ..where(products.localId.equals(productId));

    yield* query.watchSingle().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 one = row.read(oneStar) ?? 0;
      final two = row.read(twoStar) ?? 0;
      final three = row.read(threeStar) ?? 0;
      final four = row.read(fourStar) ?? 0;
      final five = row.read(fiveStar) ?? 0;

      final total = one + two + three + four + five;
      final t = total == 0 ? 1.0 : total.toDouble();

      final distributions = [one / t, two / t, three / t, four / t, five / t];

      return DetailProductReview(
        imagePaths: productImages,
        name: row.read(products.name) ?? '',
        averageRating: row.read(avgRating) ?? 0.0,
        totalRating: row.read(totalRating) ?? 0,
        rating: RatingDistribution(distributions),
        totalLike: row.read(likeSub.ref(totalLike)) ?? 0,
      );
    });
  }

  Stream<List<ProductComment>> watchAllProductComments(
    String productId,
  ) async* {
    final query =
        selectOnly(productReviews).join([
            innerJoin(
              productVariants,
              productVariants.localId.equalsExp(
                productReviews.productVariantId,
              ),
            ),
          ])
          ..addColumns([
            productReviews.customerNameSnapshot,
            productReviews.rating,
            productVariants.name,
            productVariants.isProductVariant,
            productReviews.comment,
            productReviews.createdAt,
          ])
          ..orderBy([OrderingTerm.desc(productReviews.createdAt)])
          ..where(productVariants.productId.equals(productId));

    yield* query.watch().map((rows) {
      return rows.map((row) {
        final name = row.read(productReviews.customerNameSnapshot) ?? '';

        final rating = row.read(productReviews.rating) ?? 0;

        final variantName =
            (row.read(productVariants.isProductVariant) ?? false)
            ? row.read(productVariants.name)
            : null;

        final comment = row.read(productReviews.comment);

        final date = row.read(productReviews.createdAt) ?? DateTime.now();

        return ProductComment(
          name: name,
          rating: rating,
          variantName: variantName,
          comment: comment,
          date: date,
        );
      }).toList();
    });
  }
}
