import 'dart:async';

import 'package:flutter/foundation.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:k_chart_plus/k_chart_plus.dart';
import '../core/network/dio_client.dart';
import '../core/network/spot_ws_client.dart';
import '../data/models/market/kline_bar.dart';
import '../data/models/market/order_book_entry.dart';
import 'spot_ws_provider.dart';
import 'ws_provider.dart';

// ── K 线内存缓存（Stale-While-Revalidate）──────────────────────
// key: "f|s:<symbol>:<wsInterval>"，现货/合约分开缓存。
// 作用：再次打开同一行情页时立即渲染旧数据，后台静默刷新，消除 loading 闪烁。
final _klineBarCache = <String, List<KlineBar>>{};
const _kCacheMaxSize = 30;

/// 现货 WS `event: req` 拉历史 K 默认条数（与 web `useSpotMarketWs.sendKlineReq.pageSize` 一致）
const int _spotWsKlinePageSizeDefault = 300;

/// 行情详情 Provider 的 family 参数：区分现货 / 永续，避免共用合约 WS 与缓存。
@immutable
class MarketDetailKey {
  const MarketDetailKey({required this.symbol, required this.isFutures});
  final String symbol;
  final bool isFutures;

  @override
  bool operator ==(Object other) =>
      other is MarketDetailKey &&
      other.symbol == symbol &&
      other.isFutures == isFutures;

  @override
  int get hashCode => Object.hash(symbol, isFutures);
}

String _klineCacheKey(String symbol, KlinePeriod period, bool isFutures) =>
    '${isFutures ? 'f' : 's'}:$symbol:${period.wsInterval}';

/// WS / 归一化 Map 中的数值可能是 int/double/String，避免 `as num?` 丢数据或抛错。
double _looseDouble(dynamic v) {
  if (v == null) return 0.0;
  if (v is num) return v.toDouble();
  return double.tryParse(v.toString().trim().replaceAll(',', '')) ?? 0.0;
}

void _putCache(String key, List<KlineBar> bars) {
  if (bars.isEmpty) return;
  if (_klineBarCache.length >= _kCacheMaxSize && !_klineBarCache.containsKey(key)) {
    _klineBarCache.remove(_klineBarCache.keys.first); // 移除最早的条目
  }
  _klineBarCache[key] = bars;
}

// ── 币种扩展信息（/api/contract/coin-ext/detail）─────────────

String? _toNonEmpty(dynamic v) {
  if (v == null) return null;
  final s = v.toString().trim();
  return s.isEmpty ? null : s;
}

class CoinExtInfo {
  final int? id;
  final String symbol;
  final String nameCn;
  final String nameEn;
  final String? icon;
  final int? rank;
  final String? marketCap;         // 后端返回格式化字符串，如 "$1.76万亿"
  final String? circulatingSupply; // 后端返回格式化字符串，如 "1,976.43万 BTC"
  final double? issuePrice;
  final double? athPrice;
  final String? athDate;
  final String? whitepaper;

  const CoinExtInfo({
    this.id,
    required this.symbol,
    required this.nameCn,
    required this.nameEn,
    this.icon,
    this.rank,
    this.marketCap,
    this.circulatingSupply,
    this.issuePrice,
    this.athPrice,
    this.athDate,
    this.whitepaper,
  });

  factory CoinExtInfo.fromJson(Map<String, dynamic> json) {
    double? toDouble(dynamic v) {
      if (v == null) return null;
      if (v is num) return v.toDouble();
      final s = v.toString().trim();
      return s.isEmpty ? null : double.tryParse(s);
    }

    return CoinExtInfo(
      id: json['id'] as int?,
      symbol: json['symbol']?.toString() ?? '',
      nameCn: json['nameCn']?.toString() ?? json['name']?.toString() ?? '',
      nameEn: json['nameEn']?.toString() ?? '',
      icon: json['icon']?.toString(),
      rank: json['rank'] is int ? json['rank'] : int.tryParse('${json['rank'] ?? ''}'),
      marketCap: _toNonEmpty(json['marketCap']),
      circulatingSupply: _toNonEmpty(json['circulatingSupply']),
      issuePrice: toDouble(json['issuePrice']),
      athPrice: toDouble(json['athPrice']),
      athDate: json['athDate']?.toString(),
      whitepaper: json['whitepaper']?.toString(),
    );
  }
}


// ── 周期 & 指标枚举 ────────────────────────────────────────

enum KlinePeriod { min1, min5, min15, min30, hour1, hour4, day1, week1, month1 }

extension KlinePeriodLabel on KlinePeriod {
  String get label {
    switch (this) {
      case KlinePeriod.min1:   return '1分';
      case KlinePeriod.min5:   return '5分';
      case KlinePeriod.min15:  return '15分';
      case KlinePeriod.min30:  return '30分';
      case KlinePeriod.hour1:  return '1时';
      case KlinePeriod.hour4:  return '4时';
      case KlinePeriod.day1:   return '日线';
      case KlinePeriod.week1:  return '周线';
      case KlinePeriod.month1: return '月线';
    }
  }

  /// WS 订阅的 interval 字符串
  String get wsInterval {
    switch (this) {
      case KlinePeriod.min1:   return '1m';
      case KlinePeriod.min5:   return '5m';
      case KlinePeriod.min15:  return '15m';
      case KlinePeriod.min30:  return '30m';
      case KlinePeriod.hour1:  return '1h';
      case KlinePeriod.hour4:  return '4h';
      case KlinePeriod.day1:   return '1d';
      case KlinePeriod.week1:  return '1w';
      case KlinePeriod.month1: return '1mon';
    }
  }

