import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:cbor/cbor.dart';
import '../src/rust/api/wallet.dart';
import '../src/rust/api/token.dart';
import '../src/rust/api/mint_info.dart';
import '../src/rust/api/keys.dart';
import '../src/rust/api/payment_request.dart';
import 'package:path_provider/path_provider.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:uuid/uuid.dart';
import '../data/transaction_meta_storage.dart';
import '../data/pending_token.dart';
import '../data/pending_token_storage.dart';
import '../core/utils/keyset_debug.dart';
import '../core/utils/p2pk_utils.dart';
import '../widgets/effects/cashu_confetti.dart';

/// Helper class para info de token parseado
class TokenInfo {
  final BigInt amount;
  final String mintUrl;
  final String encoded;
  final String? unit; // Unidad detectada del token (sat, usd, eur, etc.)

  TokenInfo({
    required this.amount,
    required this.mintUrl,
    required this.encoded,
    this.unit,
  });
}

/// Provider que gestiona todas las operaciones del wallet Cashu.
/// Soporta múltiples unidades (sat, usd, eur) por mint.
/// Usa múltiples instancias de Wallet compartiendo una sola DB.
class WalletProvider extends ChangeNotifier {
  WalletDatabase? _db;
  String? _mnemonic;

  /// Storage para metadata de transacciones (tipo, token, invoice)
  final TransactionMetaStorage _txMetaStorage = TransactionMetaStorage();

  /// Storage para tokens pendientes de reclamar (Receive Later)
  final PendingTokenStorage _pendingTokenStorage = PendingTokenStorage();

  /// Controller global de confetti para celebrar recepciones
  final CashuConfettiController confettiController = CashuConfettiController();

  /// Generador de UUIDs
  static const _uuid = Uuid();

  /// Mints conocidos con sus unidades soportadas.
  /// Ejemplo: {'mint.cubabitcoin.org': ['sat', 'usd']}
  final Map<String, List<String>> _mintUnits = {};

  /// Wallets por mint:unidad (lazy loading).
  /// Clave: 'mintUrl:unit', Ejemplo: 'https://mint.cubabitcoin.org:sat'
  final Map<String, Wallet> _wallets = {};

  /// Caché de keyset_id → unit para detectar unidad de tokens.
  /// Ejemplo: {'00abc123': 'sat', '00def456': 'usd'}
  final Map<String, String> _keysetUnits = {};

  /// Caché de MintInfo por URL (nombre, logo, contactos, etc.)
  final Map<String, MintInfo> _mintInfoCache = {};

  /// Mint activo actualmente
  String? _activeMintUrl;

  /// Unidad activa actualmente
  String _activeUnit = 'sat';

  /// Claves para persistencia en SharedPreferences
  static const _mintsKey = 'wallet_mints';
  static const _activeMintKey = 'wallet_active_mint';
  static const _activeUnitKey = 'wallet_active_unit';
  static const _pendingMintInvoicesKey = 'pending_mint_invoices';
  static const _pendingNostrRequestKey = 'pending_nostr_request';

  /// Mint de Cuba Bitcoin - siempre aparece primero en la lista
  static const cubaBitcoinMint = 'https://mint.cubabitcoin.org';

  // ============================================================
  // GETTERS
  // ============================================================

  bool get isInitialized => _db != null && _mnemonic != null;
  String? get activeMintUrl => _activeMintUrl;
  String get activeUnit => _activeUnit;

  /// Unidades soportadas por el mint activo
  List<String> get activeUnits => _mintUnits[_activeMintUrl] ?? ['sat'];

  /// Wallet activo (mint + unidad actuales)
  Wallet? get activeWallet {
    if (_activeMintUrl == null) return null;
    final key = '$_activeMintUrl:$_activeUnit';
    return _wallets[key];
  }

  /// Todos los wallets creados (para verificación de proofs)
  Iterable<Wallet> get allWallets => _wallets.values;

  /// Lista de URLs de mints conocidos (orden de inserción)
  List<String> get mintUrls => _mintUnits.keys.toList();

  // ============================================================
  // PENDING TOKENS GETTERS
  // ============================================================

  /// Cantidad de tokens pendientes de reclamar
  int get pendingTokenCount => _pendingTokenStorage.count;

  /// Verifica si hay tokens pendientes
  bool get hasPendingTokens => _pendingTokenStorage.hasPendingTokens;

  /// Stream de cambios en pending tokens
  Stream<void> get pendingTokenChanges => _pendingTokenStorage.changes;

  /// Lista de mints ordenados por balance.
  /// Cuba Bitcoin siempre primero, luego ordenados por: sats → usd → eur → otros.
  Future<List<String>> getSortedMintUrls() async {
    final mints = _mintUnits.keys.toList();

    if (mints.length <= 1) return mints;

    // Obtener balances de cada mint
    final mintBalances = <String, Map<String, BigInt>>{};
    for (final mint in mints) {
      mintBalances[mint] = await getBalancesForMint(mint);
    }

    // Orden de prioridad de unidades
    const unitPriority = ['sat', 'usd', 'eur'];

    // Ordenar mints (excepto Cuba Bitcoin)
    final otherMints = mints.where((m) => m != cubaBitcoinMint).toList();

    otherMints.sort((a, b) {
      final balancesA = mintBalances[a] ?? {};
      final balancesB = mintBalances[b] ?? {};

      // Comparar por cada unidad en orden de prioridad
      for (final unit in unitPriority) {
        final balA = balancesA[unit] ?? BigInt.zero;
        final balB = balancesB[unit] ?? BigInt.zero;
        if (balA != balB) {
          return balB.compareTo(balA); // Mayor primero
        }
      }

      // Si empatan en las prioritarias, comparar otras unidades alfabéticamente
      final otherUnitsA = balancesA.keys.where((u) => !unitPriority.contains(u)).toList()..sort();
      final otherUnitsB = balancesB.keys.where((u) => !unitPriority.contains(u)).toList()..sort();

      for (final unit in {...otherUnitsA, ...otherUnitsB}) {
        final balA = balancesA[unit] ?? BigInt.zero;
        final balB = balancesB[unit] ?? BigInt.zero;
        if (balA != balB) {
          return balB.compareTo(balA);
        }
      }

      return 0;
    });

    // Cuba Bitcoin siempre primero
    final result = <String>[];
    if (mints.contains(cubaBitcoinMint)) {
      result.add(cubaBitcoinMint);
    }
    result.addAll(otherMints);

    return result;
  }

  // ============================================================
  // CONNECTIVITY
  // ============================================================

  /// Verifica si podemos alcanzar un mint específico.
  /// Usa función Rust que hace GET a {mintUrl}/v1/info.
  /// Retorna true si responde 200, false en cualquier otro caso.
  Future<bool> canReachMint(String mintUrl) async {
    try {
      return await pingMint(mintUrl: mintUrl);
    } catch (e) {
      debugPrint('Ping to mint failed: $e');
      return false;
    }
  }

  // ============================================================
  // MINT INFO
  // ============================================================

  /// Obtiene la info de un mint (nombre, logo, contactos, etc.)
  /// Usa caché para evitar llamadas repetidas.
  Future<MintInfo?> fetchMintInfo(String mintUrl) async {
    // Revisar caché primero
    if (_mintInfoCache.containsKey(mintUrl)) {
      return _mintInfoCache[mintUrl];
    }

    try {
      final info = await getMintInfo(mintUrl: mintUrl);
      _mintInfoCache[mintUrl] = info;
      return info;
    } catch (e) {
      debugPrint('Error fetching mint info for $mintUrl: $e');
      return null;
    }
  }

  /// Obtiene MintInfo del caché (sin fetch)
  MintInfo? getCachedMintInfo(String mintUrl) {
    return _mintInfoCache[mintUrl];
  }

  /// Obtiene el nombre del mint (del caché o display name)
  String getMintName(String mintUrl) {
    final cached = _mintInfoCache[mintUrl];
    if (cached?.name != null && cached!.name!.isNotEmpty) {
      return cached.name!;
    }
    // Fallback: extraer nombre del URL
    return _getMintDisplayName(mintUrl);
  }

  /// Obtiene el URL del icono del mint (del caché)
  String? getMintIconUrl(String mintUrl) {
    return _mintInfoCache[mintUrl]?.iconUrl;
  }

