import 'package:flutter/material.dart';
import 'package:qpay/http/http.dart';
import 'package:qpay/models/api_response.dart';
import 'package:qpay/screens/groups/models/group_batch_model.dart';
import 'package:qpay/models/pageable_model.dart';
import 'package:qpay/screens/home/home_controller.dart';
import 'package:qpay/screens/pay/pay_controller.dart';
import 'package:qpay/screens/transactions/transaction_model.dart';

import '../models/batch_item_update_model.dart';

class BatchScreenModel {
  List<GroupBatch> batches = [];
  List<GroupBatchItem> batchItems = [];
  GroupBatch? currentBatch;
  List<BillProvider> providers = [];
  List<PaymentProcessor> processors = [];
  BillProvider? selectedProvider;
  PaymentProcessor? selectedProcessor;
  bool isLoading = false;
  bool isLoadingMore = false;
  bool isActing = false;
  String? errorMessage;
  int currentPage = 0;
  int totalPages = 0;
  int totalElements = 0;
  int pageSize = 10;
  String searchQuery = '';
  String? statusFilter;
  String? currencyFilter;
  bool selectMode = false;
  Set<String> selectedBatchIds = {};

  // Batch items pagination state
  int batchItemsCurrentPage = 0;
  int batchItemsTotalPages = 0;
  int batchItemsTotalElements = 0;
  int batchItemsPageSize = 10;
  String batchItemsSearchQuery = '';
  String? batchItemsStatusFilter;
  String? batchItemsRecipientAccount;
  bool batchItemsIsLoadingMore = false;

  // Provider products cache: providerId -> list of products
  Map<String, List<BillProduct>> providerProducts = {};
  Set<String> loadingProviderProducts = {};
}

class BatchController extends ChangeNotifier {
  final BatchScreenModel model = BatchScreenModel();
  final Http http = Http();
  BuildContext context;
  bool _disposed = false;

  BatchController(this.context);

  @override
  void dispose() {
    _disposed = true;
    super.dispose();
  }

  dynamic _unwrap(dynamic response) {
    if (response is Map && response.containsKey('body')) {
      return response['body'];
    }
    return response;
  }

  String buildQueryParameters(Map<String, String> params) {
    if (params.isEmpty) {
      return '';
    }

    String query = '';
    params.forEach((key, value) {
      query += '$key=$value&';
    });
    return query.substring(0, query.length - 1);
  }

  Future<ApiResponse<List<BillProvider>>> loadProviders(String currency) async {
    try {
      final raw = await http.get(
        '/public/providers?currency=$currency&sort=priority,asc',
      );
      final response = _unwrap(raw);
      final List<dynamic> content;
      if (response is Map && response.containsKey('content')) {
        content = response['content'] as List<dynamic>;
      } else if (response is List) {
        content = response;
      } else {
        content = [];
      }
      model.providers = content
          .map((e) => BillProvider.fromJson(e as Map<String, dynamic>))
          .toList();
      if (model.providers.isNotEmpty) {
        model.selectedProvider = model.providers.first;
      }
      if (!_disposed) notifyListeners();
      return ApiResponse.success(model.providers);
    } catch (e) {
      logger.e(e);
      return ApiResponse.failure('Failed to load providers');
    }
  }

  Future<ApiResponse<List<PaymentProcessor>>> loadProcessors(currency) async {
    try {
      final raw = await http.get(
        '/public/payment-processors?currency=$currency',
      );
      final response = _unwrap(raw);
      final List<dynamic> content = response is List
          ? response
          : (response['content'] ?? []);
      model.processors = content
          .map((e) => PaymentProcessor.fromJson(e as Map<String, dynamic>))
          .toList();
      if (model.processors.isNotEmpty) {
        model.selectedProcessor = model.processors.first;
      }
      if (!_disposed) notifyListeners();
      return ApiResponse.success(model.processors);
    } catch (e) {
      logger.e(e);
      return ApiResponse.failure('Failed to load processors');
    }
  }