  /// 现货行情 WS `market_{sym}_kline_{period}` 的 period 后缀（与 market 服务 SpotConstant / spot-websocket-api 一致）
  String get spotWsKlinePeriod {
    switch (this) {
      case KlinePeriod.min1:   return '1min';
      case KlinePeriod.min5:   return '5min';
      case KlinePeriod.min15:  return '15min';
      case KlinePeriod.min30:  return '30min';
      case KlinePeriod.hour1:  return '60min';
      case KlinePeriod.hour4:  return '4h';
      case KlinePeriod.day1:   return '1day';
      case KlinePeriod.week1:  return '1week';
      case KlinePeriod.month1: return '1month';
    }
  }
}

/// K 线订阅/请求用的 interval：合约用 Huobi 风格，现货用下划线 channel 后缀
String _klineChannelInterval(KlinePeriod period, bool isFutures) =>
    isFutures ? period.wsInterval : period.spotWsKlinePeriod;

enum ChartIndicator { ma, boll, vol, macd, rsi, kdj, wr }

extension ChartIndicatorLabel on ChartIndicator {
  String get label => name.toUpperCase();
}

// ── 市场概况数据 ───────────────────────────────────────────

class MarketStats {
  final double lastPrice;
  final String? lastPriceStr; // WS 返回的原始价格字符串
  final double markPrice;
  final double change24h;
  final double high24h;
  final double low24h;
  final double volume24h;
  final double turnover24h;

  const MarketStats({
    required this.lastPrice,
    this.lastPriceStr,
    required this.markPrice,
    required this.change24h,
    required this.high24h,
    required this.low24h,
    required this.volume24h,
    required this.turnover24h,
  });

  @override
  bool operator ==(Object other) =>
      identical(this, other) ||
      other is MarketStats &&
          lastPrice == other.lastPrice &&
          lastPriceStr == other.lastPriceStr &&
          markPrice == other.markPrice &&
          change24h == other.change24h &&
          high24h == other.high24h &&
          low24h == other.low24h &&
          volume24h == other.volume24h &&
          turnover24h == other.turnover24h;

  @override
  int get hashCode => Object.hash(
      lastPrice, lastPriceStr, markPrice, change24h, high24h, low24h, volume24h, turnover24h);
}

// ── 最新成交数据 ────────────────────────────────────────

class RecentTrade {
  final double price;
  final double quantity;
  final bool isBuyerMaker;
  final int time;
  final String tradeId;

  const RecentTrade({
    required this.price,
    required this.quantity,
    required this.isBuyerMaker,
    required this.time,
    this.tradeId = '',
  });

  @override
  bool operator ==(Object other) =>
      identical(this, other) ||
      other is RecentTrade &&
          price == other.price &&
          quantity == other.quantity &&
          isBuyerMaker == other.isBuyerMaker &&
          time == other.time &&
          tradeId == other.tradeId;

  @override
  int get hashCode => Object.hash(price, quantity, isBuyerMaker, time, tradeId);
}

// ── K 线加载类型 ──────────────────────────────────────────

enum KlineLoadType { initial, loadMore, realtimeAppend }

// ── UI State ──────────────────────────────────────────────

class MarketDetailState {
  final String symbol;
  final bool isLoading;
  final String? errorMessage;
  final MarketStats? stats;
  final List<KlineBar> klines;
  /// k_chart_plus 用的 KLineEntity 列表，与 klines 同步，指标已计算
  final List<KLineEntity> entities;
  final OrderBook? orderBook;
  final KlinePeriod period;
  final ChartIndicator indicator;
  final int topTab;
  final int bottomTab;
  final List<RecentTrade> trades;
  final KlineLoadType lastLoadType;
  final bool isLoadingMore;
  final CoinExtInfo? coinExt;

  const MarketDetailState({
    required this.symbol,
    this.isLoading = false,
    this.errorMessage,
    this.stats,
    this.klines = const [],
    this.entities = const [],
    this.orderBook,
    this.period = KlinePeriod.hour1,
    this.indicator = ChartIndicator.ma,
    this.topTab = 0,
    this.bottomTab = 0,
    this.trades = const [],
    this.lastLoadType = KlineLoadType.initial,
    this.isLoadingMore = false,
    this.coinExt,
  });

  MarketDetailState copyWith({
    bool? isLoading,
    String? errorMessage,
    MarketStats? stats,
    List<KlineBar>? klines,
    List<KLineEntity>? entities,
    OrderBook? orderBook,
    KlinePeriod? period,
    ChartIndicator? indicator,
    int? topTab,
    int? bottomTab,
    List<RecentTrade>? trades,
    KlineLoadType? lastLoadType,
    bool? isLoadingMore,
    CoinExtInfo? coinExt,
    bool clearCoinExt = false,
  }) {
    return MarketDetailState(
      symbol: symbol,
      isLoading: isLoading ?? this.isLoading,
      errorMessage: errorMessage,
      stats: stats ?? this.stats,
      klines: klines ?? this.klines,
      entities: entities ?? this.entities,
      orderBook: orderBook ?? this.orderBook,
      period: period ?? this.period,
      indicator: indicator ?? this.indicator,
      topTab: topTab ?? this.topTab,
      bottomTab: bottomTab ?? this.bottomTab,
      trades: trades ?? this.trades,
      lastLoadType: lastLoadType ?? this.lastLoadType,
      isLoadingMore: isLoadingMore ?? this.isLoadingMore,
      coinExt: clearCoinExt ? null : (coinExt ?? this.coinExt),
    );
  }
}

