import 'dart:async';
import 'dart:math' as math;

import 'package:dio/dio.dart' show DioException, DioExceptionType;
import 'package:flutter/foundation.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';

import '../core/network/api_response.dart';
import '../core/network/dio_client.dart';
import '../core/network/spot_ws_client.dart';
import '../data/services/spot_service.dart';
import 'auth_provider.dart';
import 'profile_provider.dart';
import 'spot_ws_provider.dart';

// 枚举

/// 买卖方向
enum SpotSide { buy, sell }

/// 限价 / 市价 / 条件市价（条件为客户端占位）
enum SpotOrderType { limit, market, conditionalMarket }

/// 交易页底栏：委托 / 资产
enum SpotTab { orders, assets }

/// 数量单位：base 或 quote（市价买固定 quote）
enum SpotAmountUnit { base, quote }

// 数据模型

class SpotSymbolInfo {
  final String symbol; // 如 BTC/USDT
  final String base;   // BTC
  final String quote;  // USDT
  final int pricePrecision;
  final int volumePrecision;
  final int depth0Pre;
  final int depth1Pre;
  final int depth2Pre;
  final double minLimitPrice;
  final double minLimitVolume;
  final double minMarketBuy;
  final double minMarketSell;
  final bool enableMarketBuy;
  final bool enableMarketSell;
  final double makerFee;
  final double takerFee;

  const SpotSymbolInfo({
    required this.symbol,
    required this.base,
    required this.quote,
    this.pricePrecision = 2,
    this.volumePrecision = 4,
    this.depth0Pre = 2,
    this.depth1Pre = 4,
    this.depth2Pre = 6,
    this.minLimitPrice = 0,
    this.minLimitVolume = 0,
    this.minMarketBuy = 0,
    this.minMarketSell = 0,
    this.enableMarketBuy = true,
    this.enableMarketSell = true,
    this.makerFee = 0.001,
    this.takerFee = 0.001,
  });

  factory SpotSymbolInfo.fromJson(Map<String, dynamic> json) {
    final sym = (json['symbol'] as String? ?? '').toUpperCase();
    final base = (json['base'] as String? ?? '').toUpperCase();
    final quote = (json['quote'] as String? ?? 'USDT').toUpperCase();
    return SpotSymbolInfo(
      symbol: sym,
      base: base.isNotEmpty ? base : _deriveBaseFallback(sym),
      quote: quote.isNotEmpty ? quote : 'USDT',
      pricePrecision: _toInt(json['pricePre']) ?? 2,
      volumePrecision: _toInt(json['volumePre']) ?? 4,
      depth0Pre: _toInt(json['depth0Pre']) ?? 2,
      depth1Pre: _toInt(json['depth1Pre']) ?? 4,
      depth2Pre: _toInt(json['depth2Pre']) ?? 6,
      minLimitPrice: _toDouble(json['limitPriceMin']),
      minLimitVolume: _toDouble(json['limitVolumeMin']),
      minMarketBuy: _toDouble(json['marketBuyMin']),
      minMarketSell: _toDouble(json['marketSellMin']),
      enableMarketBuy: (json['marketBuyMin'] as num? ?? 0) > 0,
      enableMarketSell: (json['marketSellMin'] as num? ?? 0) > 0,
      makerFee: _toDouble(json['makerFee']),
      takerFee: _toDouble(json['takerFee']),
    );
  }

  static String _deriveBaseFallback(String sym) {
    for (final q in const ['USDT', 'BTC', 'ETH', 'BUSD']) {
      if (sym.endsWith(q) && sym.length > q.length) {
        return sym.substring(0, sym.length - q.length);
      }
    }
    return sym;
  }
}

class SpotWalletAsset {
  final String coin;
  final double balance;
  final double frozenBalance;

  const SpotWalletAsset({
    required this.coin,
    required this.balance,
    required this.frozenBalance,
  });

  double get total => balance + frozenBalance;

  factory SpotWalletAsset.fromJson(Map<String, dynamic> json) {
    final coin = (json['symbol'] ?? json['coinId'] ?? json['coin'])
            ?.toString()
            .toUpperCase() ??
        '';
    return SpotWalletAsset(
      coin: coin,
      balance: _toDouble(json['available'] ?? json['balance']),
      frozenBalance: _toDouble(json['frozen'] ?? json['frozenBalance']),
    );
  }
}