  /// Helper: extrae nombre legible del URL del mint
  String _getMintDisplayName(String mintUrl) {
    try {
      final uri = Uri.parse(mintUrl);
      var host = uri.host;
      host = host.replaceFirst('mint.', '');
      host = host.replaceFirst('www.', '');
      return host;
    } catch (e) {
      return mintUrl;
    }
  }

  // ============================================================
  // PERSISTENCIA DE MINTS
  // ============================================================

  /// Guarda la lista de mints y sus unidades en SharedPreferences.
  Future<void> _saveMints() async {
    final prefs = await SharedPreferences.getInstance();

    // Guardar mints como JSON: {"url": ["sat", "usd"], ...}
    final mintsJson = jsonEncode(_mintUnits);
    await prefs.setString(_mintsKey, mintsJson);

    // Guardar mint y unidad activos
    if (_activeMintUrl != null) {
      await prefs.setString(_activeMintKey, _activeMintUrl!);
    }
    await prefs.setString(_activeUnitKey, _activeUnit);

    debugPrint('Mints guardados: ${_mintUnits.keys.length}');
  }

  /// Carga la lista de mints desde SharedPreferences.
  /// Retorna true si había mints guardados.
  Future<bool> _loadMints() async {
    final prefs = await SharedPreferences.getInstance();

    final mintsJson = prefs.getString(_mintsKey);
    if (mintsJson == null || mintsJson.isEmpty) {
      return false;
    }

    try {
      final decoded = jsonDecode(mintsJson) as Map<String, dynamic>;
      _mintUnits.clear();

      for (final entry in decoded.entries) {
        final units = (entry.value as List<dynamic>).cast<String>();
        _mintUnits[entry.key] = units;
      }

      // Cargar mint y unidad activos
      _activeMintUrl = prefs.getString(_activeMintKey);
      _activeUnit = prefs.getString(_activeUnitKey) ?? 'sat';

      // Verificar que el mint activo existe
      if (_activeMintUrl != null && !_mintUnits.containsKey(_activeMintUrl)) {
        _activeMintUrl = _mintUnits.keys.firstOrNull;
      }

      // Verificar que la unidad activa es válida para el mint
      if (_activeMintUrl != null) {
        final units = _mintUnits[_activeMintUrl]!;
        if (!units.contains(_activeUnit)) {
          _activeUnit = units.first;
        }
      }

      debugPrint('Mints cargados: ${_mintUnits.keys.length}');
      return _mintUnits.isNotEmpty;
    } catch (e) {
      debugPrint('Error cargando mints: $e');
      return false;
    }
  }

  // ============================================================
  // INICIALIZACION
  // ============================================================

  /// Inicializa el provider con un mnemonic.
  /// Carga mints y unidades desde SharedPreferences.
  Future<void> initialize(String mnemonic) async {
    _mnemonic = mnemonic;

    // Inicializar storage de metadata de transacciones
    await _txMetaStorage.init();

    // Inicializar storage de tokens pendientes
    await _pendingTokenStorage.init();

    // Obtener directorio de documentos (path absoluto requerido)
    final dir = await getApplicationDocumentsDirectory();
    final dbPath = '${dir.path}/elcaju_wallet.sqlite';

    // Crear base de datos (se crea automáticamente si no existe)
    _db = await WalletDatabase.newInstance(path: dbPath);

    // Intentar cargar mints desde SharedPreferences
    final hasSavedMints = await _loadMints();

    if (hasSavedMints) {
      // Cargar keysets para cada mint guardado
      for (final mintUrl in _mintUnits.keys) {
        try {
          await _fetchKeysets(mintUrl);
          // Crear wallet para la primera unidad de cada mint
          final units = _mintUnits[mintUrl]!;
          await getWallet(mintUrl, units.first);
        } catch (e) {
          debugPrint('Error cargando mint $mintUrl: $e');
        }
      }
      debugPrint('Mints restaurados: ${_mintUnits.keys.length}');
    } else {
      // Primera vez - agregar mint por defecto
      await addMint('https://mint.cubabitcoin.org');
    }

    notifyListeners();
  }

  /// Detecta si es primera vez (sin mints conocidos).
  bool isFirstTime() {
    return _mintUnits.isEmpty;
  }

  /// Genera un nuevo mnemonic de 12 palabras BIP39.
  String generateNewMnemonic() {
    return generateMnemonic();
  }

  // ============================================================
  // WALLET MANAGEMENT (Lazy Loading)
  // ============================================================

  /// Obtiene o crea un wallet para un mint y unidad específicos.
  /// Implementa lazy loading - solo crea el wallet cuando se necesita.
  Future<Wallet> getWallet(String mintUrl, String unit) async {
    if (_db == null || _mnemonic == null) {
      throw Exception('WalletProvider no inicializado');
    }

    final key = '$mintUrl:$unit';

    if (!_wallets.containsKey(key)) {
      _wallets[key] = Wallet(
        mintUrl: mintUrl,
        unit: unit,
        mnemonic: _mnemonic!,
        db: _db!,
      );
    }

    return _wallets[key]!;
  }

  /// Obtiene el wallet activo, creándolo si no existe.
  Future<Wallet> getActiveWallet() async {
    if (_activeMintUrl == null) {
      throw Exception('No hay mint activo');
    }
    return await getWallet(_activeMintUrl!, _activeUnit);
  }

  // ============================================================
  // MULTI-MINT OPERATIONS
  // ============================================================

  /// Detecta las unidades soportadas por un mint y las registra.
  /// Usa mintInfo.nuts.nut04 (MintMethodSettings) para obtener las unidades.
  Future<List<String>> _detectAndAddMint(String mintUrl) async {
    try {
      // Obtener info del mint (función del bridge Rust)
      final mintInfo = await getMintInfo(mintUrl: mintUrl);

      // Extraer unidades de nut04 (mint methods)
      final units = <String>{};

      // nut04 contiene los métodos de mint con sus unidades
      final nut04 = mintInfo.nuts.nut04;
      for (final method in nut04.methods) {
        units.add(method.unit);
      }

      // Si no hay unidades detectadas, usar 'sat' por defecto
      final unitList = units.isEmpty ? ['sat'] : units.toList();

      // Ordenar: sat primero, luego alfabético
      unitList.sort((a, b) {
        if (a == 'sat') return -1;
        if (b == 'sat') return 1;
        return a.compareTo(b);
      });

      _mintUnits[mintUrl] = unitList;

      // Obtener keysets para mapear keyset_id → unit
      await _fetchKeysets(mintUrl);

      return unitList;
    } catch (e) {
      debugPrint('Error detectando unidades de $mintUrl: $e');
      // Fallback a sat
      _mintUnits[mintUrl] = ['sat'];
      return ['sat'];
    }
  }

  /// Obtiene los keysets de un mint y los agrega al caché _keysetUnits.
  /// Usa función Rust que llama a GET {mintUrl}/v1/keysets.
  Future<void> _fetchKeysets(String mintUrl) async {
    try {
      final keysets = await fetchKeysets(mintUrl: mintUrl);
      for (final ks in keysets) {
        _keysetUnits[ks.id] = ks.unit;
      }
      debugPrint('Keysets cargados para $mintUrl: ${keysets.length}');
    } catch (e) {
      debugPrint('Error obteniendo keysets de $mintUrl: $e');
      // No lanzamos excepción, el caché simplemente no tendrá estos keysets
    }
  }

  /// Agrega un mint, detectando automáticamente sus unidades.
  Future<void> addMint(String mintUrl) async {
    if (_db == null || _mnemonic == null) {
      throw Exception('WalletProvider no inicializado');
    }

    // Detectar unidades
    final units = await _detectAndAddMint(mintUrl);

    // Crear wallet para la primera unidad
    await getWallet(mintUrl, units.first);

    // Activar si es el primer mint
    _activeMintUrl ??= mintUrl;
    if (_activeMintUrl == mintUrl) {
      _activeUnit = units.first;
    }

    // Persistir cambios
    await _saveMints();

    notifyListeners();
  }