// ── Notifier ──────────────────────────────────────────────

class MarketDetailNotifier extends FamilyNotifier<MarketDetailState, MarketDetailKey> {
  StreamSubscription? _tickerSub;
  StreamSubscription? _markSub;
  StreamSubscription? _klineSub;
  StreamSubscription? _depthSub;
  StreamSubscription? _tradeSub;
  String? _currentKlineInterval;
  /// 与 [build] 传入的 [MarketDetailKey.isFutures] 同步，供异步方法使用
  bool _isFutures = true;

  // 节流：WS 高频更新延迟到下一个事件循环，防止 layout/semantics 重入
  Timer? _depthTimer;
  OrderBook? _pendingDepth;
  Timer? _tradeTimer;
  List<RecentTrade>? _pendingTrades;

  @override
  MarketDetailState build(MarketDetailKey key) {
    final symbol = key.symbol;
    final isFutures = key.isFutures;
    _isFutures = isFutures;

    ref.onDispose(() {
      _tickerSub?.cancel();
      _markSub?.cancel();
      _klineSub?.cancel();
      _depthSub?.cancel();
      _tradeSub?.cancel();
      _depthTimer?.cancel();
      _tradeTimer?.cancel();
      if (isFutures) {
        final ws = ref.read(wsClientProvider);
        ws.unsubscribeTicker(symbol);
        ws.unsubscribeMark(symbol);
        ws.unsubscribeDepth(symbol);
        ws.unsubscribeTrade(symbol);
        if (_currentKlineInterval != null) {
          ws.unsubscribeKline(symbol, _currentKlineInterval!);
        }
      } else {
        final sw = ref.read(spotWsClientProvider);
        sw.unsubscribeTicker(symbol);
        // 深度/成交由 [spotProvider] 订阅，此处只取消本 Notifier 使用的 ticker/K 线
        if (_currentKlineInterval != null) {
          sw.unsubscribeKline(symbol, _currentKlineInterval!);
        }
      }
    });

    if (isFutures) {
      ref.listen<AsyncValue<WsConnectionState>>(
        wsConnectionStateProvider,
        (prev, next) {
          final prevState = prev?.valueOrNull;
          final nextState = next.valueOrNull;
          if (nextState == WsConnectionState.connected &&
              prevState != WsConnectionState.connected) {
            _reloadKlines(symbol, state.period, isFutures: true);
          }
        },
      );
    } else {
      ref.listen<AsyncValue<SpotWsState>>(
        spotWsConnectionStateProvider,
        (prev, next) {
          final prevState = prev?.valueOrNull;
          final nextState = next.valueOrNull;
          if (nextState == SpotWsState.connected &&
              prevState != SpotWsState.connected) {
            _reloadKlines(symbol, state.period, isFutures: false);
          }
        },
      );
    }

    Future.microtask(() => _loadData(symbol, isFutures: isFutures));
    return MarketDetailState(symbol: symbol, isLoading: true);
  }

  // ── KlineBar ↔ KLineEntity 转换 ──────────────────────

  static KLineEntity _toEntity(KlineBar bar) => KLineEntity.fromCustom(
    open: bar.open,
    close: bar.close,
    high: bar.high,
    low: bar.low,
    vol: bar.volume,
    time: bar.time.millisecondsSinceEpoch,
  );

  /// 统一入口：同步 klines + entities + 指标计算
  /// 任何修改 klines 的地方都必须走这里，防止数据不一致
  void _setKlines(List<KlineBar> newKlines, KlineLoadType type) {
    final entities = newKlines.map(_toEntity).toList();
    // 全量计算所有指标：MA 计算依赖前序数据，局部窗口计算会导致早期 MA 值偏差
    if (entities.isNotEmpty) DataUtil.calculate(entities, [5, 10, 20]);

    state = state.copyWith(
      klines: newKlines,
      entities: entities,
      lastLoadType: type,
    );
  }

  // ── 数据加载 ──────────────────────────────────────────