class SpotOrder {
  final String id;
  final String symbol;     // BTCUSDT
  final SpotSide side;
  final SpotOrderType type;
  /// 后端 type：1 限价 2 市价 3 条件
  final int typeCode;
  /// 后端 status，未知 -1
  final int statusCode;
  final double price;       // 限价委托价
  final double amount;      // 委托量（市价买为 USDT 金额）
  final double tradedAmount;
  final double tradedTurnover;
  final double avgPrice;
  final String status;     // 文本：委托中/部分成交/已成交/已撤销
  final DateTime? createTime;

  const SpotOrder({
    required this.id,
    required this.symbol,
    required this.side,
    required this.type,
    this.typeCode = 1,
    this.statusCode = -1,
    required this.price,
    required this.amount,
    this.tradedAmount = 0,
    this.tradedTurnover = 0,
    this.avgPrice = 0,
    this.status = '',
    this.createTime,
  });

  bool get isPending => status == '委托中' || status == '部分成交';

  factory SpotOrder.fromJson(Map<String, dynamic> json) {
    final dirRaw =
        (json['side'] ?? json['direction'] ?? 'BUY').toString().toUpperCase();
    final side = dirRaw == 'BUY' ? SpotSide.buy : SpotSide.sell;

    final typeRaw = json['type'];
    int typeCode = 1;
    if (typeRaw is int) {
      typeCode = typeRaw;
    } else if (typeRaw is String) {
      final tr = typeRaw.toUpperCase();
      if (tr == 'MARKET_PRICE' || tr == 'MARKET') {
        typeCode = 2;
      } else if (tr == 'STOP' || tr == 'STOP_LOSS') {
        typeCode = 3;
      } else {
        typeCode = int.tryParse(typeRaw) ?? 1;
      }
    }

    final SpotOrderType type;
    if (typeCode == 2 || typeRaw == 'MARKET_PRICE' || typeRaw == 'MARKET') {
      type = SpotOrderType.market;
    } else if (typeCode == 3) {
      type = SpotOrderType.conditionalMarket;
    } else {
      type = SpotOrderType.limit;
    }

    final ts = json['ctime'] ?? json['time'] ?? json['createTime'];
    DateTime? createTime;
    if (ts is int && ts > 0) {
      createTime = DateTime.fromMillisecondsSinceEpoch(
        ts > 9999999999 ? ts : ts * 1000,
      );
    } else if (ts is String) {
      createTime = DateTime.tryParse(ts);
    }

    final statusRaw = json['status'];
    final statusStr =
        statusRaw is String ? statusRaw : statusRaw?.toString() ?? '';
    final statusCode = _parseStatusCode(statusRaw, statusStr);
    final status = _mapStatus(statusStr);

    return SpotOrder(
      id: (json['id'] ?? json['orderId'] ?? '').toString(),
      symbol: (json['symbol'] as String? ?? '').toUpperCase(),
      side: side,
      type: type,
      typeCode: typeCode,
      statusCode: statusCode,
      price: _toDouble(json['price']),
      amount: _toDouble(json['volume'] ?? json['amount']),
      tradedAmount: _toDouble(json['dealVolume'] ?? json['tradedAmount']),
      tradedTurnover: _toDouble(json['dealMoney'] ?? json['turnover']),
      avgPrice: _toDouble(json['avgPrice'] ?? json['tradedAvgPrice']),
      status: status,
      createTime: createTime,
    );
  }

  static int _parseStatusCode(dynamic statusRaw, String statusStr) {
    if (statusRaw is int) return statusRaw;
    final n = int.tryParse(statusStr);
    if (n != null) return n;
    switch (statusStr.toUpperCase()) {
      case 'INIT':
      case 'NEW':
      case 'TRADING':
        return 1;
      case 'FILLED':
      case 'COMPLETED':
        return 2;
      case 'PART_FILLED':
      case 'PART_TRADED':
        return 3;
      case 'CANCELED':
      case 'CANCELLED':
        return 4;
      case 'PENDING_CANCEL':
        return 5;
      case 'EXPIRED':
      case 'OVERTIMED':
        return 6;
      default:
        return -1;
    }
  }