  /// Refresca la información de un mint (keysets, unidades).
  /// Útil para detectar nuevas unidades agregadas por el mint.
  Future<List<String>> refreshMint(String mintUrl) async {
    final units = await _detectAndAddMint(mintUrl);
    notifyListeners();
    return units;
  }

  /// Obtiene las unidades soportadas por un mint.
  List<String> getUnitsForMint(String mintUrl) {
    return _mintUnits[mintUrl] ?? ['sat'];
  }

  /// Lista los mints conocidos con sus unidades.
  Map<String, List<String>> listMintsWithUnits() {
    return Map.unmodifiable(_mintUnits);
  }

  /// Remueve un mint y todos sus wallets asociados.
  /// Nota: Si hay balance, el usuario puede recuperarlo después con restore (NUT-13).
  Future<void> removeMint(String mintUrl) async {
    // Eliminar wallets de este mint
    final keysToRemove = _wallets.keys
        .where((key) => key.startsWith('$mintUrl:'))
        .toList();
    for (final key in keysToRemove) {
      _wallets.remove(key);
    }

    // Eliminar de mints conocidos
    _mintUnits.remove(mintUrl);

    // Si era el mint activo, cambiar a otro
    if (_activeMintUrl == mintUrl) {
      if (_mintUnits.isNotEmpty) {
        final newMint = _mintUnits.keys.first;
        _activeMintUrl = newMint;
        _activeUnit = _mintUnits[newMint]!.first;
      } else {
        _activeMintUrl = null;
        _activeUnit = 'sat';
      }
    }

    // Persistir cambios
    await _saveMints();

    notifyListeners();
  }

  // ============================================================
  // ACTIVE MINT/UNIT SELECTION
  // ============================================================

  /// Cambia el mint activo.
  Future<void> setActiveMint(String mintUrl) async {
    if (!_mintUnits.containsKey(mintUrl)) {
      throw Exception('Mint no conocido: $mintUrl');
    }

    _activeMintUrl = mintUrl;

    // Si la unidad activa no está soportada por este mint, cambiar a la primera
    final units = _mintUnits[mintUrl]!;
    if (!units.contains(_activeUnit)) {
      _activeUnit = units.first;
    }

    // Asegurar que el wallet existe
    await getWallet(mintUrl, _activeUnit);

    // Persistir cambio
    await _saveMints();

    notifyListeners();
  }

  /// Cambia la unidad activa.
  Future<void> setActiveUnit(String unit) async {
    if (_activeMintUrl == null) return;

    final units = _mintUnits[_activeMintUrl]!;
    if (!units.contains(unit)) {
      throw Exception('Unidad $unit no soportada por el mint activo');
    }

    _activeUnit = unit;

    // Asegurar que el wallet existe
    await getWallet(_activeMintUrl!, unit);

    // Persistir cambio
    await _saveMints();

    notifyListeners();
  }

  /// Cicla a la siguiente unidad del mint activo.
  /// Orden: sat → usd → eur → sat ...
  Future<void> cycleUnit() async {
    if (_activeMintUrl == null) return;

    final units = activeUnits;
    if (units.length <= 1) return;

    final currentIndex = units.indexOf(_activeUnit);
    final nextIndex = (currentIndex + 1) % units.length;

    await setActiveUnit(units[nextIndex]);
  }

  // ============================================================
  // BALANCE
  // ============================================================

  /// Stream de balance del wallet activo (reactivo).
  Stream<BigInt>? streamBalance() {
    return activeWallet?.streamBalance();
  }

  /// Obtiene el balance del wallet activo.
  Future<BigInt> getBalance() async {
    final wallet = activeWallet;
    if (wallet == null) return BigInt.zero;
    return await wallet.balance();
  }

  /// Obtiene el balance de un mint en una unidad específica.
  Future<BigInt> getBalanceForMintUnit(String mintUrl, String unit) async {
    final key = '$mintUrl:$unit';
    final wallet = _wallets[key];
    if (wallet == null) {
      // Crear wallet si no existe
      final newWallet = await getWallet(mintUrl, unit);
      return await newWallet.balance();
    }
    return await wallet.balance();
  }

  /// Obtiene el balance total de un mint (todas las unidades).
  /// Retorna Map con unit como clave y balance como valor.
  Future<Map<String, BigInt>> getBalancesForMint(String mintUrl) async {
    final units = _mintUnits[mintUrl] ?? [];
    final balances = <String, BigInt>{};

    for (final unit in units) {
      balances[unit] = await getBalanceForMintUnit(mintUrl, unit);
    }

    return balances;
  }

  // ============================================================
  // RECEIVE (Recibir tokens Cashu)
  // ============================================================

  /// Parsea un token sin reclamarlo. Retorna null si inválido.
  /// Incluye detección de unidad si es posible.
  TokenInfo? parseToken(String encodedToken) {
    try {
      final token = Token.parse(encoded: encodedToken);
      final unit = detectTokenUnit(encodedToken);
      return TokenInfo(
        amount: token.amount,
        mintUrl: token.mintUrl,
        encoded: token.encoded,
        unit: unit,
      );
    } catch (e) {
      return null;
    }
  }

  /// Detecta la unidad de un token decodificando su keyset_id.
  /// Soporta tokens V3 (cashuA) y V4 (cashuB con fallback).
  /// Retorna null si no puede detectar la unidad.
  String? detectTokenUnit(String encodedToken) {
    try {
      // Limpiar prefijo "cashu:" si existe
      String token = encodedToken.trim();
      if (token.startsWith('cashu:')) {
        token = token.substring(6);
      }

      // Detectar versión por prefijo
      if (token.startsWith('cashuA')) {
        // Token V3: base64 JSON
        return _detectUnitFromV3(token);
      } else if (token.startsWith('cashuB')) {
        // Token V4: CBOR - intentar fallback
        return _detectUnitFromV4(token);
      }

      return null;
    } catch (e) {
      debugPrint('Error detectando unidad del token: $e');
      return null;
    }
  }

  /// Extrae keyset_id de un token V3 (cashuA + base64 JSON).
  String? _detectUnitFromV3(String token) {
    try {
      // Remover prefijo "cashuA"
      final base64Part = token.substring(6);

      // Decodificar base64
      final decoded = utf8.decode(base64.decode(base64Part));
      final json = jsonDecode(decoded) as Map<String, dynamic>;

      // Estructura V3: {"token": [{"mint": "...", "proofs": [{"id": "...", ...}]}]}
      final tokenList = json['token'] as List<dynamic>?;
      if (tokenList == null || tokenList.isEmpty) return null;

      final firstToken = tokenList[0] as Map<String, dynamic>?;
      if (firstToken == null) return null;

      final proofs = firstToken['proofs'] as List<dynamic>?;
      if (proofs == null || proofs.isEmpty) return null;

      final firstProof = proofs[0] as Map<String, dynamic>?;
      final keysetId = firstProof?['id'] as String?;

      if (keysetId != null) {
        // Lookup en caché
        final unit = _keysetUnits[keysetId];
        if (unit != null) {
          debugPrint('Token V3 detectado: keyset=$keysetId, unit=$unit');
          return unit;
        }
        debugPrint('Keyset $keysetId no encontrado en caché');
      }

      return null;
    } catch (e) {
      debugPrint('Error decodificando token V3: $e');
      return null;
    }
  }

  /// Intenta detectar unidad de un token V4 (cashuB + CBOR).
  /// Por ahora hace fallback a probar las unidades conocidas del mint.
  /// Detecta unidad de un token V4 (cashuB + base64url CBOR).
  /// V4 incluye la unidad directamente en el campo 'u'.
  String? _detectUnitFromV4(String token) {
    try {
      // Remover prefijo "cashuB"
      final base64Part = token.substring(6);

      // Decodificar base64url (V4 usa base64url, no base64 estándar)
      final normalized = base64Part
          .replaceAll('-', '+')
          .replaceAll('_', '/');
      // Agregar padding si es necesario
      final padded = normalized.padRight(
        (normalized.length + 3) & ~3,
        '=',
      );
      final bytes = base64.decode(padded);

      // Decodificar CBOR usando la API simple
      final decoded = cbor.decode(bytes);

      // V4 format: {m: mint_url, u: unit, t: [...]}
      if (decoded is CborMap) {
        // Buscar el campo 'u' (unit)
        for (final entry in decoded.entries) {
          final key = entry.key;
          if (key is CborString && key.toString() == 'u') {
            final value = entry.value;
            if (value is CborString) {
              final unit = value.toString();
              debugPrint('Token V4 detectado: unit=$unit');
              return unit;
            }
          }
        }
      }

      debugPrint('Token V4: no se encontró campo "u"');
      return null;
    } catch (e) {
      debugPrint('Error decodificando token V4: $e');
      return null;
    }
  }