  Future<ApiResponse<List<BillProduct>>> loadProviderProducts(
    String providerId,
  ) async {
    if (model.loadingProviderProducts.contains(providerId)) {
      // Already loading, return cached data if available
      final cached = model.providerProducts[providerId];
      if (cached != null) {
        return ApiResponse.success(cached);
      }
      return ApiResponse.failure('Loading in progress');
    }

    model.loadingProviderProducts.add(providerId);
    if (!_disposed) notifyListeners();

    try {
      final raw = await http.get('/public/providers/$providerId/products');
      final response = _unwrap(raw);
      final List<dynamic> list = response is List
          ? response
          : (response['content'] ?? []);
      final products = list
          .map((e) => BillProduct.fromJson(e as Map<String, dynamic>))
          .toList();
      model.providerProducts[providerId] = products;
      model.loadingProviderProducts.remove(providerId);
      if (!_disposed) notifyListeners();
      return ApiResponse.success(products);
    } catch (e) {
      logger.e(e);
      model.loadingProviderProducts.remove(providerId);
      if (!_disposed) notifyListeners();
      return ApiResponse.failure('Failed to load products');
    }
  }

  Future<ApiResponse<List<GroupBatch>>> listBatches(
    String groupId, {
    int page = 0,
    int size = 10,
    String? search,
    String? status,
    String? currency,
  }) async {
    if (page == 0) {
      model.isLoading = true;
      model.batches = [];
      model.currentPage = 0;
    } else {
      model.isLoadingMore = true;
    }
    if (!_disposed) notifyListeners();

    try {
      final params = <String, String>{
        'recipientGroupId': groupId,
        'page': page.toString(),
        'size': size.toString(),
        'sort': 'createdAt,desc',
      };
      if (search != null && search.isNotEmpty) {
        params['description'] = search;
      }
      if (status != null && status.isNotEmpty) {
        params['status'] = status;
      }
      if (currency != null && currency.isNotEmpty) {
        params['currency'] = currency;
      }

      final raw = await http.get(
        '/group-batches?${buildQueryParameters(params)}',
      );
      final response = _unwrap(raw);
      final pageableModel = PageableModel.fromJson(
        response as Map<String, dynamic>,
      );

      final newBatches = pageableModel.content
          .map((e) => GroupBatch.fromJson(e as Map<String, dynamic>))
          .toList();

      if (page == 0) {
        model.batches = newBatches;
      } else {
        model.batches.addAll(newBatches);
      }
      model.currentPage = pageableModel.number;
      model.totalPages = pageableModel.totalPages;
      model.totalElements = pageableModel.totalElements;
      model.isLoading = false;
      model.isLoadingMore = false;

      if (!_disposed) notifyListeners();
      return ApiResponse.success(model.batches);
    } catch (e) {
      logger.e(e);
      model.errorMessage = e.toString();
      if (page == 0) {
        model.batches = [];
      }
      model.isLoading = false;
      model.isLoadingMore = false;
      if (!_disposed) notifyListeners();
      return ApiResponse.failure('Failed to load batches');
    }
  }

  Future<void> searchBatches(
    String groupId, {
    String? search,
    String? status,
    String? currency,
  }) async {
    model.searchQuery = search ?? '';
    model.statusFilter = status;
    model.currencyFilter = currency;
    await listBatches(
      groupId,
      page: 0,
      size: model.pageSize,
      search: search,
      status: status,
      currency: currency,
    );
  }

  Future<void> loadNextPage(String groupId) async {
    if (model.isLoadingMore || model.currentPage + 1 >= model.totalPages) {
      return;
    }
    await listBatches(
      groupId,
      page: model.currentPage + 1,
      size: model.pageSize,
      search: model.searchQuery.isNotEmpty ? model.searchQuery : null,
      status: model.statusFilter,
      currency: model.currencyFilter,
    );
  }

  /// Navigates to a specific page of batches (0-based index).
  /// Clears the current list and loads the requested page.
  Future<void> goToPage(String groupId, int page) async {
    if (model.isLoadingMore) return;
    if (page < 0 || page >= model.totalPages) return;
    if (page == model.currentPage) return;

    // Clear existing batches so listBatches replaces instead of appending
    model.batches = [];
    model.currentPage = 0;
    if (!_disposed) notifyListeners();

    await listBatches(
      groupId,
      page: page,
      size: model.pageSize,
      search: model.searchQuery.isNotEmpty ? model.searchQuery : null,
      status: model.statusFilter,
      currency: model.currencyFilter,
    );
  }

  /// Changes the page size for batches and reloads from page 0.
  Future<void> setPageSize(String groupId, int size) async {
    if (size == model.pageSize) return;
    model.pageSize = size;
    if (!_disposed) notifyListeners();
    // Reload from page 0 with the new size
    await searchBatches(
      groupId,
      search: model.searchQuery.isNotEmpty ? model.searchQuery : null,
      status: model.statusFilter,
      currency: model.currencyFilter,
    );
  }

