import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:connectivity_plus/connectivity_plus.dart';
import 'package:dio/dio.dart';
import 'package:drift/drift.dart';
import 'package:niogu_app/core/constants/app_url.dart';
import 'package:niogu_app/core/database/app_database.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:rxdart/rxdart.dart';

class SyncService {
  final Dio _dio;
  final AppDatabase _db;
  StreamSubscription? _syncSubscription;
  bool _isProcessing = false;
  int _retryCount = 0;

  SyncService(this._dio, this._db);

  final Map<String, int> _tablePriority = {
    'tenants': 1,
    'tenant_payment_methods': 2,
    'campaigns': 3,
    'outlets': 4,
    'outlet_campaigns': 5,
    'users': 6,
    'units': 7,
    'categories': 8,
    'products': 9,
    'product_variants': 10,
    'raw_materials': 11,
    'outlet_inventories': 12,
    'receipt_items': 13,
    'customers': 14,
    'customer_addresses': 15,
    'product_likes': 16,
    'product_reviews': 17,
    'suppliers': 18,
    'orders': 19,
    'purchases': 20,
    'transactions': 21,
    'order_items': 22,
    'purchase_items': 23,
    'stock_cards': 24,
  };

  final _imageFields = [
    'server_logo_url',
    'server_photo_url',
    'server_banner_url',
    'server_image_url',
    'payment_proof_url',
    'product_image_url_snapshot',
    'item_image_url_snapshot',
  ];

  Future<void> startWatching() async {
    _syncSubscription =
        (_db.select(_db.syncQueues)..orderBy([
              (t) => OrderingTerm(expression: t.id, mode: OrderingMode.asc),
            ]))
            .watch()
            .debounceTime(const Duration(seconds: 3))
            .listen((queuedItems) async {
              if (queuedItems.isNotEmpty && !_isProcessing) {
                await processBatch();
              }
            });
  }

  Future<void> restartUpSync() async {
    final connectivityResult = await Connectivity().checkConnectivity();

    if (connectivityResult.any((con) => con == ConnectivityResult.none)) {
      throw Exception();
    }

    await (_db.update(
      _db.syncQueues,
    )..where((t) => t.retryCount.isBiggerOrEqualValue(5))).write(
      SyncQueuesCompanion(
        status: Value(SyncStatus.dirty.status),
        retryCount: Value(0),
      ),
    );
  }

  Future<void> processBatch() async {
    LogMessage.log.i("=== Process Batch ===");
    final isLoggedIn = await SystemSetting.isLoggedIn();

    if (!isLoggedIn) return;

    final connectivityResult = await Connectivity().checkConnectivity();

    if (connectivityResult.any((con) => con == ConnectivityResult.none)) return;

    if (_isProcessing) return;

    LogMessage.log.i("=== Start Process === ");

    _isProcessing = true;

    final String currentBatchId = DateTime.now().millisecondsSinceEpoch
        .toString();

    try {
      final rawItems =
          await (_db.select(_db.syncQueues)
                ..where(
                  (t) =>
                      (t.status.equals(SyncStatus.dirty.status) |
                          t.status.equals(SyncStatus.failed.status)) &
                      t.retryCount.isSmallerThanValue(5),
                )
                ..orderBy([
                  (t) => OrderingTerm(expression: t.id, mode: OrderingMode.asc),
                ]))
              .get();

      if (rawItems.isEmpty) {
        _isProcessing = false;
        return;
      }

      final sortedItems = List<SyncQueue>.from(rawItems);
      sortedItems.sort((a, b) {
        final int priorityA = _tablePriority[a.tableType] ?? 99;
        final int priorityB = _tablePriority[b.tableType] ?? 99;

        if (priorityA == priorityB) {
          return a.id.compareTo(b.id);
        }
        return priorityA.compareTo(priorityB);
      });

      final items = sortedItems.take(100).toList();

      await _db.batch((batch) {
        for (var item in items) {
          batch.update(
            _db.syncQueues,
            SyncQueuesCompanion(
              status: Value(SyncStatus.syncing.status),
              batchId: Value(currentBatchId),
            ),
            where: (t) => t.id.equals(item.id),
          );
        }
      });

      await Future.delayed(const Duration(seconds: 3));

      await _handleImageUploads(items);

      await Future.delayed(const Duration(seconds: 3));

      final refreshedItems = await (_db.select(
        _db.syncQueues,
      )..where((t) => t.id.isIn(items.map((e) => e.id)))).get();

      await Future.delayed(const Duration(seconds: 3));

      final batchPayload = {
        'batch_id': currentBatchId,
        'items': refreshedItems
            .map((item) => jsonEncode(item.payload))
            .toList(),
      };

      LogMessage.log.i("=== Process To Sync Up, Try $_retryCount ===");

      final response = await _dio.post(AppUrl.SYNC_UP, data: batchPayload);

      await Future.delayed(const Duration(seconds: 3));

      final report = response.data['report'];

      await _db.transaction(() async {
        for (var item in refreshedItems) {
          final hasError = (report['errors'] as List).any(
            (e) => e['local_id'] == item.tableLocalId,
          );

          if (!hasError) {
            final payload = Map<String, dynamic>.from(item.payload);

            await _updateMasterSyncStatus(
              item.tableType,
              item.tableLocalId,
              SyncStatus.synced.status,
              data: payload['data'],
            );

            await (_db.delete(
              _db.syncQueues,
            )..where((t) => t.id.equals(item.id))).go();
          }
        }
      });

      LogMessage.log.i("=== Process Sync Up Done ===");
    } catch (e, st) {
      LogMessage.log.e(
        "=== Process Sync Up Failed ===",
        error: e,
        stackTrace: st,
      );
      await _handleBatchFailure(currentBatchId, e.toString());
      rethrow;
    } finally {
      _isProcessing = false;
      _retryCount++;
    }
  }