  Future<void> _loadData(String symbol, {required bool isFutures}) async {
    final period = state.period;
    final interval = _klineChannelInterval(period, isFutures);
    final cacheKey = _klineCacheKey(symbol, period, isFutures);
    final cached = _klineBarCache[cacheKey];

    // ── 有缓存：立即渲染，无需 loading，WS 订阅先启动 ──────────
    if (cached != null && cached.isNotEmpty) {
      final price = cached.last.close;
      final prev = state.stats;
      state = state.copyWith(
        isLoading: false,
        stats: MarketStats(
          lastPrice: price,
          lastPriceStr: prev?.lastPriceStr,
          markPrice: prev?.markPrice ?? 0,
          change24h: prev?.change24h ?? 0,
          high24h: prev?.high24h ?? 0,
          low24h: prev?.low24h ?? 0,
          volume24h: prev?.volume24h ?? 0,
          turnover24h: prev?.turnover24h ?? 0,
        ),
      );
      _setKlines(cached, KlineLoadType.initial);
      _subscribeMarketStreams(symbol, interval, isFutures: isFutures);
    }

    // 现货须先订阅 kline channel，再 WS `event: req`（与 web `useSpotMarketWs` 一致）
    if (!isFutures && (cached == null || cached.isEmpty)) {
      _subscribeMarketStreams(symbol, interval, isFutures: false);
    }

    // ── 后台请求最新 K 线历史 ─────────────────────────────────
    final List<Map<String, dynamic>> historyData;
    if (isFutures) {
      final ws = ref.read(wsClientProvider);
      final now = DateTime.now().millisecondsSinceEpoch;
      final from = now - _periodDurationMs(period);
      historyData = await ws.requestKlineHistory(
        symbol: symbol,
        interval: interval,
        from: from,
        to: now,
      );
    } else {
      historyData = await _fetchSpotKlineHistoryRaw(
        symbol,
        period,
        pageSize: _spotWsKlinePageSizeDefault,
      );
    }

    final klines = historyData.isNotEmpty
        ? _parseKlineHistory(historyData, period)
        : (cached ?? <KlineBar>[]);

    _putCache(cacheKey, klines);

    final price = klines.isNotEmpty ? klines.last.close : 0.0;
    state = state.copyWith(
      isLoading: false,
      stats: MarketStats(
        lastPrice: price,
        lastPriceStr: state.stats?.lastPriceStr,
        markPrice: state.stats?.markPrice ?? 0,
        change24h: state.stats?.change24h ?? 0,
        high24h: state.stats?.high24h ?? 0,
        low24h: state.stats?.low24h ?? 0,
        volume24h: state.stats?.volume24h ?? 0,
        turnover24h: state.stats?.turnover24h ?? 0,
      ),
    );
    _setKlines(klines, KlineLoadType.initial);

    if (cached == null || cached.isEmpty) {
      if (isFutures) {
        _subscribeMarketStreams(symbol, interval, isFutures: true);
      }
      // 现货已在请求历史之前完成订阅；合约仍在此处一次性订阅。
    } else {
      _subscribeWsKline(symbol, interval, isFutures: isFutures);
    }
  }

  void _subscribeMarketStreams(String symbol, String interval,
      {required bool isFutures}) {
    if (isFutures) {
      _subscribeWsTicker(symbol);
      _subscribeWsMark(symbol);
      _subscribeWsDepth(symbol);
      _subscribeWsTrade(symbol);
    } else {
      _subscribeSpotTicker(symbol);
    }
    _subscribeWsKline(symbol, interval, isFutures: isFutures);
  }

  /// 等待 Spot WS 建连，`event: req` 须在 connected 状态下发送。
  Future<void> _ensureSpotWsConnected(SpotWsClient sw) async {
    if (sw.currentState == SpotWsState.connected) {
      return;
    }
    final deadline = DateTime.now().add(const Duration(seconds: 15));
    while (DateTime.now().isBefore(deadline)) {
      await Future<void>.delayed(const Duration(milliseconds: 100));
      if (sw.currentState == SpotWsState.connected) {
        return;
      }
    }
  }

  static int _klineOpenTimeMs(Map<String, dynamic> item) {
    final raw = item['beginTime'] ?? item['time'] ?? item['t'] ?? item['id'];
    int t = 0;
    if (raw is int) {
      t = raw;
    } else if (raw is num) {
      t = raw.toInt();
    } else {
      t = int.tryParse('$raw') ?? 0;
    }
    if (t <= 0) {
      return 0;
    }
    if (t < 10000000000) {
      t *= 1000;
    }
    return t;
  }

  /// 服务端返回顺序不保证稳定，先按开盘时间排序再交给 [_parseKlineHistory]。
  static void _sortSpotKlineWsRowsAscending(List<Map<String, dynamic>> rows) {
    rows.sort((a, b) => _klineOpenTimeMs(a).compareTo(_klineOpenTimeMs(b)));
  }

  /// 现货历史 K：通过现货 WS `event: req` 拉取（与 web `useSpotMarketWs` 同源）。
  /// [endIdxSec]：`SpotWsClient.requestKlineHistory.endIdx`，单位秒，`0` 表示最新一页。
  Future<List<Map<String, dynamic>>> _fetchSpotKlineHistoryRaw(
    String symbol,
    KlinePeriod period, {
    int pageSize = _spotWsKlinePageSizeDefault,
    int endIdxSec = 0,
  }) async {
    final sw = ref.read(spotWsClientProvider);
    await _ensureSpotWsConnected(sw);
    if (sw.currentState != SpotWsState.connected) {
      assert(() {
        debugPrint('[MarketDetail] spot klines req skipped: WS not connected');
        return true;
      }());
      return [];
    }
    try {
      final rows = await sw.requestKlineHistory(
        symbol: symbol,
        interval: period.spotWsKlinePeriod,
        pageSize: pageSize.clamp(1, 1000),
        endIdx: endIdxSec < 0 ? 0 : endIdxSec,
      );
      _sortSpotKlineWsRowsAscending(rows);
      return rows;
    } catch (e, st) {
      assert(() {
        debugPrint('[MarketDetail] Spot WS spot klines: $e\n$st');
        return true;
      }());
      return [];
    }
  }