  Future<ApiResponse<List<GroupBatchItem>>> listBatchItems(
    String batchId, {
    int page = 0,
    int size = 10,
    Map<String, String>? filters,
  }) async {
    if (page == 0) {
      model.isLoading = true;
      model.batchItems = [];
      model.batchItemsCurrentPage = 0;
    } else {
      model.batchItemsIsLoadingMore = true;
    }
    if (!_disposed) notifyListeners();

    try {
      final params = <String, String>{
        'groupBatchId': batchId,
        'page': page.toString(),
        'size': size.toString(),
        'sort': 'createdAt,desc',
      };
      if (filters != null) {
        params.addAll(filters);
      }

      final raw = await http.get(
        '/group-batches/items?${buildQueryParameters(params)}',
      );
      final response = _unwrap(raw);
      final pageableModel = PageableModel.fromJson(
        response as Map<String, dynamic>,
      );

      final newItems = pageableModel.content
          .map((e) => GroupBatchItem.fromJson(e as Map<String, dynamic>))
          .toList();

      if (page == 0) {
        model.batchItems = newItems;
      } else {
        model.batchItems.addAll(newItems);
      }
      model.batchItemsCurrentPage = pageableModel.number;
      model.batchItemsTotalPages = pageableModel.totalPages;
      model.batchItemsTotalElements = pageableModel.totalElements;
      model.isLoading = false;
      model.batchItemsIsLoadingMore = false;

      if (!_disposed) notifyListeners();
      return ApiResponse.success(model.batchItems);
    } catch (e) {
      logger.e(e);
      model.errorMessage = e.toString();
      if (page == 0) {
        model.batchItems = [];
      }
      model.isLoading = false;
      model.batchItemsIsLoadingMore = false;
      if (!_disposed) notifyListeners();
      return ApiResponse.failure('Failed to load batch items');
    }
  }

  Future<void> searchBatchItems(
    String batchId, {
    String? search,
    String? status,
    String? account,
  }) async {
    model.batchItemsSearchQuery = search ?? '';
    model.batchItemsStatusFilter = status;
    model.batchItemsRecipientAccount = account;
    await listBatchItems(
      batchId,
      page: 0,
      size: model.batchItemsPageSize,
      filters: _buildBatchItemFilters(
        search: search,
        status: status,
        account: account,
      ),
    );
  }

  Future<void> loadNextBatchItemsPage(String batchId) async {
    if (model.batchItemsIsLoadingMore ||
        model.batchItemsCurrentPage + 1 >= model.batchItemsTotalPages) {
      return;
    }
    await listBatchItems(
      batchId,
      page: model.batchItemsCurrentPage + 1,
      size: model.batchItemsPageSize,
      filters: _buildBatchItemFilters(
        search: model.batchItemsSearchQuery.isNotEmpty
            ? model.batchItemsSearchQuery
            : null,
        status: model.batchItemsStatusFilter,
        account: model.batchItemsRecipientAccount,
      ),
    );
  }

  /// Navigates to a specific page of batch items (0-based index).
  /// Clears the current list and loads the requested page.
  Future<void> goToBatchItemsPage(String batchId, int page) async {
    if (model.batchItemsIsLoadingMore) return;
    if (page < 0 || page >= model.batchItemsTotalPages) return;
    if (page == model.batchItemsCurrentPage) return;

    // Clear existing items so listBatchItems replaces instead of appending
    model.batchItems = [];
    model.batchItemsCurrentPage = 0;
    if (!_disposed) notifyListeners();

    await listBatchItems(
      batchId,
      page: page,
      size: model.batchItemsPageSize,
      filters: _buildBatchItemFilters(
        search: model.batchItemsSearchQuery.isNotEmpty
            ? model.batchItemsSearchQuery
            : null,
        status: model.batchItemsStatusFilter,
        account: model.batchItemsRecipientAccount,
      ),
    );
  }

  /// Changes the page size for batch items and reloads from page 0.
  void setBatchItemsPageSize(int size) {
    if (size == model.batchItemsPageSize) return;
    model.batchItemsPageSize = size;
    if (!_disposed) notifyListeners();
  }

  /// Builds the filters map for batch items queries from individual params.
  Map<String, String>? _buildBatchItemFilters({
    String? search,
    String? status,
    String? account,
  }) {
    final filters = <String, String>{};
    if (search != null && search.isNotEmpty) {
      filters['recipientName'] = search;
    }
    if (status != null && status.isNotEmpty) {
      filters['status'] = status;
    }
    if (account != null && account.isNotEmpty) {
      filters['recipientAccount'] = account;
    }
    return filters.isEmpty ? null : filters;
  }

