import 'dart:convert';

import 'package:drift/drift.dart';
import 'package:niogu_app/core/database/app_database.dart';
import 'package:niogu_app/core/database/tables/campaigns.dart';
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/sync_queues.dart';
import 'package:niogu_app/core/database/tables/tenant_payment_methods.dart';
import 'package:niogu_app/core/database/tables/tenants.dart';
import 'package:niogu_app/core/database/tables/units.dart';
import 'package:niogu_app/core/entities/custom_sync_queue.dart';
import 'package:niogu_app/core/enums/delivery_fee_type.dart';
import 'package:niogu_app/core/enums/item_type.dart';
import 'package:niogu_app/core/enums/member_level.dart';
import 'package:niogu_app/core/enums/review_display_policy.dart';
import 'package:niogu_app/core/enums/review_display_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/enums/table_type.dart';
import 'package:niogu_app/core/system/system_setting.dart';
import 'package:niogu_app/core/utils/log_message.dart';
import 'package:niogu_app/features/online_store/domain/entities/online_store.dart';
import 'package:niogu_app/features/profile/domain/entities/profile.dart';

part 'tenant_dao.g.dart';

@DriftAccessor(
  tables: [
    Units,
    Categories,
    Products,
    ProductVariants,
    OutletInventories,
    Outlets,
    Tenants,
    TenantPaymentMethods,
    Campaigns,
    SyncQueues,
  ],
)
class TenantDao extends DatabaseAccessor<AppDatabase> with _$TenantDaoMixin {
  TenantDao(AppDatabase db) : super(db);

  Stream<double> watchBusinessCompleteness() async* {
    final query = selectOnly(tenants)
      ..addColumns([
        tenants.localLogoPath,
        tenants.businessEmail,
        tenants.businessPhone,
        tenants.businessType,
      ])
      ..limit(1);

    yield* query.watchSingle().map((row) {
      double percent = 0.2;

      final logo = row.read(tenants.localLogoPath);

      final email = row.read(tenants.businessEmail);

      final phone = row.read(tenants.businessPhone);

      final category = row.read(tenants.businessType);

      if (logo != null) {
        percent += 0.2;
      }

      if (email != null && email.isNotEmpty) {
        percent += 0.2;
      }

      if (phone != null && phone.isNotEmpty) {
        percent += 0.2;
      }

      if (category != null && category.isNotEmpty) {
        percent += 0.2;
      }

      return percent;
    });
  }

  Stream<MemberLevel> watchMemberLevel() async* {
    final query = selectOnly(tenants)
      ..addColumns([tenants.subscriptionLevel])
      ..limit(1);

    yield* query.watchSingle().map((row) {
      final level = row.read(tenants.subscriptionLevel)!;

      return MemberLevel.values.byName(level);
    });
  }

  Future<BusinessInfo> getBusinessInfo() async {
    final query = selectOnly(tenants)
      ..addColumns([
        tenants.localLogoPath,
        tenants.businessCode,
        tenants.businessName,
        tenants.businessEmail,
        tenants.businessPhone,
        tenants.businessType,
      ])
      ..limit(1);

    final result = await query.getSingle();

    return BusinessInfo(
      logoPath: result.read(tenants.localLogoPath),
      businessCode: result.read(tenants.businessCode)!,
      businessName: result.read(tenants.businessName) ?? '',
      businessEmail: result.read(tenants.businessEmail) ?? '',
      businessPhoneNumber: result.read(tenants.businessPhone) ?? '',
      businessType: result.read(tenants.businessType) ?? '',
    );
  }