  Future<void> _handleImageUploads(List<SyncQueue> items) async {
    for (final item in items) {
      final payload = Map<String, dynamic>.from(item.payload);
      final data = Map<String, dynamic>.from(payload['data']);
      bool isChanged = false;

      for (final field in _imageFields) {
        if (data.containsKey(field) && data[field] != null) {
          final imageValue = data[field];

          if (imageValue is List) {
            final List<String> serverPaths = [];

            for (final path in imageValue) {
              final pathStr = path.toString();

              if (_isLocalPath(pathStr)) {
                final folderType = _getFolderTypeFromPath(pathStr);

                final uploaded = await _uploadSingleFile(pathStr, folderType);

                if (uploaded != null) {
                  serverPaths.add(uploaded);
                  isChanged = true;
                } else {
                  serverPaths.add(pathStr);
                }
              } else {
                serverPaths.add(pathStr);
              }
            }
            data[field] = serverPaths;
          } else if (imageValue is String && _isLocalPath(imageValue)) {
            final folderType = _getFolderTypeFromPath(imageValue);

            final uploaded = await _uploadSingleFile(imageValue, folderType);

            if (uploaded != null) {
              data[field] = uploaded;
              isChanged = true;
            }
          }
        }
      }

      if (isChanged) {
        payload['data'] = data;
        await (_db.update(_db.syncQueues)..where((t) => t.id.equals(item.id)))
            .write(SyncQueuesCompanion(payload: Value(payload)));

        LogMessage.log.i(
          "Payload updated with server paths for item: ${item.tableLocalId}",
        );
      }
    }
  }

  bool _isLocalPath(String path) {
    return path.startsWith('/') ||
        path.contains('app_flutter') ||
        path.contains('cache');
  }

  String _getFolderTypeFromPath(String path) {
    if (path.contains('product_category_images')) {
      return 'product_category_images';
    } else if (path.contains('product_images')) {
      return 'product_images';
    } else if (path.contains('product_variant_images')) {
      return 'product_variant_images';
    } else if (path.contains('payment_proof_orders')) {
      return 'payment_proof_orders';
    } else if (path.contains('payment_proof_purchases')) {
      return 'payment_proof_purchases';
    } else if (path.contains('outlet_banner_images')) {
      return 'outlet_banner_images';
    } else if(path.contains('campaign_images')) {
      return 'campaign_images';
    } else if(path.contains('logo_images')) {
      return 'logo_images';
    }
    return 'others';
  }