  Future<ApiResponse<GroupBatch>> getBatch(
    String batchId,
    String workspaceId,
  ) async {
    model.isActing = true;
    if (!_disposed) notifyListeners();
    try {
      final raw = await http.get(
        '/group-batches/$batchId?workspaceId=$workspaceId',
      );
      final response = _unwrap(raw);
      final batch = GroupBatch.fromJson(response as Map<String, dynamic>);
      model.currentBatch = batch;
      model.isActing = false;
      if (!_disposed) notifyListeners();
      return ApiResponse.success(batch);
    } catch (e) {
      logger.e(e);
      model.isActing = false;
      if (!_disposed) notifyListeners();
      return ApiResponse.failure('Failed to load batch');
    }
  }

  Future<ApiResponse<GroupBatch>> createBatch(CreateBatchRequest req) async {
    model.isActing = true;
    if (!_disposed) notifyListeners();
    try {
      final raw = await http.post('/group-batches', req.toJson());
      final response = _unwrap(raw);
      final batch = GroupBatch.fromJson(response as Map<String, dynamic>);
      model.currentBatch = batch;
      model.isActing = false;
      if (!_disposed) notifyListeners();
      return ApiResponse.success(batch);
    } catch (e, s) {
      logger.e(e);
      logger.e(s);
      model.isActing = false;
      if (!_disposed) notifyListeners();
      return ApiResponse.failure('Failed to create batch');
    }
  }

  Future<ApiResponse<GroupBatch>> updateBatch(
    GroupBatchUpdateRequest request,
  ) async {
    model.isActing = true;
    if (!_disposed) notifyListeners();
    try {
      final raw = await http.put(
        '/group-batches/${request.id}',
        request.toJson(),
      );
      final response = _unwrap(raw);
      final batch = GroupBatch.fromJson(response as Map<String, dynamic>);
      // Update the batch in the local list if present
      final index = model.batches.indexWhere((b) => b.id == batch.id);
      if (index != -1) {
        model.batches[index] = batch;
      }
      model.currentBatch = batch;
      model.isActing = false;
      if (!_disposed) notifyListeners();
      return ApiResponse.success(batch);
    } catch (e, s) {
      logger.e(e);
      logger.e(s);
      model.isActing = false;
      if (!_disposed) notifyListeners();
      return ApiResponse.failure('Failed to update batch');
    }
  }

  Future<ApiResponse<List<GroupBatchItem>>> updateBatchItems(
    String batchId,
    GroupBatchItemUpdateList updateList,
  ) async {
    model.isActing = true;
    if (!_disposed) notifyListeners();
    try {
      final raw = await http.put(
        '/group-batches/$batchId/items',
        updateList.toJson(),
      );
      final response = _unwrap(raw);
      final items = (response as List<dynamic>)
          .map((e) => GroupBatchItem.fromJson(e as Map<String, dynamic>))
          .toList();
      // Update items in the local batch items list if the current batch matches
      model.batchItems = items;
      model.isActing = false;
      if (!_disposed) notifyListeners();
      return ApiResponse.success(items);
    } catch (e, s) {
      logger.e(e);
      logger.e(s);
      model.isActing = false;
      if (!_disposed) notifyListeners();
      return ApiResponse.failure('Failed to update batch items');
    }
  }

  Future<ApiResponse<GroupBatch>> confirmBatch(
    String batchId,
    String workspaceId,
    String authType,
  ) async {
    model.isActing = true;
    if (!_disposed) notifyListeners();
    try {
      final key =
          'batch-$batchId-confirm-${DateTime.now().millisecondsSinceEpoch}';
      final body = BatchActionRequest(
        workspaceId: workspaceId,
        authType: authType,
        idempotencyKey: key,
      );
      final raw = await http.post(
        '/group-batches/$batchId/confirm',
        body.toJson(),
      );
      final response = _unwrap(raw);
      final batch = GroupBatch.fromJson(response as Map<String, dynamic>);
      model.currentBatch = batch;
      model.isActing = false;
      if (!_disposed) notifyListeners();
      return ApiResponse.success(batch);
    } catch (e) {
      logger.e(e);
      model.isActing = false;
      if (!_disposed) notifyListeners();
      return ApiResponse.failure('Failed to confirm batch');
    }
  }