  Future<int> updateBusinessInfo(BusinessInfo business) async {
    return transaction(() async {
      final now = DateTime.now();

      final currentTenantId = await SystemSetting.getTenantId();

      final rowsAffected = (await update(tenants)).write(
        TenantsCompanion(
          localLogoPath: Value(business.logoPath),
          businessName: Value(business.businessName),
          businessEmail: Value(business.businessEmail),
          businessPhone: Value(business.businessPhoneNumber),
          businessType: Value(business.businessType),
          syncStatus: Value(SyncStatus.dirty.status),
          updatedAt: Value(now),
        ),
      );

      final payload = SyncPayload(
        table: TableType.tenants.type,
        localId: currentTenantId!,
        action: SyncAction.update.action,
        data: {
          'uuid': currentTenantId,
          'server_logo_url': business.logoPath,
          'business_name': business.businessName,
          'business_email': business.businessEmail,
          'business_phone': business.businessPhoneNumber,
          'business_type': business.businessType,
          'updated_at': now.toIso8601String(),
        },
      ).toJson();

      final syncQueueComp = SyncQueuesCompanion(
        tableType: Value(TableType.tenants.type),
        tableLocalId: Value(currentTenantId),
        action: Value(SyncAction.update.action),
        status: Value(SyncStatus.dirty.status),
        payload: Value(payload),
      );

      final existing =
          await (select(syncQueues)..where(
                (tbl) =>
                    tbl.tableType.equals(TableType.tenants.type) &
                    tbl.tableLocalId.equals(currentTenantId),
              ))
              .getSingleOrNull();

      if (existing != null) {
        await (update(
          syncQueues,
        )..where((tbl) => tbl.id.equals(existing.id))).write(syncQueueComp);
      } else {
        await into(syncQueues).insert(syncQueueComp);
      }

      return rowsAffected;
    });
  }

  Future<ConfigureOnlineStore> getConfigureOnlineStore() async {
    final queryTenant = selectOnly(tenants)
      ..addColumns([
        tenants.onlinStoreName,
        tenants.onlineOpenTime,
        tenants.onlineCloseTime,
        tenants.isCloseService,
        tenants.preparationTimeMinutes,
        tenants.orderIntervalMinutes,
      ])
      ..limit(1);

    final queryProduct = selectOnly(productVariants)
      ..addColumns([productVariants.localId])
      ..where(
        productVariants.isVisibleOnline & productVariants.deletedAt.isNull(),
      );

    final tenantRow = await queryTenant.getSingle();

    final productRows = await queryProduct.get();

    final List<String> productIds = productRows
        .map((row) => row.read(productVariants.localId) ?? '')
        .toList();

    return ConfigureOnlineStore(
      onlineStoreName: tenantRow.read(tenants.onlinStoreName) ?? '',
      onlineOpenTime: tenantRow.read(tenants.onlineOpenTime) ?? '',
      onlineCloseTime: tenantRow.read(tenants.onlineCloseTime) ?? '',
      isCloseService: tenantRow.read(tenants.isCloseService) ?? false,
      preparationTimeMinutes:
          tenantRow.read(tenants.preparationTimeMinutes) ?? 0,
      orderIntervalMinutes: tenantRow.read(tenants.orderIntervalMinutes) ?? 0,
      productIds: productIds,
    );
  }

  Future<void> configureOnlineStore(ConfigureOnlineStore configure) async {
    await transaction(() async {
      final now = DateTime.now();

      final currentTenantId = await SystemSetting.getTenantId();

      await batch((batch) async {
        batch.update(
          tenants,
          TenantsCompanion(
            onlinStoreName: Value(configure.onlineStoreName),
            onlineOpenTime: Value(configure.onlineOpenTime),
            onlineCloseTime: Value(configure.onlineCloseTime),
            isCloseService: Value(configure.isCloseService),
            preparationTimeMinutes: Value(configure.preparationTimeMinutes),
            orderIntervalMinutes: Value(configure.orderIntervalMinutes),
            syncStatus: Value(SyncStatus.dirty.status),
            updatedAt: Value(now),
          ),
        );

        final existing =
            await (select(syncQueues)..where(
                  (tbl) =>
                      tbl.tableType.equals(TableType.tenants.type) &
                      tbl.tableLocalId.equals(currentTenantId!),
                ))
                .getSingleOrNull();

        if (existing != null) {
          CustomSyncQueue.updateSyncQueue(
            type: TableType.tenants,
            id: currentTenantId!,
            action: SyncAction.update,
            batch: batch,
            syncQueues: syncQueues,
            data: {
              'uuid': currentTenantId,
              'online_store_name': configure.onlineStoreName,
              'online_open_time': configure.onlineOpenTime,
              'online_close_time': configure.onlineCloseTime,
              'is_close_service': configure.isCloseService,
              'preparation_time_minutes': configure.preparationTimeMinutes,
              'order_interval_minutes': configure.orderIntervalMinutes,
              'updated_at': now.toIso8601String(),
            },
          );
        } else {
          CustomSyncQueue.addToSyncQueue(
            type: TableType.tenants,
            id: currentTenantId!,
            action: SyncAction.update,
            batch: batch,
            syncQueues: syncQueues,
            data: {
              'uuid': currentTenantId,
              'online_store_name': configure.onlineStoreName,
              'online_open_time': configure.onlineOpenTime,
              'online_close_time': configure.onlineCloseTime,
              'is_close_service': configure.isCloseService,
              'preparation_time_minutes': configure.preparationTimeMinutes,
              'order_interval_minutes': configure.orderIntervalMinutes,
              'updated_at': now.toIso8601String(),
            },
          );
        }

        batch.update(
          productVariants,
          ProductVariantsCompanion(
            isVisibleOnline: Value(true),
            syncStatus: Value(SyncStatus.dirty.status),
            updatedAt: Value(now),
          ),
          where: (tbl) => tbl.localId.isIn(configure.productIds),
        );

        batch.update(
          productVariants,
          ProductVariantsCompanion(
            isVisibleOnline: Value(false),
            syncStatus: Value(SyncStatus.dirty.status),
            updatedAt: Value(now),
          ),
          where: (tbl) => tbl.localId.isNotIn(configure.productIds),
        );

        CustomSyncQueue.addToSyncQueue(
          type: TableType.product_variants,
          id: 'BULK_VISIBILITY_${now.millisecondsSinceEpoch}',
          action: SyncAction.bulk_update,
          batch: batch,
          syncQueues: syncQueues,
          data: {
            'visible_ids': configure.productIds,
            'updated_at': now.toIso8601String(),
          },
        );
      });
    });
  }