  /// Reclama un token Cashu detectando automáticamente su unidad.
  /// Si el mint del token es diferente al activo, lo agrega.
  /// NO cambia la unidad activa (comportamiento similar a cashu.me).
  /// Si el token es P2PK, pasar la clave privada en p2pkPrivateKey.
  /// Retorna monto recibido.
  Future<BigInt> receiveToken(String encodedToken, {String? p2pkPrivateKey}) async {
    // Parsear token (incluye detección de unidad)
    final tokenInfo = parseToken(encodedToken);
    if (tokenInfo == null) {
      throw Exception('Token inválido');
    }

    final tokenMint = tokenInfo.mintUrl;

    // Verificar si conocemos este mint
    if (!_mintUnits.containsKey(tokenMint)) {
      // Agregar el mint automáticamente (esto también carga keysets)
      await addMint(tokenMint);

      // Intentar detectar unidad de nuevo ahora que tenemos keysets
      final detectedUnit = detectTokenUnit(encodedToken);
      if (detectedUnit != null) {
        return await _receiveWithUnit(encodedToken, tokenMint, detectedUnit, p2pkPrivateKey);
      }
    }

    // Si tenemos unidad detectada, usarla
    if (tokenInfo.unit != null) {
      return await _receiveWithUnit(encodedToken, tokenMint, tokenInfo.unit!, p2pkPrivateKey);
    }

    // Fallback: intentar con cada unidad del mint hasta que funcione
    final units = _mintUnits[tokenMint] ?? ['sat'];
    Exception? lastError;

    for (final unit in units) {
      try {
        return await _receiveWithUnit(encodedToken, tokenMint, unit, p2pkPrivateKey);
      } catch (e) {
        lastError = e as Exception;
        debugPrint('Receive falló con unidad $unit: $e');
        continue;
      }
    }

    // Si ninguna unidad funcionó, lanzar el último error
    throw lastError ?? Exception('No se pudo reclamar el token');
  }

  /// Recibe un token con una unidad específica.
  /// Guarda metadata type=cashu para identificar en historial.
  Future<BigInt> _receiveWithUnit(
    String encodedToken,
    String mintUrl,
    String unit,
    String? p2pkPrivateKey,
  ) async {
    final wallet = await getWallet(mintUrl, unit);
    final token = Token.parse(encoded: encodedToken);

    // Si hay clave P2PK, usarla para desbloquear el token
    final opts = p2pkPrivateKey != null
        ? ReceiveOptions(signingKeys: [p2pkPrivateKey])
        : null;

    // DEBUG: counters antes de receive
    await KeysetDebug.logCounters('BEFORE receive ($unit)');

    // DEBUG: transacciones ANTES del receive
    await _debugLogTransactions(wallet, 'BEFORE receive');

    BigInt amount;
    try {
      amount = await wallet.receive(token: token, opts: opts);
    } catch (e) {
      final errorStr = e.toString().toLowerCase();
      if (errorStr.contains('already signed')) {
        debugPrint('[RECEIVE] Already signed error, restoring wallet counters...');
        await wallet.restore();
        // Retry once after counter resync
        amount = await wallet.receive(token: token, opts: opts);
      } else {
        rethrow;
      }
    }

    // DEBUG: counters después de receive
    await KeysetDebug.logCounters('AFTER receive ($unit)');

    // DEBUG: transacciones DESPUÉS del receive (antes de checkPending)
    await _debugLogTransactions(wallet, 'AFTER receive (before checkPending)');

    // Guardar metadata para la transacción recién creada
    await _saveMetaForRecentReceive(wallet, encodedToken);

    // Verificar transacciones pendientes para actualizar outgoing pending → settled
    try {
      await wallet.checkPendingTransactions();
    } catch (e) {
      debugPrint('Check pending after receive failed: $e');
    }

    // DEBUG: transacciones DESPUÉS de checkPending
    await _debugLogTransactions(wallet, 'AFTER checkPending');

    debugPrint('Token recibido: $amount $unit en $mintUrl');
    confettiController.fire();
    notifyListeners();
    return amount;
  }

  /// DEBUG: Lista todas las transacciones de un wallet para diagnóstico.
  Future<void> _debugLogTransactions(Wallet wallet, String label) async {
    try {
      final allTxs = await wallet.listTransactions();
      debugPrint('[TX DEBUG] ===== $label =====');
      debugPrint('[TX DEBUG] Total transacciones: ${allTxs.length}');
      for (final tx in allTxs) {
        final dir = tx.direction == TransactionDirection.incoming ? 'IN' : 'OUT';
        final status = tx.status == TransactionStatus.pending ? 'PENDING' : 'SETTLED';
        debugPrint('[TX DEBUG]   $dir ${tx.amount} ${tx.unit} [$status] fee=${tx.fee} id=${tx.id.substring(0, 16)}...');
      }
    } catch (e) {
      debugPrint('[TX DEBUG] Error listando transacciones: $e');
    }
  }

  /// Guarda metadata para la transacción de receive más reciente.
  Future<void> _saveMetaForRecentReceive(Wallet wallet, String tokenEncoded) async {
    try {
      final txs = await wallet.listTransactions(
        direction: TransactionDirection.incoming,
      );

      if (txs.isNotEmpty) {
        final recentTx = txs.first;

        await _txMetaStorage.save(
          recentTx.id,
          TransactionMeta(
            type: TransactionType.cashu,
            token: tokenEncoded,
          ),
        );
        debugPrint('Receive metadata guardada para tx ${recentTx.id}');
      }
    } catch (e) {
      debugPrint('Error guardando receive metadata: $e');
    }
  }

  /// Reclama un token P2PK (bloqueado a una clave pública).
  /// Usa la unidad del token automáticamente (no requiere cambiar unidad activa).
  Future<BigInt> receiveP2pkToken(
    String encodedToken,
    List<String> signingKeys,
  ) async {
    final tokenInfo = parseToken(encodedToken);
    if (tokenInfo == null) {
      throw Exception('Token inválido');
    }

    // Agregar mint si no existe
    if (!_mintUnits.containsKey(tokenInfo.mintUrl)) {
      await addMint(tokenInfo.mintUrl);
    }

    // Usar la unidad del token (fallback a 'sat' si no especificada)
    final tokenUnit = tokenInfo.unit ?? 'sat';
    final wallet = await getWallet(tokenInfo.mintUrl, tokenUnit);
    final token = Token.parse(encoded: encodedToken);

    final amount = await wallet.receive(
      token: token,
      opts: ReceiveOptions(signingKeys: signingKeys),
    );

    notifyListeners();
    return amount;
  }

  // ============================================================
  // SEND (Enviar tokens Cashu)
  // ============================================================

  /// Prepara un envío (reserva proofs, calcula fees).
  Future<PreparedSend> prepareSend(BigInt amount) async {
    final wallet = await getActiveWallet();
    return await wallet.prepareSend(amount: amount);
  }

  /// Prepara un envío P2PK (bloqueado a una clave pública).
  /// includeFee: true asegura que se reserven proofs para cubrir el swap fee (CDK 0.15+).
  Future<PreparedSend> prepareSendP2pk(BigInt amount, String pubkey) async {
    final wallet = await getActiveWallet();
    return await wallet.prepareSend(
      amount: amount,
      opts: SendOptions(pubkey: pubkey, includeFee: true),
    );
  }