  Future<ApiResponse<TransactionModel>> requestBatch(
    String batchId,
    String workspaceId,
  ) async {
    model.isActing = true;
    if (!_disposed) notifyListeners();
    try {
      final key =
          'batch-$batchId-request-${DateTime.now().millisecondsSinceEpoch}';
      final body = BatchActionRequest(
        workspaceId: workspaceId,
        idempotencyKey: key,
      );
      final raw = await http.post(
        '/group-batches/$batchId/request',
        body.toJson(),
      );
      final response = _unwrap(raw);
      final txn = TransactionModel.fromJson(response as Map<String, dynamic>);
      model.isActing = false;
      if (!_disposed) notifyListeners();
      return ApiResponse.success(txn);
    } catch (e, s) {
      logger.e(e);
      logger.e(s);
      model.isActing = false;
      if (!_disposed) notifyListeners();
      return ApiResponse.failure('Failed to request batch payment');
    }
  }

  Future<ApiResponse<Map<String, dynamic>>> downloadBatchReport(
    String batchId,
    String workspaceId,
  ) async {
    model.isActing = true;
    if (!_disposed) notifyListeners();
    try {
      final raw = await http.get(
        '/group-batches/$batchId/report?workspaceId=$workspaceId',
      );
      final response = _unwrap(raw);
      model.isActing = false;
      if (!_disposed) notifyListeners();
      return ApiResponse.success(response as Map<String, dynamic>);
    } catch (e, s) {
      logger.e(e);
      logger.e(s);
      model.isActing = false;
      if (!_disposed) notifyListeners();
      return ApiResponse.failure('Failed to download batch report');
    }
  }

  Future<ApiResponse<TransactionModel>> pollBatch(
    String batchId,
    String workspaceId,
  ) async {
    model.isActing = true;
    if (!_disposed) notifyListeners();
    try {
      final key =
          'batch-$batchId-poll-${DateTime.now().millisecondsSinceEpoch}';
      final body = BatchActionRequest(
        workspaceId: workspaceId,
        idempotencyKey: key,
      );
      final raw = await http.post(
        '/group-batches/$batchId/poll',
        body.toJson(),
      );
      final response = _unwrap(raw);
      final batchTransaction = TransactionModel.fromJson(
        response as Map<String, dynamic>,
      );
      model.isActing = false;
      if (!_disposed) notifyListeners();
      if (batchTransaction.pollingStatus == 'SUCCESS') {
        return ApiResponse.success(batchTransaction);
      } else {
        return ApiResponse.failure(
          response['errorMessage'] ?? "Transaction failed",
        );
      }
    } catch (e, s) {
      logger.e(e);
      logger.e(s);
      model.isActing = false;
      if (!_disposed) notifyListeners();
      return ApiResponse.failure('Failed to poll batch');
    }
  }

  Future<void> deleteBatch({
    required String batchId,
    required String workspaceId,
  }) async {
    try {
      await http.delete('/group-batches/$batchId?workspaceId=$workspaceId');
      model.batches.removeWhere((b) => b.id == batchId);
      model.selectedBatchIds.remove(batchId);
      model.currentBatch = null;
      if (!_disposed) notifyListeners();
    } catch (e) {
      logger.e(e);
      model.errorMessage = e.toString();
      if (!_disposed) notifyListeners();
    }
  }

  Future<void> deleteSelectedBatches({
    required String groupId,
    required String workspaceId,
  }) async {
    final idsToDelete = List<String>.from(model.selectedBatchIds);
    model.selectMode = false;
    model.selectedBatchIds.clear();
    if (!_disposed) notifyListeners();

    for (final id in idsToDelete) {
      await deleteBatch(batchId: id, workspaceId: workspaceId);
    }

    // Refresh after deletion
    await listBatches(groupId, page: 0);
  }

  void selectProvider(BillProvider provider) {
    model.selectedProvider = provider;
    if (!_disposed) notifyListeners();
  }

  void selectProcessor(PaymentProcessor processor) {
    model.selectedProcessor = processor;
    if (!_disposed) notifyListeners();
  }

  void toggleSelectMode() {
    model.selectMode = !model.selectMode;
    if (!model.selectMode) {
      model.selectedBatchIds.clear();
    }
    if (!_disposed) notifyListeners();
  }

  void toggleBatchSelection(String batchId) {
    if (model.selectedBatchIds.contains(batchId)) {
      model.selectedBatchIds.remove(batchId);
    } else {
      model.selectedBatchIds.add(batchId);
    }
    if (!_disposed) notifyListeners();
  }
}