  static String _mapStatus(String raw) {
    switch (raw.toUpperCase()) {
      case '0':
      case '1':
        return '委托中';
      case '3':
        return '部分成交';
      case '2':
        return '已成交';
      case '4':
      case '5':
      case '6':
        return '已撤销';
      case 'TRADING':
      case 'NEW':
      case 'INIT':
        return '委托中';
      case 'PART_FILLED':
      case 'PART_TRADED':
        return '部分成交';
      case 'COMPLETED':
      case 'FILLED':
        return '已成交';
      case 'CANCELED':
      case 'CANCELLED':
      case 'OVERTIMED':
        return '已撤销';
      default:
        return raw;
    }
  }
}

/// trade_ticker 成交一条
class SpotPublicTrade {
  const SpotPublicTrade({
    required this.price,
    required this.quantity,
    required this.isBuyerMaker,
    required this.time,
    this.tradeId = '',
  });
  final double price;
  final double quantity;
  final bool isBuyerMaker;
  final int time;
  final String tradeId;
}

class SpotState {
  /// 交易对，如 BTCUSDT
  final String symbol;
  final SpotSymbolInfo? info;

  // 行情
  final double lastPrice;
  final String? lastPriceStr; // WS 返回的原始价格字符串
  final double change24h;
  final List<Map<String, dynamic>> orderBookAsks;
  final List<Map<String, dynamic>> orderBookBids;
  final List<SpotPublicTrade> recentPublicTrades;

  // 钱包
  final List<SpotWalletAsset> wallets;
  final double totalAmount; // USDT 总估值
  final double todayPnl;
  final double todayPnlRate;

  // 委托
  final List<SpotOrder> openOrders;
  final bool ordersHasMore;
  final int ordersPage;

  // 表单
  final SpotSide side;
  final SpotOrderType orderType;
  final SpotAmountUnit amountUnit; // 限价单可切换；市价买默认 quote(USDT)，市价卖默认 base
  final double sliderPercent;       // 0..1

  // UI 状态
  final SpotTab activeTab;
  final bool isLoading;        // 首屏骨架
  final bool isTabLoading;     // 列表加载
  final bool hideOtherSymbols;

  const SpotState({
    required this.symbol,
    this.info,
    this.lastPrice = 0,
    this.lastPriceStr,
    this.change24h = 0,
    this.orderBookAsks = const [],
    this.orderBookBids = const [],
    this.recentPublicTrades = const [],
    this.wallets = const [],
    this.totalAmount = 0,
    this.todayPnl = 0,
    this.todayPnlRate = 0,
    this.openOrders = const [],
    this.ordersHasMore = false,
    this.ordersPage = 1,
    this.side = SpotSide.buy,
    this.orderType = SpotOrderType.market,
    this.amountUnit = SpotAmountUnit.quote,
    this.sliderPercent = 0,
    this.activeTab = SpotTab.orders,
    this.isLoading = true,
    this.isTabLoading = false,
    this.hideOtherSymbols = false,
  });

  String get apiSymbol => symbol.replaceAll('/', '').replaceAll('-', '').toUpperCase();
  String get baseCoin => info?.base ?? _deriveBase(symbol);
  String get quoteCoin => info?.quote ?? 'USDT';
  int get pricePrecision => info?.pricePrecision ?? 2;
  int get volumePrecision => info?.volumePrecision ?? 4;
  int get depth0Pre => info?.depth0Pre ?? 2;
  int get depth1Pre => info?.depth1Pre ?? 4;
  int get depth2Pre => info?.depth2Pre ?? 6;

  SpotAmountUnit get effectiveAmountUnit {
    if (orderType == SpotOrderType.limit) return amountUnit;
    return side == SpotSide.buy ? SpotAmountUnit.quote : SpotAmountUnit.base;
  }

  bool get showPriceInput => orderType == SpotOrderType.limit;
  bool get showTriggerPrice => orderType == SpotOrderType.conditionalMarket;