  /// 解析 K 线历史数据，并自动修复时间戳。
  /// 部分交易所（如本项目）对月线/周线返回服务器处理时间而非 K 线开盘时间，
  /// 导致所有 bar 的时间戳集中在同一分钟内，无法正常显示时间轴。
  /// 修复方案：当实际时间跨度 < 期望时间跨度的 10% 时，按周期均匀重建时间戳。
  /// [anchorEndMs] 仅在 loadMore 时传入，作为重建时间的右锚点；
  /// 初始加载时传 null，使用当前时间作为锚点。
  static List<KlineBar> _parseKlineHistory(
    List<Map<String, dynamic>> data,
    KlinePeriod period, {
    int? anchorEndMs,
  }) {
    if (data.isEmpty) return [];

    // 提取 K 线时间（毫秒）。现货 history 用 `id` 表示开盘时间（秒）；合约多为毫秒 beginTime
    final times = data.map((item) {
      final raw = item['beginTime'] ?? item['time'] ?? item['t'] ?? item['id'];
      int t;
      if (raw is int) {
        t = raw;
      } else {
        t = int.tryParse('$raw') ?? 0;
      }
      if (t <= 0) return 0;
      if (t < 10000000000) return t * 1000; // 秒 → 毫秒
      return t;
    }).toList();

    final actualSpan = (times.last - times.first).abs();
    final stepMs = _periodStepMs(period);
    // 实际跨度 < 期望跨度 10% → 服务端时间不可信，需重建
    final expectedMinSpan = (data.length - 1) * stepMs * 0.1;
    final needRebuild = data.length > 1 && actualSpan < expectedMinSpan;

    List<int> finalTimes;
    if (needRebuild) {
      // 以锚点为最后一根 K 线时间，往前均匀分布
      final anchor = anchorEndMs ?? DateTime.now().millisecondsSinceEpoch;
      finalTimes = List.generate(
        data.length,
        (i) => anchor - (data.length - 1 - i) * stepMs,
      );
    } else {
      finalTimes = times;
    }

    return data.asMap().entries.map((entry) {
      final item = entry.value;
      return KlineBar(
        time: DateTime.fromMillisecondsSinceEpoch(finalTimes[entry.key]),
        open: double.tryParse('${item['open']}') ?? 0,
        close: double.tryParse('${item['close']}') ?? 0,
        high: double.tryParse('${item['high']}') ?? 0,
        low: double.tryParse('${item['low']}') ?? 0,
        volume: double.tryParse('${item['volume'] ?? item['vol']}') ?? 0,
      );
    }).toList();
  }

  /// 单根 K 线的毫秒步长（用于时间戳修复）
  static int _periodStepMs(KlinePeriod period) {
    switch (period) {
      case KlinePeriod.min1:   return 60 * 1000;
      case KlinePeriod.min5:   return 5 * 60 * 1000;
      case KlinePeriod.min15:  return 15 * 60 * 1000;
      case KlinePeriod.min30:  return 30 * 60 * 1000;
      case KlinePeriod.hour1:  return 60 * 60 * 1000;
      case KlinePeriod.hour4:  return 4 * 60 * 60 * 1000;
      case KlinePeriod.day1:   return 24 * 60 * 60 * 1000;
      case KlinePeriod.week1:  return 7 * 24 * 60 * 60 * 1000;
      case KlinePeriod.month1: return 30 * 24 * 60 * 60 * 1000;
    }
  }

  static int _periodDurationMs(KlinePeriod period) {
    // 初始加载约 100 根 K 线
    const bars = 100;
    switch (period) {
      case KlinePeriod.min1:   return bars * 60 * 1000;
      case KlinePeriod.min5:   return bars * 5 * 60 * 1000;
      case KlinePeriod.min15:  return bars * 15 * 60 * 1000;
      case KlinePeriod.min30:  return bars * 30 * 60 * 1000;
      case KlinePeriod.hour1:  return bars * 60 * 60 * 1000;
      case KlinePeriod.hour4:  return bars * 4 * 60 * 60 * 1000;
      case KlinePeriod.day1:   return bars * 24 * 60 * 60 * 1000;
      case KlinePeriod.week1:  return bars * 7 * 24 * 60 * 60 * 1000;
      case KlinePeriod.month1: return bars * 30 * 24 * 60 * 60 * 1000;
    }
  }

  /// 将时间对齐到所在周期的起始时刻
  static DateTime _alignToPeriodStart(DateTime t, KlinePeriod period) {
    switch (period) {
      case KlinePeriod.min1:
        return DateTime(t.year, t.month, t.day, t.hour, t.minute);
      case KlinePeriod.min5:
        return DateTime(t.year, t.month, t.day, t.hour, (t.minute ~/ 5) * 5);
      case KlinePeriod.min15:
        return DateTime(t.year, t.month, t.day, t.hour, (t.minute ~/ 15) * 15);
      case KlinePeriod.min30:
        return DateTime(t.year, t.month, t.day, t.hour, (t.minute ~/ 30) * 30);
      case KlinePeriod.hour1:
        return DateTime(t.year, t.month, t.day, t.hour);
      case KlinePeriod.hour4:
        return DateTime(t.year, t.month, t.day, (t.hour ~/ 4) * 4);
      case KlinePeriod.day1:
        return DateTime(t.year, t.month, t.day);
      case KlinePeriod.week1:
        // 以周一为起始
        return DateTime(t.year, t.month, t.day - (t.weekday - 1));
      case KlinePeriod.month1:
        return DateTime(t.year, t.month, 1);
    }
  }

  /// 给定某周期起始时间，返回下一个周期的起始时间
  static DateTime _nextPeriodStart(DateTime periodStart, KlinePeriod period) {
    switch (period) {
      case KlinePeriod.min1:
        return periodStart.add(const Duration(minutes: 1));
      case KlinePeriod.min5:
        return periodStart.add(const Duration(minutes: 5));
      case KlinePeriod.min15:
        return periodStart.add(const Duration(minutes: 15));
      case KlinePeriod.min30:
        return periodStart.add(const Duration(minutes: 30));
      case KlinePeriod.hour1:
        return periodStart.add(const Duration(hours: 1));
      case KlinePeriod.hour4:
        return periodStart.add(const Duration(hours: 4));
      case KlinePeriod.day1:
        return periodStart.add(const Duration(days: 1));
      case KlinePeriod.week1:
        return periodStart.add(const Duration(days: 7));
      case KlinePeriod.month1:
        // DateTime 自动处理跨年：DateTime(2025, 13, 1) → 2026-01-01
        return DateTime(periodStart.year, periodStart.month + 1, 1);
    }
  }