  /// Confirma un envío preparado y retorna el token encoded.
  /// Guarda metadata type=cashu para identificar en historial.
  Future<String> confirmSend(PreparedSend prepared, String? memo) async {
    final wallet = await getActiveWallet();

    final result = await wallet.send(
      send: prepared,
      memo: memo,
      includeMemo: memo != null && memo.isNotEmpty,
    );

    // Save token metadata using the deterministic transaction ID returned by CDK
    // (SHA-256 of sorted proof Y values — no racy listTransactions needed)
    // Best-effort: don't fail the send if metadata persistence fails
    final txId = result.transactionId;
    if (txId != null && txId.isNotEmpty) {
      try {
        await _txMetaStorage.save(
          txId,
          TransactionMeta(
            type: TransactionType.cashu,
            token: result.token.encoded,
          ),
        );
        debugPrint('Token guardado para tx $txId');
      } catch (e) {
        debugPrint('Error guardando send metadata: $e');
      }
    }

    notifyListeners();
    return result.token.encoded;
  }

  /// Cancela un envío preparado (libera proofs reservados).
  Future<void> cancelSend(PreparedSend prepared) async {
    final wallet = await getActiveWallet();
    await wallet.cancelSend(send: prepared);
  }

  /// Método de conveniencia: prepara y confirma en un solo paso.
  /// Si confirmSend falla, libera proofs reservados con cancelSend.
  Future<String> sendTokens(BigInt amount, String? memo) async {
    final prepared = await prepareSend(amount);
    debugPrint('[SEND] prepareSend OK - fee=${prepared.fee}');
    try {
      final token = await confirmSend(prepared, memo);
      debugPrint('[SEND] Send completed');
      return token;
    } catch (e) {
      try {
        await cancelSend(prepared);
      } catch (cancelErr) {
        debugPrint('[SEND] cancelSend failed: $cancelErr');
      }
      rethrow;
    }
  }

  /// Envía tokens P2PK (bloqueados a una clave pública).
  /// CDK 0.15+ con includeFee: true maneja correctamente mints con ppk>0.
  /// Si confirmSend falla, libera proofs reservados con cancelSend.
  Future<String> sendTokensP2pk(BigInt amount, String pubkey, String? memo) async {
    debugPrint('[P2PK] Sending $amount to ${pubkey.length > 16 ? pubkey.substring(0, 16) : pubkey}...');
    final prepared = await prepareSendP2pk(amount, pubkey);
    debugPrint('[P2PK] prepareSend OK - fee=${prepared.fee}');
    try {
      final token = await confirmSend(prepared, memo);
      debugPrint('[P2PK] Send completed');
      return token;
    } catch (e) {
      try {
        await cancelSend(prepared);
      } catch (cancelErr) {
        debugPrint('[P2PK] cancelSend failed: $cancelErr');
      }
      rethrow;
    }
  }

  /// Verifica si hay transacciones salientes pendientes en el wallet activo.
  Future<bool> hasPendingOutgoingTransactions() async {
    try {
      final wallet = await getActiveWallet();
      final txs = await wallet.listTransactions();
      return txs.any((tx) =>
        tx.direction == TransactionDirection.outgoing &&
        tx.status == TransactionStatus.pending);
    } catch (_) {
      return false;
    }
  }

  // ============================================================
  // PAYMENT REQUEST (NUT-18/26)
  // ============================================================

  /// Paga un Payment Request (NUT-18 creqA / NUT-26 creqB / BIP-321).
  /// CDK maneja: NUT-10 spending conditions, transporte (Nostr/HTTP POST),
  /// preparación del token y entrega automática.
  Future<void> payPaymentRequest(
    String encodedRequest, {
    BigInt? customAmount,
  }) async {
    final wallet = await getActiveWallet();
    debugPrint('[PAY_REQUEST] Paying payment request via ${wallet.mintUrl}');
    await wallet.payPaymentRequest(
      encoded: encodedRequest,
      customAmount: customAmount,
    );
    debugPrint('[PAY_REQUEST] Payment delivered successfully');
    notifyListeners();
  }

  // ============================================================
  // MINT (Depositar via Lightning)
  // ============================================================

  /// Suscripción activa al stream de mint (vive en el provider, no en la UI).
  StreamSubscription<MintQuote>? _activeMintSubscription;

  /// Controller del stream actual de mint (para cerrar al iniciar uno nuevo).
  StreamController<MintQuote>? _activeMintController;

  /// Inicia un depósito via Lightning.
  /// Retorna Stream con estados: unpaid -> paid -> issued.
  /// La suscripción al CDK vive en el provider para que los side effects
  /// (guardar metadata, confetti) ocurran aunque la UI se cierre.
  Future<Stream<MintQuote>> mintTokens(BigInt amount, String? description) async {
    final wallet = activeWallet;
    if (wallet == null) {
      throw Exception('No hay wallet activo');
    }

    final mintUrl = _activeMintUrl!;
    final unit = _activeUnit;
    String? invoiceBolt11;

    // Cerrar controller anterior si existe (evitar leak)
    if (_activeMintController != null && !_activeMintController!.isClosed) {
      _activeMintController!.close();
    }

    // StreamController que la UI puede escuchar y cancelar libremente
    final controller = StreamController<MintQuote>();
    _activeMintController = controller;

    // Cancelar suscripción anterior si existe (await evita race de callbacks)
    await _activeMintSubscription?.cancel();
    _activeMintSubscription = null;

    // Suscribirse al stream del CDK desde el provider (persiste sin UI)
    _activeMintSubscription = wallet.mint(
      amount: amount,
      description: description,
    ).listen(
      (quote) async {
        // Guardar invoice temprano en SharedPreferences
        if (quote.state == MintQuoteState.unpaid) {
          invoiceBolt11 = quote.request;
          _savePendingMintInvoice(quote.id, quote.request, mintUrl, unit, amount);
        }

        // Cuando se completa, guardar metadata, confetti, limpiar pending
        if (quote.state == MintQuoteState.issued && invoiceBolt11 != null) {
          final saved = await _saveMintMetadata(wallet, invoiceBolt11!, quote.transactionId);
          // Only remove pending invoice if metadata was saved;
          // otherwise _matchPendingMintInvoices can recover it on next startup
          if (saved) _removePendingMintInvoice(quote.id);
        }

        // Reenviar a la UI (si sigue escuchando)
        if (!controller.isClosed) {
          controller.add(quote);
        }
      },
      onError: (error) {
        if (!controller.isClosed) {
          controller.addError(error);
        }
      },
      onDone: () {
        _activeMintSubscription = null;
        if (!controller.isClosed) {
          controller.close();
        }
      },
    );

    return controller.stream;
  }

  /// Guarda un invoice de mint pendiente para recuperarlo después.
  Future<void> _savePendingMintInvoice(
    String quoteId, String invoice, String mintUrl, String unit, BigInt amount,
  ) async {
    try {
      final prefs = await SharedPreferences.getInstance();
      final jsonStr = prefs.getString(_pendingMintInvoicesKey) ?? '{}';
      final map = Map<String, dynamic>.from(jsonDecode(jsonStr));
      map[quoteId] = {
        'invoice': invoice,
        'mintUrl': mintUrl,
        'unit': unit,
        'amount': amount.toString(),
        'createdAt': DateTime.now().toIso8601String(),
      };
      await prefs.setString(_pendingMintInvoicesKey, jsonEncode(map));
      debugPrint('Pending mint invoice guardado: $quoteId');
    } catch (e) {
      debugPrint('Error guardando pending mint invoice: $e');
    }
  }

  /// Elimina un invoice de mint pendiente (ya fue procesado).
  Future<void> _removePendingMintInvoice(String quoteId) async {
    try {
      final prefs = await SharedPreferences.getInstance();
      final jsonStr = prefs.getString(_pendingMintInvoicesKey) ?? '{}';
      final map = Map<String, dynamic>.from(jsonDecode(jsonStr));
      map.remove(quoteId);
      await prefs.setString(_pendingMintInvoicesKey, jsonEncode(map));
      debugPrint('Pending mint invoice eliminado: $quoteId');
    } catch (e) {
      debugPrint('Error eliminando pending mint invoice: $e');
    }
  }

  /// TTL para invoices de mint pendientes (24 horas).
  static const _pendingMintInvoiceTtl = Duration(hours: 24);

