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/ledger/role.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<List<LedgerAccount>> _resource = Resource(data: []);
  Resource<List<LedgerAccount>> get resource => _resource;

  List<LedgerAccount> get accounts => (_resource.data ?? [])
      .where((la) => la.role == LedgerAccountRole.operating)
      .toList();
  bool get isLoading => _resource.isLoading;
  Exception? get error => _resource.error;

  bool _isRefreshingBalances = false;
  bool get isRefreshingBalances => _isRefreshingBalances;

  final Set<String> _refreshingAccounts = <String>{};
  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<String, int> _accountSeq = <String, int>{};

  // 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<Wallet> updateLedgerAccount(LedgerAccount ledgerAccount) {
    throw Exception('update ledger account is not implemented');
  }

  Future<void> 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<List<LedgerAccount>>(
          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<void> 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<void> 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<void> create({
    required Describable describable,
    required CurrencyCode 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<List<LedgerAccount>> newResource, {
    required bool notify,
  }) {
    _resource = newResource;
    if (notify) notifyListeners();
  }

  List<LedgerAccount>? _replaceWallet(
    String ledgerAccountRef,
    LedgerAccount Function(LedgerAccount) updater,
  ) {
    final idx = accounts.indexWhere(
      (w) => w.ledgerAccountRef == ledgerAccountRef,
    );
    if (idx < 0) return null;

    final next = List<LedgerAccount>.from(accounts);
    next[idx] = updater(next[idx]);
    return next;
  }

  Future<_LedgerAccountLoadResult> _withBalances(
    String orgRef,
    List<LedgerAccount> base,
  ) async {
    Exception? firstError;

    final withBalances = await _mapConcurrent<LedgerAccount, LedgerAccount>(
      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<List<R>> _mapConcurrent<T, R>(
    List<T> items,
    int concurrency,
    Future<R> Function(T) fn,
  ) async {
    if (items.isEmpty) return <R>[];

    final results = List<R?>.filled(items.length, null);
    var nextIndex = 0;

    Future<void> 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<R>();
  }
}

class _LedgerAccountLoadResult {
  final List<LedgerAccount> wallets;
  final Exception? error;

  const _LedgerAccountLoadResult(this.wallets, this.error);
}