  /// 可用 USDT
  double get availableQuote {
    final w = wallets.firstWhere(
      (a) => a.coin == quoteCoin,
      orElse: () => SpotWalletAsset(coin: quoteCoin, balance: 0, frozenBalance: 0),
    );
    return w.balance;
  }

  /// 可用 base
  double get availableBase {
    final w = wallets.firstWhere(
      (a) => a.coin == baseCoin,
      orElse: () => SpotWalletAsset(coin: baseCoin, balance: 0, frozenBalance: 0),
    );
    return w.balance;
  }

  /// openOrders 为全量；开启「隐藏其他」时按本交易对过滤。
  List<SpotOrder> get displayOrders {
    if (!hideOtherSymbols) return openOrders;
    return openOrders.where((o) => o.symbol == apiSymbol).toList();
  }

  static String _deriveBase(String s) {
    final up = s.toUpperCase().replaceAll('/', '').replaceAll('-', '');
    for (final q in const ['USDT', 'BTC', 'ETH', 'BUSD']) {
      if (up.endsWith(q) && up.length > q.length) {
        return up.substring(0, up.length - q.length);
      }
    }
    return up;
  }

  SpotState copyWith({
    String? symbol,
    SpotSymbolInfo? info,
    double? lastPrice,
    String? lastPriceStr,
    double? change24h,
    List<Map<String, dynamic>>? orderBookAsks,
    List<Map<String, dynamic>>? orderBookBids,
    List<SpotPublicTrade>? recentPublicTrades,
    List<SpotWalletAsset>? wallets,
    double? totalAmount,
    double? todayPnl,
    double? todayPnlRate,
    List<SpotOrder>? openOrders,
    bool? ordersHasMore,
    int? ordersPage,
    SpotSide? side,
    SpotOrderType? orderType,
    SpotAmountUnit? amountUnit,
    double? sliderPercent,
    SpotTab? activeTab,
    bool? isLoading,
    bool? isTabLoading,
    bool? hideOtherSymbols,
  }) {
    return SpotState(
      symbol: symbol ?? this.symbol,
      info: info ?? this.info,
      lastPrice: lastPrice ?? this.lastPrice,
      lastPriceStr: lastPriceStr ?? this.lastPriceStr,
      change24h: change24h ?? this.change24h,
      orderBookAsks: orderBookAsks ?? this.orderBookAsks,
      orderBookBids: orderBookBids ?? this.orderBookBids,
      recentPublicTrades: recentPublicTrades ?? this.recentPublicTrades,
      wallets: wallets ?? this.wallets,
      totalAmount: totalAmount ?? this.totalAmount,
      todayPnl: todayPnl ?? this.todayPnl,
      todayPnlRate: todayPnlRate ?? this.todayPnlRate,
      openOrders: openOrders ?? this.openOrders,
      ordersHasMore: ordersHasMore ?? this.ordersHasMore,
      ordersPage: ordersPage ?? this.ordersPage,
      side: side ?? this.side,
      orderType: orderType ?? this.orderType,
      amountUnit: amountUnit ?? this.amountUnit,
      sliderPercent: sliderPercent ?? this.sliderPercent,
      activeTab: activeTab ?? this.activeTab,
      isLoading: isLoading ?? this.isLoading,
      isTabLoading: isTabLoading ?? this.isTabLoading,
      hideOtherSymbols: hideOtherSymbols ?? this.hideOtherSymbols,
    );
  }
}

// Notifier

class SpotNotifier extends AutoDisposeFamilyNotifier<SpotState, String> {
  StreamSubscription<Map<String, dynamic>>? _tickerSub;
  StreamSubscription<Map<String, dynamic>>? _depthSub;
  StreamSubscription<Map<String, dynamic>>? _tradeSub;
  StreamSubscription<Map<String, dynamic>>? _assetSub;
  StreamSubscription<Map<String, dynamic>>? _orderSub;
  bool _userPushChannelsRetained = false;

  SpotService get _service => SpotService(ref.read(dioClientProvider));

