import 'dart:async'; import 'dart:math'; import 'package:flutter/foundation.dart'; import 'package:collection/collection.dart'; import 'package:pshared/models/currency.dart'; import 'package:pshared/models/describable.dart'; import 'package:pshared/models/ledger/account.dart'; import 'package:pshared/models/payment/wallet.dart'; import 'package:pshared/provider/organizations.dart'; import 'package:pshared/provider/resource.dart'; import 'package:pshared/service/ledger.dart'; import 'package:pshared/utils/exception.dart'; class LedgerAccountsProvider with ChangeNotifier { final LedgerService _service; OrganizationsProvider? _organizations; LedgerAccountsProvider(this._service); Resource> _resource = Resource(data: []); Resource> get resource => _resource; List get accounts => _resource.data ?? []; bool get isLoading => _resource.isLoading; Exception? get error => _resource.error; bool _isRefreshingBalances = false; bool get isRefreshingBalances => _isRefreshingBalances; final Set _refreshingAccounts = {}; bool isWalletRefreshing(String ledgerAccountRef) => _refreshingAccounts.contains(ledgerAccountRef); // Expose current org id so UI controller can reset per-org state if needed. String? get organizationRef => (_organizations?.isOrganizationSet ?? false) ? _organizations!.current.id : null; // Used to ignore stale async results (org changes / overlapping requests). int _opSeq = 0; // Per-wallet refresh sequence guard. final Map _accountSeq = {}; // Keep modest concurrency to avoid hammering the backend. static const int _balanceConcurrency = 6; void update(OrganizationsProvider organizations) { _organizations = organizations; if (organizations.isOrganizationSet) { unawaited(loadAccountsWithBalances()); } } Future updateLedgerAccount(LedgerAccount ledgerAccount) { throw Exception('update ledger account is not implemented'); } Future loadAccountsWithBalances() async { final org = _organizations; if (org == null || !org.isOrganizationSet) return; final orgRef = org.current.id; final seq = ++_opSeq; _isRefreshingBalances = false; _refreshingAccounts.clear(); _applyResource(_resource.copyWith(isLoading: true, error: null), notify: true); try { final base = await _service.list(orgRef); final result = await _withBalances(orgRef, base); if (seq != _opSeq) return; _applyResource( Resource>(data: result.wallets, isLoading: false, error: result.error), notify: true, ); } catch (e) { if (seq != _opSeq) return; _applyResource( _resource.copyWith(isLoading: false, error: toException(e)), notify: true, ); } } Future refreshBalances() async { final org = _organizations; if (org == null || !org.isOrganizationSet) return; if (accounts.isEmpty) return; final orgId = org.current.id; final seq = ++_opSeq; _isRefreshingBalances = true; _applyResource(_resource.copyWith(error: null), notify: false); notifyListeners(); try { final result = await _withBalances(orgId, accounts); if (seq != _opSeq) return; _applyResource( _resource.copyWith(data: result.wallets, error: result.error), notify: false, ); } catch (e) { if (seq != _opSeq) return; _applyResource(_resource.copyWith(error: toException(e)), notify: false); } finally { if (seq == _opSeq) { _isRefreshingBalances = false; notifyListeners(); } } } Future refreshBalance(String ledgerAccountRef) async { final org = _organizations; if (org == null || !org.isOrganizationSet) return; if (_refreshingAccounts.contains(ledgerAccountRef)) return; final existing = accounts.firstWhereOrNull((w) => w.ledgerAccountRef == ledgerAccountRef); if (existing == null) return; final orgRef = org.current.id; final seq = (_accountSeq[ledgerAccountRef] ?? 0) + 1; _accountSeq[ledgerAccountRef] = seq; _refreshingAccounts.add(ledgerAccountRef); notifyListeners(); try { final balance = await _service.getBalance( organizationRef: orgRef, ledgerAccountRef: ledgerAccountRef, ); if ((_accountSeq[ledgerAccountRef] ?? 0) != seq) return; final next = _replaceWallet(ledgerAccountRef, (w) => w.copyWith(balance: balance)); if (next == null) return; _applyResource(_resource.copyWith(data: next), notify: false); } catch (e) { if ((_accountSeq[ledgerAccountRef] ?? 0) != seq) return; _applyResource(_resource.copyWith(error: toException(e)), notify: false); } finally { if ((_accountSeq[ledgerAccountRef] ?? 0) == seq) { _refreshingAccounts.remove(ledgerAccountRef); notifyListeners(); } } } Future create({ required Describable describable, required Currency currency, String? ownerRef, }) async { final org = _organizations; if (org == null || !org.isOrganizationSet) return; _applyResource(_resource.copyWith(isLoading: true, error: null), notify: true); try { await _service.create( organizationRef: org.current.id, currency: currency, describable: describable, ownerRef: ownerRef, ); await loadAccountsWithBalances(); } catch (e) { _applyResource(_resource.copyWith(isLoading: false, error: toException(e)), notify: true); rethrow; } } // ---------- internals ---------- void _applyResource(Resource> newResource, {required bool notify}) { _resource = newResource; if (notify) notifyListeners(); } List? _replaceWallet(String ledgerAccountRef, LedgerAccount Function(LedgerAccount) updater) { final idx = accounts.indexWhere((w) => w.ledgerAccountRef == ledgerAccountRef); if (idx < 0) return null; final next = List.from(accounts); next[idx] = updater(next[idx]); return next; } Future<_LedgerAccountLoadResult> _withBalances(String orgRef, List base) async { Exception? firstError; final withBalances = await _mapConcurrent( base, _balanceConcurrency, (ledgerAccount) async { try { final balance = await _service.getBalance( organizationRef: orgRef, ledgerAccountRef: ledgerAccount.ledgerAccountRef, ); return ledgerAccount.copyWith(balance: balance); } catch (e) { firstError ??= toException(e); return ledgerAccount; } }, ); return _LedgerAccountLoadResult(withBalances, firstError); } static Future> _mapConcurrent( List items, int concurrency, Future Function(T) fn, ) async { if (items.isEmpty) return []; final results = List.filled(items.length, null); var nextIndex = 0; Future worker() async { while (true) { final i = nextIndex++; if (i >= items.length) return; results[i] = await fn(items[i]); } } final workers = List.generate(min(concurrency, items.length), (_) => worker()); await Future.wait(workers); return results.cast(); } } class _LedgerAccountLoadResult { final List wallets; final Exception? error; const _LedgerAccountLoadResult(this.wallets, this.error); }