  /// Obtiene todos los invoices de mint pendientes, limpiando expirados.
  Future<Map<String, dynamic>> _getPendingMintInvoices() async {
    try {
      final prefs = await SharedPreferences.getInstance();
      final jsonStr = prefs.getString(_pendingMintInvoicesKey) ?? '{}';
      final map = Map<String, dynamic>.from(jsonDecode(jsonStr));

      // Filtrar expirados
      final now = DateTime.now();
      final expired = <String>[];
      for (final entry in map.entries) {
        final data = Map<String, dynamic>.from(entry.value);
        final createdAt = DateTime.tryParse(data['createdAt'] ?? '');
        if (createdAt == null || now.difference(createdAt) > _pendingMintInvoiceTtl) {
          expired.add(entry.key);
        }
      }

      // Limpiar expirados si hay
      if (expired.isNotEmpty) {
        for (final key in expired) {
          map.remove(key);
        }
        await prefs.setString(_pendingMintInvoicesKey, jsonEncode(map));
        debugPrint('Pending mint invoices expirados eliminados: ${expired.length}');
      }

      return map;
    } catch (e) {
      return {};
    }
  }

  /// Busca un invoice pendiente que coincida con un mintUrl y unit.
  /// Retorna el invoice string o null.
  Future<String?> findPendingMintInvoice(String mintUrl, String unit) async {
    final pending = await _getPendingMintInvoices();
    for (final entry in pending.values) {
      final data = Map<String, dynamic>.from(entry);
      if (data['mintUrl'] == mintUrl && data['unit'] == unit) {
        return data['invoice'] as String?;
      }
    }
    return null;
  }

  // ============================================================
  // PENDING NOSTR PAYMENT REQUESTS
  // ============================================================

  /// TTL for pending Nostr requests (24 hours).
  static const _pendingNostrRequestTtl = Duration(hours: 24);
  StreamSubscription<NostrPaymentEvent>? _pendingNostrSubscription;
  bool _pendingNostrResumeAttempted = false;
  static const _pendingNostrSecretKey = 'pending_nostr_secret';
  static const _secureStorage = FlutterSecureStorage();

  /// Save a pending Nostr payment request for recovery after app restart.
  /// Secret key goes to FlutterSecureStorage, metadata to SharedPreferences.
  Future<void> savePendingNostrRequest(PersistedRequestData data) async {
    try {
      // Secret key in encrypted storage
      await _secureStorage.write(
        key: _pendingNostrSecretKey,
        value: data.secretHex,
      );
      // Metadata in SharedPreferences (no secrets)
      final prefs = await SharedPreferences.getInstance();
      await prefs.setString(_pendingNostrRequestKey, jsonEncode({
        'pubkeyHex': data.pubkeyHex,
        'relays': data.relays,
        'amount': data.amount?.toString(),
        'unit': data.unit,
        'mintUrl': data.mintUrl,
        'createdAt': DateTime.now().toIso8601String(),
      }));
    } catch (e) {
      debugPrint('Error saving pending Nostr request: $e');
    }
  }

  /// Remove the pending Nostr payment request (payment received or cancelled).
  Future<void> removePendingNostrRequest() async {
    try {
      await _secureStorage.delete(key: _pendingNostrSecretKey);
      final prefs = await SharedPreferences.getInstance();
      await prefs.remove(_pendingNostrRequestKey);
    } catch (e) {
      debugPrint('Error removing pending Nostr request: $e');
    }
  }

  /// Check for a pending Nostr payment request and resume listening.
  /// Called automatically during app startup. Guarded against duplicate calls.
  Future<void> resumePendingNostrRequest() async {
    if (_pendingNostrResumeAttempted) return;
    _pendingNostrResumeAttempted = true;
    await _pendingNostrSubscription?.cancel();
    _pendingNostrSubscription = null;
    try {
      final prefs = await SharedPreferences.getInstance();
      final jsonStr = prefs.getString(_pendingNostrRequestKey);
      if (jsonStr == null) return;

      final map = Map<String, dynamic>.from(jsonDecode(jsonStr));

      // Check TTL
      final createdAt = DateTime.tryParse(map['createdAt'] ?? '');
      if (createdAt == null ||
          DateTime.now().difference(createdAt) > _pendingNostrRequestTtl) {
        await removePendingNostrRequest();
        debugPrint('Pending Nostr request expired, removed');
        return;
      }

      // Read secret from secure storage
      final secretHex = await _secureStorage.read(key: _pendingNostrSecretKey);
      if (secretHex == null) {
        await prefs.remove(_pendingNostrRequestKey);
        debugPrint('Pending Nostr request has no secret key, removed');
        return;
      }

      // Reconstruct handle
      final data = PersistedRequestData(
        secretHex: secretHex,
        pubkeyHex: map['pubkeyHex'] as String,
        relays: List<String>.from(map['relays']),
        amount: map['amount'] != null
            ? BigInt.tryParse(map['amount'] as String)
            : null,
        unit: map['unit'] as String,
        mintUrl: map['mintUrl'] as String,
      );

      final handle = NostrListenerHandle.fromPersisted(data: data);

      // Get or create the wallet for this mint+unit (lazy instantiation)
      Wallet wallet;
      try {
        wallet = await getWallet(data.mintUrl, data.unit);
      } catch (e) {
        debugPrint('No wallet for pending Nostr request: $e');
        await removePendingNostrRequest();
        return;
      }

      // Resume listening in background
      debugPrint('Resuming pending Nostr payment request...');
      _pendingNostrSubscription = wallet.waitForNostrPayment(handle: handle).listen(
        (event) {
          if (event.state == NostrPaymentState.received) {
            debugPrint('Pending Nostr payment received: ${event.amount}');
            removePendingNostrRequest();
            confettiController.fire();
            _pendingNostrSubscription?.cancel();
            _pendingNostrSubscription = null;
          }
        },
        onError: (error) {
          debugPrint('Pending Nostr listener error (kept for retry): $error');
          _pendingNostrSubscription = null;
          _pendingNostrResumeAttempted = false; // Allow retry on next startup
        },
      );
    } catch (e) {
      debugPrint('Error resuming pending Nostr request: $e');
    }
  }

  /// Guarda metadata para una transacción de mint (Lightning deposit).
  /// Returns true if metadata was actually saved.
  Future<bool> _saveMintMetadata(Wallet wallet, String invoice, String? transactionId) async {
    try {
      if (transactionId != null && transactionId.isNotEmpty) {
        await _txMetaStorage.save(
          transactionId,
          TransactionMeta(
            type: TransactionType.lightning,
            invoice: invoice,
          ),
        );
        debugPrint('Mint metadata guardada para tx $transactionId');
        confettiController.fire();
        notifyListeners();
        return true;
      } else {
        debugPrint('Mint metadata: no transaction ID available');
        confettiController.fire();
        notifyListeners();
        return false;
      }
    } catch (e) {
      debugPrint('Error guardando mint metadata: $e');
      return false;
    }
  }

  // ============================================================
  // MELT (Retirar a Lightning)
  // ============================================================

  /// Invoice temporal para guardar metadata después de melt
  String? _pendingMeltInvoice;

  /// Obtiene quote para pagar un invoice BOLT11.
  /// Guarda el invoice temporalmente para asociarlo a la transacción después.
  Future<MeltQuote> getMeltQuote(String bolt11Invoice) async {
    _pendingMeltInvoice = bolt11Invoice;
    final wallet = await getActiveWallet();
    return await wallet.meltQuote(request: bolt11Invoice);
  }

  /// Ejecuta el pago del invoice.
  /// Guarda metadata type=lightning para identificar en historial.
  Future<BigInt> melt(MeltQuote quote) async {
    final wallet = await getActiveWallet();
    final totalPaid = await wallet.melt(quote: quote);

    // Guardar metadata con el invoice
    if (_pendingMeltInvoice != null) {
      await _saveMeltMetadata(wallet, _pendingMeltInvoice!);
      _pendingMeltInvoice = null;
    }

    notifyListeners();
    return totalPaid;
  }

  /// Guarda metadata para una transacción de melt (Lightning withdrawal).
  Future<void> _saveMeltMetadata(Wallet wallet, String invoice) async {
    try {
      final txs = await wallet.listTransactions(
        direction: TransactionDirection.outgoing,
      );

      if (txs.isNotEmpty) {
        final recentTx = txs.first;

        await _txMetaStorage.save(
          recentTx.id,
          TransactionMeta(
            type: TransactionType.lightning,
            invoice: invoice,
          ),
        );
        debugPrint('Melt metadata guardada para tx ${recentTx.id}');
      }
    } catch (e) {
      debugPrint('Error guardando melt metadata: $e');
    }
  }