  @override
  SpotState build(String symbol) {
    ref.onDispose(_dispose);

    ref.listen<SpotWsClient>(spotWsClientProvider, (prev, next) {
      if (prev != null && !identical(prev, next)) {
        Future.microtask(() => _bindAllStreams(symbol));
      }
    });

    ref.listen<AsyncValue<SpotWsState>>(spotWsConnectionStateProvider,
        (prev, next) {
      final s = next.valueOrNull;
      if (s != SpotWsState.connected) return;
      if (!ref.read(isLoggedInProvider)) return;
      if (prev?.valueOrNull == SpotWsState.reconnecting) {
        Future.microtask(() async {
          try {
            await Future.wait([_loadWallets(), _loadCurrentOrders()]);
          } catch (_) {}
        });
      }
    });

    Future.microtask(() => _init(symbol));

    ref.listen<bool>(isLoggedInProvider, (prev, loggedIn) {
      if (loggedIn) {
        Future.microtask(() async {
          try {
            await Future.wait([_loadWallets(), _loadCurrentOrders()]);
          } catch (_) {}
          _subscribeUserPushChannels();
        });
      } else {
        _releaseSpotUserPushChannels();
        state = state.copyWith(
          wallets: const [],
          openOrders: const [],
        );
      }
    });

    return SpotState(symbol: symbol, isLoading: true);
  }

  Future<void> _init(String symbol) async {
    await _loadSymbolInfo(symbol);
    _subscribeWebSocket(symbol);
    state = state.copyWith(isLoading: false, isTabLoading: true);
    if (ref.read(isLoggedInProvider)) {
      try {
        await Future.wait([_loadWallets(), _loadCurrentOrders()]);
      } catch (_) {}
      _subscribeUserPushChannels();
    }
    state = state.copyWith(isTabLoading: false);
  }

  Future<void> _loadSymbolInfo(String symbol) async {
    try {
      final list = await _service.getSymbols();
      final sym = symbol.replaceAll('/', '').replaceAll('-', '').toUpperCase();
      final match = list.where((m) {
        final s = (m['symbol'] as String? ?? '').toUpperCase();
        return s == sym;
      }).firstOrNull;
      if (match != null) {
        state = state.copyWith(info: SpotSymbolInfo.fromJson(match));
      }
    } catch (_) {}
  }

  Future<void> _loadWallets() async {
    if (!ref.read(isLoggedInProvider)) return;
    try {
      final data = await _service.getAssets();
      final list = data['assetList'];
      if (list is List) {
        final wallets = list
            .whereType<Map<String, dynamic>>()
            .map(SpotWalletAsset.fromJson)
            .toList();
        state = state.copyWith(
          wallets: wallets,
          totalAmount: _toDouble(data['totalAmount']),
          todayPnl: _toDouble(data['todayPnl']),
          todayPnlRate: _toDouble(data['todayPnlRate']),
        );
      }
    } catch (_) {}
  }

  Future<void> _loadCurrentOrders() async {
    if (!ref.read(isLoggedInProvider)) return;
    try {
      final data = await _service.getCurrentOrders();
      final records = data['records'];
      if (records is List) {
        final orders = records
            .whereType<Map<String, dynamic>>()
            .map(SpotOrder.fromJson)
            .toList();
        state = state.copyWith(openOrders: orders);
      }
    } catch (_) {}
  }

  Future<void> _loadOrderBook(String symbol) async {}

  void _bindAllStreams(String symbol) {
    _tickerSub?.cancel();
    _depthSub?.cancel();
    _tradeSub?.cancel();
    _tickerSub = null;
    _depthSub = null;
    _tradeSub = null;
    _cancelUserPushStreamSubscriptionsOnly();
    _userPushChannelsRetained = false;
    _subscribeWebSocket(symbol);
    if (ref.read(isLoggedInProvider)) {
      _subscribeUserPushChannels();
    }
  }

  void _subscribeUserPushChannels() {
    if (!ref.read(isLoggedInProvider)) return;
    final uid = ref.read(profileProvider).user.uid;
    if (uid.isEmpty) return;

    if (_userPushChannelsRetained) {
      try {
        final ws = ref.read(spotWsClientProvider);
        ws.releaseSpotAssetChannel();
        ws.releaseSpotOrderChannel();
      } catch (_) {}
      _userPushChannelsRetained = false;
    }
    _cancelUserPushStreamSubscriptionsOnly();

    final ws = ref.read(spotWsClientProvider);
    ws.retainSpotAssetChannel();
    ws.retainSpotOrderChannel();
    _userPushChannelsRetained = true;
    _assetSub = ws.assetStream.listen(_onAssetPush);
    _orderSub = ws.orderStream.listen(_onOrderPush);
  }