  Future<String?> _uploadSingleFile(String localPath, String folderType) async {
    try {
      final file = File(localPath);
      if (!await file.exists()) {
        LogMessage.log.e("File fisik tidak ditemukan: $localPath");
        return null;
      }

      final fileName = localPath.split('/').last;

      final formData = FormData.fromMap({
        "type": folderType,
        "image": await MultipartFile.fromFile(localPath, filename: fileName),
      });

      final response = await _dio.post(AppUrl.UPLOAD_IMAGE, data: formData);

      if (response.statusCode == 200) {
        return response.data['data']['server_path'];
      }
      return null;
    } catch (e) {
      LogMessage.log.e("Gagal upload ke $folderType: $e");
      return null;
    }
  }

  Future<void> _updateMasterSyncStatus(
    String tableName,
    String localId,
    String status, {
    Map<String, dynamic>? data,
  }) async {
    String setClause = 'SET sync_status = ?, server_id = ?';

    List<Variable> variables = [Variable(status), Variable(localId)];

    if (data != null) {
      for (var field in _imageFields) {
        if (data.containsKey(field) && data[field] != null) {
          setClause += ', $field = ?';

          final value = data[field] is List
              ? jsonEncode(data[field])
              : data[field];

          variables.add(Variable(value));
        }
      }
    }

    variables.add(Variable(localId));

    await _db.customUpdate(
      'UPDATE $tableName $setClause WHERE local_id = ?',
      variables: variables,
      updates: {_getTableSet(TableType.values.byName(tableName))},
    );
  }

  ResultSetImplementation _getTableSet(TableType type) {
    switch (type) {
      case TableType.campaigns:
        return _db.campaigns;
      case TableType.categories:
        return _db.categories;
      case TableType.customers:
        return _db.customers;
      case TableType.order_items:
        return _db.orderItems;
      case TableType.orders:
        return _db.orders;
      case TableType.outlet_campaigns:
        return _db.outletCampaigns;
      case TableType.outlet_inventories:
        return _db.outletInventories;
      case TableType.outlets:
        return _db.outlets;
      case TableType.product_likes:
        return _db.productLikes;
      case TableType.product_reviews:
        return _db.productReviews;
      case TableType.product_variants:
        return _db.productVariants;
      case TableType.products:
        return _db.products;
      case TableType.purchase_items:
        return _db.purchaseItems;
      case TableType.purchases:
        return _db.purchases;
      case TableType.raw_materials:
        return _db.rawMaterials;
      case TableType.receipt_items:
        return _db.receiptItems;
      case TableType.stock_cards:
        return _db.stockCards;
      case TableType.suppliers:
        return _db.suppliers;
      case TableType.tenant_payment_methods:
        return _db.tenantPaymentMethods;
      case TableType.tenants:
        return _db.tenants;
      case TableType.transactions:
        return _db.transactions;
      case TableType.units:
        return _db.units;
      case TableType.users:
        return _db.users;
    }
  }

  Future<void> _handleBatchFailure(String batchId, String errorMessage) async {
    await Future.delayed(const Duration(milliseconds: 500));

    try {
      await _db.transaction(() async {
        final items = await (_db.select(
          _db.syncQueues,
        )..where((t) => t.batchId.equals(batchId))).get();

        for (var item in items) {
          final isPermanentFailure = item.retryCount >= 5;

          await (_db.update(
            _db.syncQueues,
          )..where((t) => t.id.equals(item.id))).write(
            SyncQueuesCompanion(
              status: Value(
                isPermanentFailure
                    ? SyncStatus.failed.status
                    : SyncStatus.dirty.status,
              ),
              batchId: const Value(null),
              retryCount: Value(item.retryCount + 1),
              lastError: Value(errorMessage),
            ),
          );
        }
      });

      LogMessage.log.e(
        "Batch $batchId failed. Items reverted to dirty for retry. with error $errorMessage",
      );
    } catch (e, st) {
      LogMessage.log.e(
        "Critical error in _handleBatchFailure: ${e.toString()}",
        error: e,
        stackTrace: st,
      );
      rethrow;
    }
  }

  void stopWatching() => _syncSubscription?.cancel();
}