  // ============================================================
  // HISTORIAL
  // ============================================================

  /// Obtiene transacciones del wallet activo.
  Future<List<Transaction>> getTransactions({
    TransactionDirection? direction,
  }) async {
    final wallet = activeWallet;
    if (wallet == null) return [];
    return await wallet.listTransactions(direction: direction);
  }

  /// Obtiene transacciones de todos los wallets.
  Future<List<Transaction>> getAllTransactions({
    TransactionDirection? direction,
  }) async {
    final allTransactions = <Transaction>[];

    for (final wallet in _wallets.values) {
      try {
        final txs = await wallet.listTransactions(direction: direction);
        allTransactions.addAll(txs);
      } catch (e) {
        debugPrint('Error obteniendo transacciones: $e');
      }
    }

    // Ordenar por timestamp (más reciente primero)
    allTransactions.sort((a, b) => b.timestamp.compareTo(a.timestamp));

    return allTransactions;
  }

  /// Obtiene el tipo de una transacción (cashu o lightning).
  /// Busca primero en metadata del CDK, luego en storage local.
  /// Si no hay metadata y es incoming, asume Lightning (los receives Cashu
  /// siempre guardan metadata inmediatamente).
  /// Limitación conocida: transacciones anteriores al sistema de metadata
  /// o donde el guardado falló silenciosamente serían clasificadas como Lightning.
  TransactionType getTransactionType(Transaction tx) {
    final type = _txMetaStorage.getType(tx.id, tx.metadata);
    // Si el storage devuelve cashu por defecto pero no tiene metadata real,
    // y la transacción es incoming → probablemente es Lightning
    if (!_txMetaStorage.has(tx.id) &&
        tx.metadata['type'] == null &&
        tx.direction == TransactionDirection.incoming) {
      return TransactionType.lightning;
    }
    return type;
  }

  /// Obtiene metadata adicional de una transacción.
  TransactionMeta? getTransactionMeta(String transactionId) {
    return _txMetaStorage.get(transactionId);
  }

  /// Verifica si una transacción tiene metadata guardada.
  bool hasTransactionMeta(String transactionId) {
    return _txMetaStorage.has(transactionId);
  }

  // ============================================================
  // VERIFICACION DE PROOFS
  // ============================================================

  /// Verifica proofs pendientes en todos los wallets.
  /// Llamar en background al iniciar la app.
  /// También vincula transacciones incoming sin metadata con invoices pendientes.
  Future<void> checkPendingTransactions() async {
    // Iterar todos los mints y todas sus unidades, no solo los wallets
    // ya instanciados. initialize() solo precarga units.first por mint,
    // así que quotes/sagas/melts en otras unidades se perderían.
    for (final entry in _mintUnits.entries) {
      for (final unit in entry.value) {
        try {
          final wallet = await getWallet(entry.key, unit);

          try {
            // Reclamar quotes pagados pero no emitidos (Lightning → proofs)
            await wallet.checkAllMintQuotes();
          } catch (e) {
            debugPrint('Check mint quotes failed: $e');
          }

          try {
            // Recuperar sagas incompletas (swaps/mints/melts interrumpidos)
            await wallet.recoverIncompleteSagas();
          } catch (e) {
            debugPrint('Recover incomplete sagas failed: $e');
          }

          try {
            // Finalizar retiros Lightning que quedaron a medias
            await wallet.finalizePendingMelts();
          } catch (e) {
            debugPrint('Finalize pending melts failed: $e');
          }

          try {
            await wallet.checkPendingTransactions();
          } catch (e) {
            // Silencioso - puede fallar offline
            debugPrint('Check pending failed: $e');
          }
        } catch (e) {
          debugPrint('Error getting wallet ${entry.key}:$unit: $e');
        }
      }
    }

    // Vincular transacciones incoming sin metadata con pending invoices
    await _matchPendingMintInvoices();

    // Resume pending Nostr payment request if app was killed mid-wait
    await resumePendingNostrRequest();
  }

  /// Busca transacciones incoming sin metadata y las vincula con
  /// invoices de mint pendientes guardados en SharedPreferences.
  Future<void> _matchPendingMintInvoices() async {
    try {
      final pending = await _getPendingMintInvoices();
      if (pending.isEmpty) return;

      // Obtener todas las transacciones incoming
      final allTxs = await getAllTransactions(
        direction: TransactionDirection.incoming,
      );

      for (final tx in allTxs) {
        // Solo procesar transacciones sin metadata
        if (_txMetaStorage.has(tx.id)) continue;

        // Buscar un pending invoice que coincida con mintUrl, unit y amount
        String? matchedQuoteId;
        String? matchedInvoice;

        for (final entry in pending.entries) {
          final data = Map<String, dynamic>.from(entry.value);
          if (data['mintUrl'] == tx.mintUrl && data['unit'] == tx.unit && data['amount'] == tx.amount.toString()) {
            matchedQuoteId = entry.key;
            matchedInvoice = data['invoice'] as String?;
            break;
          }
        }

        if (matchedQuoteId != null && matchedInvoice != null) {
          await _txMetaStorage.save(
            tx.id,
            TransactionMeta(
              type: TransactionType.lightning,
              invoice: matchedInvoice,
            ),
          );
          // Limpiar el pending invoice ya vinculado
          await _removePendingMintInvoice(matchedQuoteId);
          pending.remove(matchedQuoteId);
          debugPrint('Matched pending mint invoice → tx ${tx.id}');
          confettiController.fire();
          notifyListeners();
        }
      }
    } catch (e) {
      debugPrint('Error matching pending mint invoices: $e');
    }
  }

  /// Verifica si un token específico fue gastado.
  Future<bool> isTokenSpent(String encodedToken) async {
    final wallet = activeWallet;
    if (wallet == null) return false;

    final token = Token.parse(encoded: encodedToken);
    return await wallet.isTokenSpent(token: token);
  }

  // ============================================================
  // RESTORE (NUT-13 + NUT-09)
  // ============================================================

  /// Escanea un mint para todas sus unidades usando NUT-13.
  /// Retorna Map con unit como clave y balance recuperado como valor.
  Future<Map<String, BigInt>> restoreFromMint(String mintUrl) async {
    if (_db == null || _mnemonic == null) {
      throw Exception('WalletProvider no inicializado');
    }

    final results = <String, BigInt>{};
    final units = _mintUnits[mintUrl] ?? ['sat'];

    for (final unit in units) {
      try {
        final wallet = await getWallet(mintUrl, unit);
        final balanceBefore = await wallet.balance();

        await wallet.restore();

        final balanceAfter = await wallet.balance();
        final recovered = balanceAfter - balanceBefore;
        results[unit] = recovered;
      } catch (e) {
        debugPrint('Error restaurando $mintUrl:$unit: $e');
        results[unit] = BigInt.from(-1); // Indica error
      }
    }

    notifyListeners();
    return results;
  }

  /// Escanea todos los mints conocidos para recuperar tokens.
  Future<Map<String, Map<String, BigInt>>> restoreAllMints() async {
    final results = <String, Map<String, BigInt>>{};

    for (final mintUrl in _mintUnits.keys) {
      results[mintUrl] = await restoreFromMint(mintUrl);
    }

    return results;
  }