  void _cancelUserPushStreamSubscriptionsOnly() {
    _assetSub?.cancel();
    _orderSub?.cancel();
    _assetSub = null;
    _orderSub = null;
  }

  void _releaseSpotUserPushChannels() {
    _cancelUserPushStreamSubscriptionsOnly();
    if (!_userPushChannelsRetained) return;
    try {
      final ws = ref.read(spotWsClientProvider);
      ws.releaseSpotAssetChannel();
      ws.releaseSpotOrderChannel();
    } catch (_) {}
    _userPushChannelsRetained = false;
  }

  void _onAssetPush(Map<String, dynamic> msg) {
    final list = msg['accountList'];
    if (list is! List) return;
    if (list.isEmpty) {
      Future.microtask(() => _loadWallets());
      return;
    }
    final merged = _mergeAccountListIntoWallets(state.wallets, list);
    state = state.copyWith(wallets: merged);
  }

  void _onOrderPush(Map<String, dynamic> msg) {
    final list = msg['orderList'];
    if (list is! List) return;

    final oc = msg['orderCount'];
    final int? totalPending = oc is int
        ? oc
        : oc is num
            ? oc.toInt()
            : int.tryParse(oc?.toString() ?? '');

    if (list.isEmpty) {
      if (totalPending == 0) {
        state = state.copyWith(openOrders: []);
      } else {
        Future.microtask(() => _loadCurrentOrders());
      }
      return;
    }

    final incoming = <SpotOrder>[];
    final symbolsTouched = <String>{};
    for (final e in list) {
      if (e is Map) {
        final o = SpotOrder.fromJson(Map<String, dynamic>.from(e));
        incoming.add(o);
        if (o.symbol.isNotEmpty) {
          symbolsTouched.add(o.symbol.toUpperCase());
        }
      }
    }
    if (symbolsTouched.isEmpty) {
      state = state.copyWith(openOrders: incoming);
      return;
    }
    final kept = [
      for (final o in state.openOrders)
        if (!symbolsTouched.contains(o.symbol.toUpperCase())) o,
    ];
    state = state.copyWith(openOrders: [...kept, ...incoming]);
  }

  List<SpotWalletAsset> _mergeAccountListIntoWallets(
    List<SpotWalletAsset> current,
    List<dynamic> incoming,
  ) {
    final byCoin = <String, SpotWalletAsset>{
      for (final w in current) w.coin: w,
    };
    for (final raw in incoming) {
      if (raw is! Map) continue;
      final a = SpotWalletAsset.fromJson(Map<String, dynamic>.from(raw));
      if (a.coin.isEmpty) continue;
      byCoin[a.coin] = a;
    }
    final out = byCoin.values.toList()
      ..sort((a, b) => a.coin.compareTo(b.coin));
    return out;
  }