  static int _loadMoreRangeMs(KlinePeriod period) {
    const count = 200;
    switch (period) {
      case KlinePeriod.min1:   return count * 60 * 1000;
      case KlinePeriod.min5:   return count * 5 * 60 * 1000;
      case KlinePeriod.min15:  return count * 15 * 60 * 1000;
      case KlinePeriod.min30:  return count * 30 * 60 * 1000;
      case KlinePeriod.hour1:  return count * 60 * 60 * 1000;
      case KlinePeriod.hour4:  return count * 4 * 60 * 60 * 1000;
      case KlinePeriod.day1:   return count * 24 * 60 * 60 * 1000;
      case KlinePeriod.week1:  return count * 7 * 24 * 60 * 60 * 1000;
      case KlinePeriod.month1: return count * 30 * 24 * 60 * 60 * 1000;
    }
  }

  /// 加载更早的 K线历史（向左滑动触发）
  Future<void> loadMoreKlines() async {
    if (state.isLoadingMore || state.klines.isEmpty) return;
    state = state.copyWith(isLoadingMore: true);

    final interval = _klineChannelInterval(state.period, _isFutures);
    final earliest = state.klines.first.time.millisecondsSinceEpoch;

    final List<Map<String, dynamic>> historyData;
    if (_isFutures) {
      final ws = ref.read(wsClientProvider);
      final to = earliest - 1;
      final from = to - _loadMoreRangeMs(state.period);
      historyData = await ws.requestKlineHistory(
        symbol: state.symbol,
        interval: interval,
        from: from,
        to: to,
      );
    } else {
      final anchorMs = earliest - 1;
      historyData = await _fetchSpotKlineHistoryRaw(
        state.symbol,
        state.period,
        pageSize: 200,
        endIdxSec: anchorMs < 0 ? 0 : anchorMs ~/ 1000,
      );
    }

    if (historyData.isEmpty) {
      state = state.copyWith(isLoadingMore: false);
      return;
    }

    final older = _parseKlineHistory(
      historyData,
      state.period,
      anchorEndMs: earliest - 1,
    );
    final existingTimes = state.klines.map((k) => k.time.millisecondsSinceEpoch).toSet();
    final dedupedOlder = older.where((k) => !existingTimes.contains(k.time.millisecondsSinceEpoch)).toList();
    final merged = [...dedupedOlder, ...state.klines];

    state = state.copyWith(isLoadingMore: false);
    _setKlines(merged, KlineLoadType.loadMore);
  }

  // ── WS 订阅 ────────────────────────────────────────────

  static String _normSym(String s) =>
      s.replaceAll('/', '').replaceAll('-', '').toUpperCase();

  void _subscribeWsTicker(String symbol) {
    final ws = ref.read(wsClientProvider);
    ws.subscribeTicker(symbol);

    _tickerSub?.cancel();
    final want = _normSym(symbol);
    _tickerSub = ws.tickerStream
        .where((data) => _normSym('${data['symbol'] ?? ''}') == want)
        .listen((data) {
      final price = (data['price'] as num?)?.toDouble();
      if (price == null) return;
      final priceStr = data['priceStr'] as String? ?? '';

      final change24h = (data['change24h'] as num?)?.toDouble() ?? 0;
      final high24h = (data['high24h'] as num?)?.toDouble() ?? 0;
      final low24h = (data['low24h'] as num?)?.toDouble() ?? 0;
      final turnover = (data['volume24h'] as num?)?.toDouble() ?? 0;
      final volumeCoin = (data['volumeCoin'] as num?)?.toDouble() ?? 0;
      final oldStats = state.stats;
      if (oldStats == null) return;

      state = state.copyWith(
        stats: MarketStats(
          lastPrice: price,
          lastPriceStr: priceStr.isNotEmpty ? priceStr : null,
          markPrice: oldStats.markPrice,
          change24h: change24h,
          high24h: high24h > 0 ? high24h : oldStats.high24h,
          low24h: low24h > 0 ? low24h : oldStats.low24h,
          volume24h: volumeCoin > 0 ? volumeCoin : oldStats.volume24h,
          turnover24h: turnover > 0 ? turnover : oldStats.turnover24h,
        ),
      );
    });
  }

  /// 现货 ticker → 与合约统一的 [MarketStats] 字段
  void _subscribeSpotTicker(String symbol) {
    final sw = ref.read(spotWsClientProvider);
    sw.subscribeTicker(symbol);

    _tickerSub?.cancel();
    final want = _normSym(symbol);
    _tickerSub = sw.tickerStream
        .where((data) => _normSym('${data['symbol'] ?? ''}') == want)
        .listen((data) {
      final price = _looseDouble(data['price']);
      if (price <= 0) return;
      final priceStr = data['priceStr'] as String? ?? '';

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

      final change24h = pct(data['change24h']);
      final high24h = _looseDouble(data['high']);
      final low24h = _looseDouble(data['low']);
      final turnover = _looseDouble(data['turnover']);
      final volumeCoin = _looseDouble(data['volume']);
      final oldStats = state.stats;
      if (oldStats == null) return;

      state = state.copyWith(
        stats: MarketStats(
          lastPrice: price,
          lastPriceStr: priceStr.isNotEmpty ? priceStr : null,
          markPrice: oldStats.markPrice,
          change24h: change24h ?? oldStats.change24h,
          high24h: high24h > 0 ? high24h : oldStats.high24h,
          low24h: low24h > 0 ? low24h : oldStats.low24h,
          volume24h: volumeCoin > 0 ? volumeCoin : oldStats.volume24h,
          turnover24h: turnover > 0 ? turnover : oldStats.turnover24h,
        ),
      );
    });
  }