  Stream<List<OnlineStoreProduct>> watchAllOnlineStoreProducts() async* {
    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,
            products.name,
            products.localImagePath,
            products.hasVariant,
            products.deletedAt,
            productVariants.localId,
            productVariants.name,
            productVariants.isProductVariant,
            productVariants.deletedAt,
            outletInventories.stockType,
            outletInventories.stock,
            units.name,
            outletInventories.sellingPrice,
            outletInventories.deletedAt,
          ])
          ..orderBy([OrderingTerm.desc(products.createdAt)]);

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

      for (final row in rows) {
        final productId = row.read(products.localId)!;

        final stockType = StockType.values.byName(
          (row.read(outletInventories.stockType) ?? ''),
        );

        final remainingStock = row.read(outletInventories.stock) ?? 0.0;

        final sellingPrice = row.read(outletInventories.sellingPrice) ?? 0.0;

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

        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,
          () => OnlineStoreProduct(
            id: productId,
            name: row.read(products.name) ?? '',
            stockType: stockType,
            remainingStock: remainingStock,
            unit: row.read(units.name) ?? '',
            sellingPrice: sellingPrice,
            imagePath: productImages.lastOrNull,
            hasVariant: hasVariant,
            variants: [],
            hasBeenDelete: productHasBeenDelete,
          ),
        );

        groupedMap[productId]!.variants.add(
          VariantOnlineStoreProduct(
            id: row.read(productVariants.localId)!,
            name: row.read(productVariants.name) ?? '',
            stockType: stockType,
            remainingStock: remainingStock,
            unit: row.read(units.name) ?? '',
            sellingPrice: sellingPrice,
            isProductVariant:
                row.read(productVariants.isProductVariant) ?? false,
            hasBeenDelete: variantHasBeenDelete,
          ),
        );
      }

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

  Future<ShippingConfiguration> getShippingConfiuration() async {
    final query = selectOnly(tenants)
      ..addColumns([
        tenants.allowCod,
        tenants.deliveryFeeType,
        tenants.maxDeliveryRadiusKm,
        tenants.deliveryFlatFee,
        tenants.deliveryFeePerKm,
        tenants.deliveryBaseFee,
      ])
      ..limit(1);

    final result = await query.getSingle();

    return ShippingConfiguration(
      allowCOD: result.read(tenants.allowCod) ?? false,
      deliveryFeeType: DeliveryFeeType.values.byName(
        (result.read(tenants.deliveryFeeType) ?? ''),
      ),
      maxDeliveryRadiusKm: result.read(tenants.maxDeliveryRadiusKm) ?? 0.0,
      deliveryFlatFee: result.read(tenants.deliveryFlatFee) ?? 0.0,
      deliveryFeePerKm: result.read(tenants.deliveryFeePerKm) ?? 0.0,
      deliveryBaseFee: result.read(tenants.deliveryBaseFee) ?? 0.0,
    );
  }

  Future<int> shippingConfiguration(ShippingConfiguration configure) async {
    return transaction(() async {
      final now = DateTime.now();

      final currentTenantId = await SystemSetting.getTenantId();

      final rowsAffected = (await update(tenants).write(
        TenantsCompanion(
          allowCod: Value(configure.allowCOD),
          deliveryFeeType: Value(configure.deliveryFeeType.type),
          maxDeliveryRadiusKm: Value(configure.maxDeliveryRadiusKm),
          deliveryFlatFee: Value(configure.deliveryFlatFee),
          deliveryFeePerKm: Value(configure.deliveryFeePerKm),
          deliveryBaseFee: Value(configure.deliveryBaseFee),
          syncStatus: Value(SyncStatus.dirty.status),
          updatedAt: Value(now),
        ),
      ));

      final payload = SyncPayload(
        table: TableType.tenants.type,
        localId: currentTenantId!,
        action: SyncAction.update.action,
        data: {
          'uuid': currentTenantId,
          'allow_cod': configure.allowCOD,
          'delivery_fee_type': configure.deliveryFeeType.type,
          'max_delivery_radius_km': configure.maxDeliveryRadiusKm,
          'delivery_flat_fee': configure.deliveryFlatFee,
          'delivery_fee_per_km': configure.deliveryFeePerKm,
          'delivery_base_fee': configure.deliveryBaseFee,
          'updated_at': now.toIso8601String(),
        },
      ).toJson();

      final syncQueueComp = SyncQueuesCompanion(
        tableType: Value(TableType.tenants.type),
        tableLocalId: Value(currentTenantId),
        action: Value(SyncAction.update.action),
        status: Value(SyncStatus.dirty.status),
        payload: Value(payload),
      );

      final existing =
          await (select(syncQueues)..where(
                (tbl) =>
                    tbl.tableType.equals(TableType.tenants.type) &
                    tbl.tableLocalId.equals(currentTenantId),
              ))
              .getSingleOrNull();

      if (existing != null) {
        await (update(
          syncQueues,
        )..where((tbl) => tbl.id.equals(existing.id))).write(syncQueueComp);
      } else {
        await into(syncQueues).insert(syncQueueComp);
      }

      return rowsAffected;
    });
  }

  Future<List<ConfigurePaymentMethod>> getConfigurePaymentMethod() async {
    final query = selectOnly(tenantPaymentMethods)
      ..addColumns([
        tenantPaymentMethods.localId,
        tenantPaymentMethods.sourceName,
        tenantPaymentMethods.accountNumber,
        tenantPaymentMethods.receipentName,
      ]);

    final results = await query.get();

    return results.map((row) {
      return ConfigurePaymentMethod(
        localId: row.read(tenantPaymentMethods.localId)!,
        sourceName: row.read(tenantPaymentMethods.sourceName) ?? '',
        accountNumber: row.read(tenantPaymentMethods.accountNumber) ?? '',
        receipentName: row.read(tenantPaymentMethods.receipentName) ?? '',
      );
    }).toList();
  }

  Future<void> paymentMethodConfiguration(
    List<TenantPaymentMethodsCompanion> companions,
  ) async {
    try {
      await transaction(() async {
        final now = DateTime.now();

        await batch((batch) {
          for (final companion in companions) {
            batch.insert(
              tenantPaymentMethods,
              companion,
              onConflict: DoUpdate((old) {
                return companion.copyWith(
                  sourceName: companion.sourceName,
                  accountNumber: companion.accountNumber,
                  receipentName: companion.receipentName,
                  syncStatus: Value(SyncStatus.dirty.status),
                  updatedAt: Value(now),
                );
              }),
            );

            CustomSyncQueue.addToSyncQueue(
              type: TableType.tenant_payment_methods,
              id: companion.localId.value,
              action: SyncAction.update_or_create,
              batch: batch,
              syncQueues: syncQueues,
              data: {
                'uuid': companion.localId.value,
                'source_name': companion.sourceName.value,
                'account_number': companion.accountNumber.value,
                'receipent_name': companion.receipentName.value,
                'updated_at': now.toIso8601String(),
              },
            );
          }
        });
      });
    } catch (e, st) {
      LogMessage.log.e(e.toString(), error: e, stackTrace: st);
      rethrow;
    }
  }

  Future<RatingConfiguration> getRatingConfiguration() async {
    final query = selectOnly(tenants)
      ..addColumns([tenants.reviewDisplayType, tenants.reviewDisplayPolicy])
      ..limit(1);

    final result = await query.getSingle();

    return RatingConfiguration(
      type: ReviewDisplayType.values.byName(
        result.read(tenants.reviewDisplayType)!,
      ),
      policy: ReviewDisplayPolicy.values.byName(
        result.read(tenants.reviewDisplayPolicy)!,
      ),
    );
  }

  Future<int> ratingConfiguration(RatingConfiguration configuration) async {
    return await transaction(() async {
      final now = DateTime.now();

      final currentTenantId = await SystemSetting.getTenantId();

      final rowsAffected = (await update(tenants)).write(
        TenantsCompanion(
          reviewDisplayType: Value(configuration.type.type),
          reviewDisplayPolicy: Value(configuration.policy.policy),
          syncStatus: Value(SyncStatus.dirty.status),
          updatedAt: Value(now),
        ),
      );

      final payload = SyncPayload(
        table: TableType.tenants.type,
        localId: currentTenantId!,
        action: SyncAction.update.action,
        data: {
          'uuid': currentTenantId,
          'review_display_type': configuration.type.type,
          'review_display_policy': configuration.policy.policy,
          'updated_at': now.toIso8601String(),
        },
      ).toJson();

      final syncQueueComp = SyncQueuesCompanion(
        tableType: Value(TableType.tenants.type),
        tableLocalId: Value(currentTenantId),
        action: Value(SyncAction.update.action),
        status: Value(SyncStatus.dirty.status),
        payload: Value(payload),
      );

      final existing =
          await (select(syncQueues)..where(
                (tbl) =>
                    tbl.tableType.equals(TableType.tenants.type) &
                    tbl.tableLocalId.equals(currentTenantId),
              ))
              .getSingleOrNull();

      if (existing != null) {
        await (update(
          syncQueues,
        )..where((tbl) => tbl.id.equals(existing.id))).write(syncQueueComp);
      } else {
        await into(syncQueues).insert(syncQueueComp);
      }

      return rowsAffected;
    });
  }

  Stream<List<ProductCampaign>> watchAllProductCampaigns() async* {
    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,
            products.localImagePath,
            products.name,
            outletInventories.stockType,
            outletInventories.stock.sum(),
            units.name,
          ])
          ..groupBy([products.localId])
          ..orderBy([OrderingTerm.desc(products.createdAt)])
          ..where(products.deletedAt.isNull())
          ..where(productVariants.deletedAt.isNull())
          ..where(outletInventories.deletedAt.isNull());

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

        List<String?> productImages = [];

        if (rawImages is String) {
          productImages = (jsonDecode(rawImages) as List).cast<String>();
        } else if (rawImages is List) {
          productImages = (rawImages).cast<String>();
        }

        return ProductCampaign(
          id: row.read(products.localId)!,
          imagePath: productImages.lastOrNull,
          name: row.read(products.name) ?? '',
          stockType: StockType.values.byName(
            (row.read(outletInventories.stockType)!),
          ),
          remainingStock: row.read(outletInventories.stock.sum()) ?? 0.0,
          unit: row.read(units.name) ?? '',
        );
      }).toList();
    });
  }

  Stream<List<CategoryCampaign>> watchAllCategoryCampaigns() async* {
    final query =
        selectOnly(categories).join([
            leftOuterJoin(
              products,
              products.categoryId.equalsExp(categories.localId),
            ),
          ])
          ..addColumns([
            categories.localId,
            categories.localImagePath,
            categories.name,
            products.localId.count(),
          ])
          ..groupBy([categories.localId])
          ..orderBy([OrderingTerm.desc(products.localId.count())]);

    yield* query.watch().map((rows) {
      return rows.map((row) {
        return CategoryCampaign(
          id: row.read(categories.localId)!,
          imagePath: row.read(categories.localImagePath),
          name: row.read(categories.name) ?? '',
          productUseAmount: row.read(products.localId.count()) ?? 0,
        );
      }).toList();
    });
  }

  Stream<List<TargetOutlet>> watchAllTargetOutlets() async* {
    final query = selectOnly(outlets)
      ..addColumns([outlets.localId, outlets.name, outlets.fullAddress])
      ..where(outlets.deletedAt.isNull());

    yield* query.watch().map((rows) {
      return rows.map((row) {
        return TargetOutlet(
          id: row.read(outlets.localId)!,
          name: row.read(outlets.name) ?? '',
          address: row.read(outlets.fullAddress),
        );
      }).toList();
    });
  }
}