  void _subscribeWebSocket(String symbol) {
    final wsSymbol = symbol.replaceAll('/', '').replaceAll('-', '').toLowerCase();
    final ws = ref.read(spotWsClientProvider);

    ws.subscribeTicker(wsSymbol);
    ws.subscribeDepth(wsSymbol);
    ws.subscribeTrade(wsSymbol);

    _tickerSub?.cancel();
    _tickerSub = ws.tickerStream
        .where((d) => (d['symbol'] as String? ?? '').toLowerCase() == wsSymbol)
        .listen((data) {
      final price = (data['price'] as num?)?.toDouble() ?? 0;
      final change = (data['change24h'] as num?)?.toDouble() ?? state.change24h;
      final priceStr = data['priceStr'] as String? ?? '';
      if (price > 0) {
        state = state.copyWith(
          lastPrice: price,
          lastPriceStr: priceStr.isNotEmpty ? priceStr : null,
          change24h: change,
        );
      }
    });

    _depthSub?.cancel();
    _depthSub = ws.depthStream
        .where((d) => (d['symbol'] as String? ?? '').toLowerCase() == wsSymbol)
        .listen((data) {
      if (!data.containsKey('asks') && !data.containsKey('bids')) return;
      var asks = state.orderBookAsks;
      var bids = state.orderBookBids;
      if (data.containsKey('asks')) {
        asks = _normalizeDepthMaps(data['asks']);
      }
      if (data.containsKey('bids')) {
        bids = _normalizeDepthMaps(data['bids']);
      }
      state = state.copyWith(orderBookAsks: asks, orderBookBids: bids);
    });

    _tradeSub?.cancel();
    _tradeSub = ws.tradeStream
        .where((d) => (d['symbol'] as String? ?? '').toLowerCase() == wsSymbol)
        .listen((data) {
      final side = (data['side'] ?? '').toString().toUpperCase();
      final trade = SpotPublicTrade(
        price: (data['price'] as num?)?.toDouble() ?? 0,
        quantity: (data['quantity'] as num?)?.toDouble() ?? 0,
        isBuyerMaker: side == 'SELL',
        time: data['time'] as int? ?? 0,
        tradeId: data['id']?.toString() ?? '',
      );
      if (trade.price <= 0) return;
      final next = [trade, ...state.recentPublicTrades];
      if (next.length > 50) next.removeRange(50, next.length);
      state = state.copyWith(recentPublicTrades: next);
    });
  }

  static List<Map<String, dynamic>> _normalizeDepthMaps(dynamic raw) {
    if (raw is! List) return [];
    return raw
        .map((e) {
          if (e is Map<String, dynamic>) return e;
          if (e is Map) return Map<String, dynamic>.from(e);
          return <String, dynamic>{};
        })
        .where((m) => m.isNotEmpty)
        .toList();
  }

  void stopPolling() {}

  void resumePolling() {
    if (ref.read(isLoggedInProvider)) {
      Future.microtask(() async {
        try {
          await Future.wait([_loadWallets(), _loadCurrentOrders()]);
        } catch (_) {}
      });
    }
  }

  void _dispose() {
    final symbol = state.symbol
        .replaceAll('/', '')
        .replaceAll('-', '')
        .toLowerCase();
    try {
      final ws = ref.read(spotWsClientProvider);
      ws.unsubscribeTicker(symbol);
      ws.unsubscribeDepth(symbol);
      ws.unsubscribeTrade(symbol);
    } catch (_) {}
    _tickerSub?.cancel();
    _depthSub?.cancel();
    _tradeSub?.cancel();
    _releaseSpotUserPushChannels();
  }

  void setSide(SpotSide side) => state = state.copyWith(side: side);

  void setOrderType(SpotOrderType type) {
    state = state.copyWith(orderType: type, sliderPercent: 0);
  }

  void setAmountUnit(SpotAmountUnit unit) =>
      state = state.copyWith(amountUnit: unit);

  void setSliderPercent(double pct) =>
      state = state.copyWith(sliderPercent: pct.clamp(0.0, 1.0));

  void setActiveTab(SpotTab tab) {
    state = state.copyWith(activeTab: tab);
  }

  void toggleHideOtherSymbols() =>
      state = state.copyWith(hideOtherSymbols: !state.hideOtherSymbols);

  /// 下单，null 表示成功。
  Future<String?> placeOrder({
    SpotSide? side,
    SpotOrderType? type,
    double? price,
    required double amount,
  }) async {
    final actualSide = side ?? state.side;
    final actualType = type ?? state.orderType;

    if (amount <= 0) return 'errEnterAmount';
    if (actualType == SpotOrderType.limit && (price == null || price <= 0)) {
      return 'errEnterPrice';
    }

    // 客户端模拟"市价条件委托"：暂未支持
    if (actualType == SpotOrderType.conditionalMarket) {
      return 'errConditionalNotSupported';
    }

    try {
      await _service.placeOrder(
        symbol: state.apiSymbol,
        side: actualSide == SpotSide.buy ? 'BUY' : 'SELL',
        type: actualType == SpotOrderType.limit ? 1 : 2,
        price: actualType == SpotOrderType.limit ? (price ?? 0) : 0,
        volume: amount,
      );
      // 下单后立即刷新
      await Future.wait([_loadWallets(), _loadCurrentOrders()]);
      // 防止后端异步入账，2 秒后再刷一次
      Future.delayed(const Duration(seconds: 2), () {
        _loadWallets();
        _loadCurrentOrders();
      });
      return null;
    } catch (e) {
      return _errMsg(e);
    }
  }