  void _subscribeWsMark(String symbol) {
    final ws = ref.read(wsClientProvider);
    ws.subscribeMark(symbol);

    _markSub?.cancel();
    _markSub = ws.markStream
        .where((data) => (data['symbol'] as String? ?? '').toUpperCase() == symbol.toUpperCase())
        .listen((data) {
      final markPrice = (data['markPrice'] as num?)?.toDouble() ?? 0;
      final oldStats = state.stats;
      if (oldStats == null || markPrice <= 0) return;
      state = state.copyWith(
        stats: MarketStats(
          lastPrice: oldStats.lastPrice,
          lastPriceStr: oldStats.lastPriceStr, // 保留 ticker 推送的原始字符串
          markPrice: markPrice,
          change24h: oldStats.change24h,
          high24h: oldStats.high24h,
          low24h: oldStats.low24h,
          volume24h: oldStats.volume24h,
          turnover24h: oldStats.turnover24h,
        ),
      );
    });
  }

  void _subscribeWsDepth(String symbol) {
    final ws = ref.read(wsClientProvider);
    ws.subscribeDepth(symbol);

    _depthSub?.cancel();
    final want = _normSym(symbol);
    _depthSub = ws.orderBookStream
        .where((d) => _normSym('${d['symbol'] ?? ''}') == want)
        .listen((data) {
      final rawBids = data['bids'] as List<dynamic>? ?? [];
      final rawAsks = data['asks'] as List<dynamic>? ?? [];

      double cumTotal = 0;
      List<OrderBookEntry> parse(List<dynamic> raw) {
        cumTotal = 0;
        return raw.map((e) {
          final m = e as Map;
          final amt = (m['quantity'] as num?)?.toDouble() ?? 0;
          cumTotal += amt;
          return OrderBookEntry(price: (m['price'] as num?)?.toDouble() ?? 0, amount: amt, total: cumTotal);
        }).toList();
      }

      _pendingDepth = OrderBook(asks: parse(rawAsks), bids: parse(rawBids));
      if (_depthTimer == null || !_depthTimer!.isActive) {
        _depthTimer = Timer(Duration.zero, () {
          final pending = _pendingDepth;
          _pendingDepth = null;
          if (pending != null) state = state.copyWith(orderBook: pending);
        });
      }
    });
  }

  void _subscribeWsTrade(String symbol) {
    final ws = ref.read(wsClientProvider);
    ws.subscribeTrade(symbol);

    _tradeSub?.cancel();
    final want = _normSym(symbol);
    _tradeSub = ws.tradeStream
        .where((d) => _normSym('${d['symbol'] ?? ''}') == want)
        .listen((data) {
      final trade = RecentTrade(
        price: (data['price'] as num?)?.toDouble() ?? 0,
        quantity: (data['quantity'] as num?)?.toDouble() ?? 0,
        isBuyerMaker: data['isBuyerMaker'] == true,
        time: data['time'] as int? ?? 0,
        tradeId: data['tradeId']?.toString() ?? '',
      );

      final trades = [trade, ...(state.trades)];
      if (trades.length > 50) trades.removeRange(50, trades.length);
      _pendingTrades = trades;
      if (_tradeTimer == null || !_tradeTimer!.isActive) {
        _tradeTimer = Timer(Duration.zero, () {
          final pending = _pendingTrades;
          _pendingTrades = null;
          if (pending != null) state = state.copyWith(trades: pending);
        });
      }
    });
  }

  void _subscribeWsKline(String symbol, String interval,
      {required bool isFutures}) {
    if (isFutures) {
      final ws = ref.read(wsClientProvider);

      if (_currentKlineInterval != null && _currentKlineInterval != interval) {
        ws.unsubscribeKline(symbol, _currentKlineInterval!);
      }
      _currentKlineInterval = interval;
      ws.subscribeKline(symbol, interval);

      _klineSub?.cancel();
      final want = _normSym(symbol);
      _klineSub = ws.klineStream
          .where((d) =>
              _normSym('${d['symbol'] ?? ''}') == want &&
              d['interval'] == interval)
          .listen((data) {
        _onKlineTick(data, interval);
      });
    } else {
      final sw = ref.read(spotWsClientProvider);

      if (_currentKlineInterval != null && _currentKlineInterval != interval) {
        sw.unsubscribeKline(symbol, _currentKlineInterval!);
      }
      _currentKlineInterval = interval;
      sw.subscribeKline(symbol, interval);

      _klineSub?.cancel();
      final want = _normSym(symbol);
      _klineSub = sw.klineStream
          .where((d) =>
              _normSym('${d['symbol'] ?? ''}') == want &&
              '${d['interval']}' == interval)
          .listen((data) {
        _onKlineTick(data, interval);
      });
    }
  }

