import 'dart:async';

import 'package:flutter/foundation.dart';

import 'package:collection/collection.dart';

import 'package:pshared/models/describable.dart';
import 'package:pshared/models/organization/bound.dart';
import 'package:pshared/models/payment/methods/data.dart';
import 'package:pshared/models/payment/methods/type.dart';
import 'package:pshared/models/payment/type.dart';
import 'package:pshared/models/permissions/bound.dart';
import 'package:pshared/models/recipient/payment_method_draft.dart';
import 'package:pshared/models/storable.dart';
import 'package:pshared/provider/organizations.dart';
import 'package:pshared/provider/recipient/provider.dart';
import 'package:pshared/service/recipient/pmethods.dart';
import 'package:pshared/utils/exception.dart';


class RecipientMethodsCacheProvider extends ChangeNotifier {
  late OrganizationsProvider _organizations;

  final Map<String, List<PaymentMethod>> _methodsByRecipient = {};
  final Map<String, bool> _loadingByRecipient = {};
  final Map<String, Object?> _errorByRecipient = {};
  Set<String> _trackedRecipients = {};

  List<PaymentMethod> methodsForRecipient(String recipientId) =>
      List<PaymentMethod>.unmodifiable(_methodsByRecipient[recipientId] ?? const []);

  bool isLoadingFor(String recipientId) => _loadingByRecipient[recipientId] == true;
  Object? errorFor(String recipientId) => _errorByRecipient[recipientId];
  bool hasMethodsFor(String recipientId) => _methodsByRecipient.containsKey(recipientId);
  bool isTrackingRecipient(String recipientId) => _trackedRecipients.contains(recipientId);

  void updateProviders(OrganizationsProvider organizations, RecipientsProvider recipients) {
    _organizations = organizations;
    if (!_organizations.isOrganizationSet) return;

    final nextRecipients = recipients.items.map((r) => r.id).toSet();
    if (setEquals(_trackedRecipients, nextRecipients)) return;

    final removed = _trackedRecipients.difference(nextRecipients);
    for (final recipientId in removed) {
      _methodsByRecipient.remove(recipientId);
      _loadingByRecipient.remove(recipientId);
      _errorByRecipient.remove(recipientId);
    }

    final added = nextRecipients.difference(_trackedRecipients);
    _trackedRecipients = nextRecipients;

    for (final recipientId in added) {
      unawaited(_loadRecipientMethods(recipientId));
    }
  }

  Future<void> refreshRecipient(String recipientId) => _loadRecipientMethods(recipientId, force: true);

  Future<void> syncRecipientMethods({
    required String recipientId,
    required List<RecipientMethodDraft> methods,
    required Map<PaymentType, String> names,
  }) async {
    await _ensureLoaded(recipientId);
    final current = List<PaymentMethod>.from(_methodsByRecipient[recipientId] ?? const []);
    final currentById = {for (final method in current) method.id: method};
    final desired = methods.where((m) => m.data != null).toList();
    final desiredExisting = desired.where((m) => m.existing != null).toList();
    final desiredExistingIds = desiredExisting.map((m) => m.existing!.id).toSet();

    for (final method in current.toList()) {
      if (!desiredExistingIds.contains(method.id)) {
        await PaymentMethodService.delete(method);
        current.removeWhere((m) => m.id == method.id);
      }
    }

    for (final entry in desiredExisting) {
      final existing = entry.existing;
      final data = entry.data;
      if (existing == null || data == null) continue;
      final currentMethod = currentById[existing.id] ?? existing;
      final updated = currentMethod.copyWith(data: data);
      final updatedList = await PaymentMethodService.update(updated);
      final updatedMethod = updatedList.firstWhereOrNull((m) => m.id == updated.id) ?? updated;
      final index = current.indexWhere((m) => m.id == updatedMethod.id);
      if (index != -1) {
        current[index] = updatedMethod;
      }
    }

    for (final entry in desired.where((m) => m.existing == null)) {
      final data = entry.data;
      if (data == null) continue;
      final type = entry.type;
      final created = await _createMethod(
        recipientId: recipientId,
        data: data,
        name: names[type] ?? type.name,
      );
      current.add(created);
    }

    _methodsByRecipient[recipientId] = _sortedMethods(current);
    notifyListeners();
  }

  Future<void> _ensureLoaded(String recipientId) async {
    if (_methodsByRecipient.containsKey(recipientId)) return;
    await _loadRecipientMethods(recipientId);
  }

  Future<void> _loadRecipientMethods(String recipientId, {bool force = false}) async {
    if (!force && _loadingByRecipient[recipientId] == true) return;
    _setLoading(recipientId, true);
    try {
      final list = await PaymentMethodService.list(_organizations.current.id, recipientId);
      _methodsByRecipient[recipientId] = _sortedMethods(list);
      _errorByRecipient.remove(recipientId);
    } catch (e) {
      _errorByRecipient[recipientId] = toException(e);
    } finally {
      _setLoading(recipientId, false);
    }
  }

  Future<PaymentMethod> _createMethod({
    required String recipientId,
    required PaymentMethodData data,
    required String name,
  }) async {
    final organizationRef = _organizations.current.id;
    final method = PaymentMethod(
      storable: newStorable(),
      permissionBound: newPermissionBound(
        organizationBound: newOrganizationBound(organizationRef: organizationRef),
      ),
      recipientRef: recipientId,
      data: data,
      describable: newDescribable(name: name),
    );
    final created = await PaymentMethodService.create(organizationRef, method);
    return created.first;
  }

  void _setLoading(String recipientId, bool value) {
    _loadingByRecipient[recipientId] = value;
    notifyListeners();
  }

  List<PaymentMethod> _sortedMethods(List<PaymentMethod> methods) =>
      methods.toList()..sort((a, b) => a.storable.createdAt.compareTo(b.storable.createdAt));
}