  Future<String?> cancelOrder(SpotOrder order) async {
    if (order.id.isEmpty) return 'errInvalidOrderId';
    try {
      await _service.cancelOrder(int.tryParse(order.id) ?? 0);
      await _loadCurrentOrders();
      return null;
    } catch (e) {
      return _errMsg(e);
    }
  }

  Future<String?> cancelAll() async {
    final list = state.displayOrders.where((o) => o.isPending).toList();
    if (list.isEmpty) return 'errNoOrdersToCancel';
    String? lastErr;
    for (final o in list) {
      final err = await cancelOrder(o);
      if (err != null) lastErr = err;
    }
    return lastErr;
  }

  /// 划转；direction 1→现货 2→资金，null 成功。
  Future<String?> transfer({
    required String symbol,
    required double amount,
    required int direction,
  }) async {
    try {
      await _service.transfer(symbol: symbol, amount: amount, direction: direction);
      await _loadWallets();
      return null;
    } catch (e) {
      return _errMsg(e);
    }
  }

  Future<void> refresh() async {
    await Future.wait([_loadWallets(), _loadCurrentOrders()]);
    await _loadOrderBook(state.symbol);
  }

  ({double payload, double display})? prepareAmount({
    required SpotSide side,
    required SpotOrderType type,
    required double inputAmount,
    required SpotAmountUnit unit,
    double? price, // 限价时用户输入价格
  }) {
    if (inputAmount <= 0) return null;
    final volPre = state.volumePrecision;
    final factor = math.pow(10, volPre).toDouble();

    if (type == SpotOrderType.market || type == SpotOrderType.conditionalMarket) {
      if (side == SpotSide.buy) {
        // 市价买：按 USDT 金额下单
        return (payload: inputAmount, display: inputAmount);
      }
      // 市价卖：按 base 数量下单
      final base = (inputAmount * factor).floorToDouble() / factor;
      return (payload: base, display: base);
    }
    // 限价：amount 始终是 base 数量
    final p = price ?? 0;
    if (unit == SpotAmountUnit.base) {
      final v = (inputAmount * factor).floorToDouble() / factor;
      return (payload: v, display: v);
    }
    // quote → 换算为 base
    if (p <= 0) return null;
    final v = (inputAmount / p * factor).floorToDouble() / factor;
    return (payload: v, display: v);
  }

  String _errMsg(Object e) {
    if (e is ApiException) return e.message;
    if (e is DioException) {
      final inner = e.error;
      if (inner is ApiException) return inner.message;
      final data = e.response?.data;
      if (data is Map) {
        final msg = data['message'] as String? ?? data['msg'] as String?;
        if (msg != null && msg.isNotEmpty) return msg;
      }
      if (e.type == DioExceptionType.connectionTimeout ||
          e.type == DioExceptionType.receiveTimeout ||
          e.type == DioExceptionType.sendTimeout) {
        return 'errTimeout';
      }
      if (e.type == DioExceptionType.connectionError) {
        return 'errNetworkError';
      }
    }
    final s = e.toString();
    final m = RegExp(r'ApiException\(\d+\):\s*(.+?)[\r\n]?$', multiLine: true)
        .firstMatch(s);
    if (m != null) return m.group(1)!.trim();
    return s;
  }
}

final spotProvider =
    AutoDisposeNotifierProviderFamily<SpotNotifier, SpotState, String>(
  SpotNotifier.new,
);

final spotActiveSymbolProvider = StateProvider<String>((ref) => '');

double _toDouble(dynamic v) {
  if (v == null) return 0.0;
  if (v is num) return v.toDouble();
  return double.tryParse(v.toString()) ?? 0.0;
}

int? _toInt(dynamic v) {
  if (v == null) return null;
  if (v is num) return v.toInt();
  return int.tryParse(v.toString());
}

// ignore: unused_element
const _kDebugProvider = false;
// ignore: unused_element
void _dlog(String msg) {
  if (_kDebugProvider) debugPrint('[SpotProvider] $msg');
}