  static double _dynToDouble(dynamic v) {
    if (v == null) return 0;
    if (v is num) return v.toDouble();
    return double.tryParse(v.toString()) ?? 0;
  }

  void _onKlineTick(Map<String, dynamic> data, String interval) {
    final rawT = data['time'] as int? ?? 0;
    if (rawT == 0) return;
    final time = DateTime.fromMillisecondsSinceEpoch(
      rawT < 10000000000 ? rawT * 1000 : rawT,
    );
    final volRaw = data['volume'] ?? data['vol'];
    final bar = KlineBar(
      time: time,
      open: _dynToDouble(data['open']),
      close: _dynToDouble(data['close']),
      high: _dynToDouble(data['high']),
      low: _dynToDouble(data['low']),
      volume: _dynToDouble(volRaw),
    );

    final klines = [...state.klines];
    if (klines.isNotEmpty) {
      final period = state.period;
      final alignedBar = _alignToPeriodStart(bar.time, period);
      final alignedLast = _alignToPeriodStart(klines.last.time, period);

      if (alignedLast == alignedBar) {
        klines[klines.length - 1] = KlineBar(
          time: klines.last.time,
          open: bar.open,
          high: bar.high,
          low: bar.low,
          close: bar.close,
          volume: bar.volume,
        );
      } else if (_nextPeriodStart(alignedLast, period) == alignedBar) {
        klines.add(KlineBar(
          time: alignedBar,
          open: bar.open,
          high: bar.high,
          low: bar.low,
          close: bar.close,
          volume: bar.volume,
        ));
      }
    } else {
      klines.add(bar);
    }

    _setKlines(klines, KlineLoadType.realtimeAppend);

    final oldStats = state.stats;
    if (oldStats != null && bar.close > 0) {
      state = state.copyWith(
        stats: MarketStats(
          lastPrice: bar.close,
          lastPriceStr: oldStats.lastPriceStr, // 保留 ticker 推送的原始字符串
          markPrice: oldStats.markPrice,
          change24h: oldStats.change24h,
          high24h: oldStats.high24h,
          low24h: oldStats.low24h,
          volume24h: oldStats.volume24h,
          turnover24h: oldStats.turnover24h,
        ),
      );
    }
  }

  // ── UI 操作 ────────────────────────────────────────────

  void setTopTab(int tab) {
    state = state.copyWith(topTab: tab);
    if (tab == 1) {
      // 每次切入概览 tab 都重新请求，保证数据最新
      _loadCoinExt(state.symbol);
    }
  }

  Future<void> _loadCoinExt(String symbol) async {
    // 提取基础币名（BTCUSDT → BTC）
    final baseAsset = symbol.toUpperCase().replaceAll('USDT', '').replaceAll('PERP', '');
    try {
      final dio = ref.read(dioClientProvider);
      final resp = await dio.get(
        'contract/coin-ext/detail',
        queryParameters: {'symbol': baseAsset},
      );
      final data = resp.data;
      Map<String, dynamic>? json;
      if (data is Map) {
        final body = data['data'] ?? data['result'] ?? data;
        if (body is Map<String, dynamic>) {
          json = body;
        }
      }
      if (json != null) {
        state = state.copyWith(coinExt: CoinExtInfo.fromJson(json));
      }
    } catch (_) {
      // 静默失败，UI 降级到默认显示
    }
  }

  void setPeriod(KlinePeriod period) {
    if (period == state.period) return;
    final cacheKey = _klineCacheKey(state.symbol, period, _isFutures);
    final cached = _klineBarCache[cacheKey];
    if (cached != null && cached.isNotEmpty) {
      // 有缓存：立即切换显示，后台刷新
      state = state.copyWith(period: period);
      _setKlines(cached, KlineLoadType.initial);
    } else {
      // 无缓存：清空旧数据，等待加载
      state = state.copyWith(period: period, klines: [], entities: []);
    }
    _reloadKlines(state.symbol, period, isFutures: _isFutures);
  }

  Future<void> _reloadKlines(String symbol, KlinePeriod period,
      {required bool isFutures}) async {
    final interval = _klineChannelInterval(period, isFutures);

    if (!isFutures) {
      _subscribeWsKline(symbol, interval, isFutures: false);
    }

    final List<Map<String, dynamic>> historyData;
    if (isFutures) {
      final ws = ref.read(wsClientProvider);
      final now = DateTime.now().millisecondsSinceEpoch;
      final from = now - _periodDurationMs(period);
      historyData = await ws.requestKlineHistory(
        symbol: symbol,
        interval: interval,
        from: from,
        to: now,
      );
    } else {
      historyData = await _fetchSpotKlineHistoryRaw(
        symbol,
        period,
        pageSize: _spotWsKlinePageSizeDefault,
      );
    }

    final cacheKey = _klineCacheKey(symbol, period, isFutures);
    final klines = historyData.isNotEmpty
        ? _parseKlineHistory(historyData, period)
        : (_klineBarCache[cacheKey] ?? <KlineBar>[]);

    _putCache(cacheKey, klines);
    _setKlines(klines, KlineLoadType.initial);
    if (isFutures) {
      _subscribeWsKline(symbol, interval, isFutures: true);
    }
  }

  void setIndicator(ChartIndicator indicator) => state = state.copyWith(indicator: indicator);
  void setBottomTab(int tab) => state = state.copyWith(bottomTab: tab);
}

final marketDetailProvider =
    NotifierProviderFamily<MarketDetailNotifier, MarketDetailState, MarketDetailKey>(
  MarketDetailNotifier.new,
);