  /// Restaura tokens usando otro mnemonic y los transfiere al wallet actual.
  Future<BigInt> restoreWithMnemonic(
    String mnemonic,
    List<String> mintUrls,
  ) async {
    if (_db == null || _mnemonic == null) {
      throw Exception('WalletProvider no inicializado');
    }

    BigInt totalRecovered = BigInt.zero;

    // Normalizar mnemonic
    final normalizedMnemonic =
        mnemonic.trim().toLowerCase().replaceAll(RegExp(r'\s+'), ' ');

    // Crear DB temporal
    final dir = await getApplicationDocumentsDirectory();
    final tempDbPath =
        '${dir.path}/temp_restore_${DateTime.now().millisecondsSinceEpoch}.sqlite';

    WalletDatabase? tempDb;

    try {
      tempDb = await WalletDatabase.newInstance(path: tempDbPath);

      for (final mintUrl in mintUrls) {
        // Obtener unidades del mint
        final units = _mintUnits[mintUrl] ?? ['sat'];

        for (final unit in units) {
          try {
            // Crear wallet temporal
            final tempWallet = Wallet(
              mintUrl: mintUrl,
              unit: unit,
              mnemonic: normalizedMnemonic,
              db: tempDb,
            );

            // Escanear
            await tempWallet.restore();
            final tempBalance = await tempWallet.balance();

            if (tempBalance > BigInt.zero) {
              // Enviar todo el balance
              final prepared =
                  await tempWallet.prepareSend(amount: tempBalance);
              final result = await tempWallet.send(
                send: prepared,
                memo: 'Recuperación El Caju',
                includeMemo: true,
              );

              // Reclamar en nuestro wallet
              final ourWallet = await getWallet(mintUrl, unit);
              final received = await ourWallet.receive(token: result.token);
              totalRecovered += received;
            }
          } catch (e) {
            debugPrint('Error restaurando $mintUrl:$unit: $e');
          }
        }
      }
    } finally {
      // Limpiar DB temporal
      tempDb = null;
      try {
        final tempFile = File(tempDbPath);
        if (await tempFile.exists()) {
          await tempFile.delete();
        }
      } catch (e) {
        debugPrint('Error limpiando DB temporal: $e');
      }
    }

    notifyListeners();
    return totalRecovered;
  }

  // ============================================================
  // BORRAR WALLET
  // ============================================================

  /// Borra la base de datos SQLite del wallet.
  Future<bool> deleteDatabase() async {
    try {
      // Limpiar referencias
      _wallets.clear();
      _mintUnits.clear();
      _activeMintUrl = null;
      _activeUnit = 'sat';
      _mnemonic = null;
      _db = null;

      // Limpiar metadata de transacciones
      await _txMetaStorage.clear();

      // Borrar archivo
      final dir = await getApplicationDocumentsDirectory();
      final dbPath = '${dir.path}/elcaju_wallet.sqlite';
      final dbFile = File(dbPath);
      if (await dbFile.exists()) {
        await dbFile.delete();
      }

      notifyListeners();
      return true;
    } catch (e) {
      debugPrint('Error al borrar database: $e');
      return false;
    }
  }

  /// Resetea el estado del provider (sin borrar la DB).
  void reset() {
    _wallets.clear();
    _mintUnits.clear();
    _activeMintUrl = null;
    _activeUnit = 'sat';
    _mnemonic = null;
    _db = null;
    notifyListeners();
  }

  // ============================================================
  // PENDING TOKENS (Receive Later)
  // ============================================================

  /// Guarda un token para reclamar después.
  /// Retorna el PendingToken creado o null si hay error/límite alcanzado.
  Future<PendingToken?> addPendingToken(String encodedToken) async {
    // Parsear token para validar y extraer info
    final tokenInfo = parseToken(encodedToken);
    if (tokenInfo == null) {
      debugPrint('Token inválido para pending');
      return null;
    }

    // Generar ID único
    final id = _uuid.v4();

    // Guardar en storage
    final pending = await _pendingTokenStorage.add(
      id: id,
      encoded: encodedToken,
      amount: tokenInfo.amount,
      mintUrl: tokenInfo.mintUrl,
      unit: tokenInfo.unit,
    );

    if (pending != null) {
      notifyListeners();
    }

    return pending;
  }

  /// Lista todos los tokens pendientes
  List<PendingToken> listPendingTokens() {
    return _pendingTokenStorage.listAll();
  }

  /// Lista solo tokens pendientes válidos (no expirados)
  List<PendingToken> listValidPendingTokens() {
    return _pendingTokenStorage.listValid();
  }

  /// Elimina un token pendiente por ID
  Future<void> removePendingToken(String id) async {
    await _pendingTokenStorage.remove(id);
    notifyListeners();
  }

  /// Intenta reclamar un token pendiente.
  /// Retorna el monto recibido si tiene éxito, o lanza excepción.
  /// Si el token está gastado o es inválido, lo elimina automáticamente.
  /// Verifica conectividad al mint antes de intentar reclamar.
  /// [p2pkPrivateKey] clave privada para desbloquear tokens P2PK.
  Future<BigInt> claimPendingToken(String id, {String? p2pkPrivateKey}) async {
    final pending = _pendingTokenStorage.get(id);
    if (pending == null) {
      throw Exception('Token pendiente no encontrado');
    }

    // Verificar conectividad al mint primero (evita esperas largas sin conexión)
    final canReach = await canReachMint(pending.mintUrl);
    if (!canReach) {
      throw Exception('No connection to mint');
    }

    try {
      BigInt amount;

      if (P2PKUtils.isP2PKLocked(pending.encoded) && p2pkPrivateKey == null) {
        throw Exception('P2PK token requires a private key to claim');
      }

      amount = await receiveToken(
        pending.encoded,
        p2pkPrivateKey: p2pkPrivateKey,
      );

      // Éxito: eliminar de pending
      await _pendingTokenStorage.remove(id);
      notifyListeners();

      debugPrint('Token pendiente reclamado: $id, monto: $amount');
      return amount;
    } catch (e) {
      final errorStr = e.toString().toLowerCase();

      // Si el token ya fue gastado o es inválido, eliminarlo
      if (errorStr.contains('already spent') ||
          errorStr.contains('token already') ||
          errorStr.contains('invalid')) {
        await _pendingTokenStorage.remove(id);
        notifyListeners();
        debugPrint('Token pendiente eliminado (gastado/inválido): $id');
      } else {
        // Otro error: registrar intento fallido
        await _pendingTokenStorage.recordFailedAttempt(id, e.toString());
      }

      rethrow;
    }
  }

  /// Verifica y reclama automáticamente tokens pendientes.
  /// Retorna un mapa con estadísticas: claimed, failed, removed, totalClaimed, unit.
  /// [p2pkKeyResolver] función que dado un token encoded retorna la clave privada P2PK (o null).
  Future<Map<String, dynamic>> checkPendingTokens({String? Function(String encodedToken)? p2pkKeyResolver}) async {
    final tokens = _pendingTokenStorage.listValid();
    if (tokens.isEmpty) {
      return {'claimed': 0, 'failed': 0, 'removed': 0, 'totalClaimed': BigInt.zero, 'unit': _activeUnit};
    }

    int claimed = 0;
    int failed = 0;
    int removed = 0;
    BigInt totalClaimed = BigInt.zero;
    String? claimedUnit;

    for (final token in tokens) {
      try {
        final p2pkKey = p2pkKeyResolver?.call(token.encoded);
        final amount = await claimPendingToken(token.id, p2pkPrivateKey: p2pkKey);
        claimed++;
        totalClaimed += amount;
        claimedUnit ??= token.unit; // Usar la unidad del primer token reclamado
        debugPrint('Auto-claim exitoso: ${token.id}');
      } catch (e) {
        final errorStr = e.toString().toLowerCase();
        if (errorStr.contains('already spent') ||
            errorStr.contains('token already') ||
            errorStr.contains('invalid')) {
          removed++;
          debugPrint('Auto-claim: token eliminado (gastado): ${token.id}');
        } else {
          failed++;
          debugPrint('Auto-claim fallido: ${token.id} - $e');
        }
      }
    }

    // Limpiar expirados también
    final expired = await _pendingTokenStorage.cleanExpired();
    removed += expired;

    if (claimed > 0 || removed > 0) {
      notifyListeners();
    }

    return {
      'claimed': claimed,
      'failed': failed,
      'removed': removed,
      'totalClaimed': totalClaimed,
      'unit': claimedUnit ?? _activeUnit,
    };
  }

  /// Limpia tokens pendientes expirados
  Future<int> cleanExpiredPendingTokens() async {
    final cleaned = await _pendingTokenStorage.cleanExpired();
    if (cleaned > 0) {
      notifyListeners();
    }
    return cleaned;
  }

  @override
  void dispose() {
    _activeMintSubscription?.cancel();
    if (_activeMintController != null && !_activeMintController!.isClosed) {
      _activeMintController!.close();
    }
    confettiController.dispose();
    super.dispose();
  }
}
