package com.market.service.impl;

import com.baomidou.dynamic.datasource.annotation.DS;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.market.entity.Kline;
import com.market.mapper.FuturesKlineMapper;
import com.market.mapper.KlineTableMapper;
import com.market.model.CoinThumb;
import com.market.service.KlineService;
import com.market.utils.KlineUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.dao.DuplicateKeyException;
import org.springframework.stereotype.Service;

import java.math.BigDecimal;
import java.util.List;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;

import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.data.redis.core.RedisTemplate;

/** 同一根线写库间隔：5 秒 */
@Slf4j
@DS("market")
@Service
public class KlineServiceImpl implements KlineService {

    private static final long DB_FLUSH_INTERVAL_MS = 5_000L;

    @Autowired
    private KlineTableMapper klineTableMapper;

    @Autowired
    private FuturesKlineMapper futuresKlineMapper;

    @Autowired
    private KlineUtils klineUtils;

    @Autowired
    @Qualifier("jdkRedisTemplate")
    private RedisTemplate<String, Object> redisTemplate;

    /** NEW_COIN_PRICE Hash key（field=contractId, value=price） */
    private static final String REDIS_PRICE_KEY = "NEW_COIN_PRICE:";

    /** market-contract Redis pub/sub 频道，contract-swap-api 订阅该频道更新引擎价格 */
    private static final String REDIS_CHANNEL_CONTRACT = "market-contract";

    private final ObjectMapper objectMapper = new ObjectMapper();

    /** 已存在的分表名集合，首次调用时从 DB 加载 */
    private final Set<String> existingTables = ConcurrentHashMap.newKeySet();
    private volatile boolean tableInitialized = false;

    // ------------------------------------------------------------------ ensureTable

    @Override
    public void ensureTable(String symbol) {
        initTablesIfNeeded();
        String tableName = KlineUtils.tableName(symbol);
        if (existingTables.contains(tableName)) return;
        klineTableMapper.createTableLike(tableName);
        existingTables.add(tableName);
        log.info("创建 kline 分表: {}", tableName);
    }

    private void initTablesIfNeeded() {
        if (tableInitialized) return;
        synchronized (this) {
            if (tableInitialized) return;
            List<String> tables = klineTableMapper.listKlineTables();
            existingTables.addAll(tables);
            tableInitialized = true;
            log.info("已加载 {} 个 kline 分表", tables.size());
        }
    }

    // ------------------------------------------------------------------ processKline

    @Override
    public void processKline(String symbol, String interval, JsonNode k) {
        String tableName = KlineUtils.tableName(symbol);
        String cacheKey = symbol + KlineUtils.CACHE_KEY_SEP + interval;

        Kline incoming = toEntity(interval, k);
        Kline cached = klineUtils.KLINE_CACHE.getIfPresent(cacheKey);

        if (cached == null) {
            // 内存无缓存（首次或重启）：通过 beginTime + period 判断新增还是修改
            insertOrUpdate(tableName, incoming);
            klineUtils.KLINE_CACHE.put(cacheKey, incoming);
            return;
        }

        if (incoming.getBeginTime().equals(cached.getBeginTime())) {
            // 同一根线：每 5 秒写库一次，其余只更新内存
            klineUtils.KLINE_CACHE.put(cacheKey, incoming);
            long now = System.currentTimeMillis();
            Long last = klineUtils.LAST_FLUSH_TIME.getIfPresent(cacheKey);
            if (last == null) {
                klineUtils.LAST_FLUSH_TIME.put(cacheKey, now);
                futuresKlineMapper.update(tableName, incoming);
            }
            return;
        }

        // 1. 将新线开盘价赋给上一根线的收盘价，修改 DB
        cached.setClose(incoming.getOpen());
        futuresKlineMapper.update(tableName, cached);

        // 2. 新增新线到 DB（唯一键冲突 = 多节点并发写入，内存已更新，跳过）
        try {
            futuresKlineMapper.insert(tableName, incoming);
        } catch (DuplicateKeyException dup) {
            log.debug("合约 K 线多节点重复写入跳过: symbol={}, interval={}, beginTime={}",
                    symbol, interval, incoming.getBeginTime());
        }

        // 3. 替换内存
        klineUtils.KLINE_CACHE.put(cacheKey, incoming);
    }

    private void insertOrUpdate(String tableName, Kline kline) {
        Kline existing = futuresKlineMapper.selectByBeginTime(
                tableName, kline.getBeginTime(), kline.getPeriod());
        if (existing == null) {
            try {
                futuresKlineMapper.insert(tableName, kline);
            } catch (DuplicateKeyException dup) {
                // 唯一键冲突：多节点并发写入同一根 K 线，内存已更新，跳过
                log.debug("合约 K 线多节点重复写入跳过: table={}, beginTime={}", tableName, kline.getBeginTime());
            }
        } else {
            futuresKlineMapper.update(tableName, kline);
        }
    }

    // ------------------------------------------------------------------ applyManualPrice

    /**
     * 手动注入价格：遍历所有已订阅周期，将内存缓存和数据库中最新一根 K 线的收盘价更新为指定价格，
     * 并同步更新 Redis 中合约最新价（NEW_COIN_PRICE Hash）。
     *
     * @param symbol 小写交易对，如 btcusdt
     * @param price  注入价格
     */
    @Override
    public void applyManualPrice(String symbol, BigDecimal price) {
        // 1. 遍历所有周期，更新内存缓存和数据库
        String tableName = KlineUtils.tableName(symbol);
        for (String period : KlineUtils.PERIODS) {
            updatePeriodClose(tableName, symbol, period, price);
        }

        // 2. 更新 Redis 最新成交价 Hash（NEW_COIN_PRICE:）
        updateRedisPrice(symbol, price);

        // 3. 向 market-contract 频道发布 CoinThumb，通知合约撮合引擎刷新价格
        publishContractPrice(symbol, price);
    }

    /** 更新单个周期的内存缓存和数据库收盘价 */
    private void updatePeriodClose(String tableName, String symbol, String period, BigDecimal price) {
        String cacheKey = symbol + KlineUtils.CACHE_KEY_SEP + period;
        Kline cached = klineUtils.KLINE_CACHE.getIfPresent(cacheKey);
        if (cached == null) {
            return;
        }
        // 更新内存
        cached.setClose(price);
        klineUtils.KLINE_CACHE.put(cacheKey, cached);
        // 更新数据库
        try {
            futuresKlineMapper.update(tableName, cached);
        } catch (Exception e) {
            log.warn("手动价格写库失败: symbol={}, period={}", symbol, period, e);
        }
    }

    /** 更新 Redis NEW_COIN_PRICE Hash（field=contractId） */
    private void updateRedisPrice(String symbol, BigDecimal price) {
        try {
            Long contractId = klineUtils.SYMBOL_ID_MAP.get(symbol);
            if (contractId == null) {
                return;
            }
            redisTemplate.opsForHash().put(REDIS_PRICE_KEY, contractId.toString(), price.toPlainString());
        } catch (Exception e) {
            log.warn("手动价格写 Redis 失败: symbol={}", symbol, e);
        }
    }

    /**
     * 向 market-contract Redis 频道发布 CoinThumb，
     * contract-swap-api 的 MarketListener 订阅该频道并驱动引擎价格刷新。
     * symbol 格式：btcusdt → BTC/USDT
     */
    private void publishContractPrice(String symbol, BigDecimal price) {
        try {
            String upper = symbol.toUpperCase();
            String sym = upper.endsWith("USDT")
                    ? upper.substring(0, upper.length() - 4) + "/USDT"
                    : upper;

            CoinThumb thumb = new CoinThumb();
            thumb.setSymbol(sym);
            thumb.setOpen(price);
            thumb.setHigh(price);
            thumb.setLow(price);
            thumb.setClose(price);
            thumb.setTime(System.currentTimeMillis() / 1000);

            String json = objectMapper.writeValueAsString(thumb);
            redisTemplate.convertAndSend(REDIS_CHANNEL_CONTRACT, json);
        } catch (Exception e) {
            log.warn("手动价格推送 market-contract 失败: symbol={}", symbol, e);
        }
    }

    // ------------------------------------------------------------------ queryHistory

    @Override
    public List<Kline> queryHistory(String symbol, String interval, Long from, Long to) {
        return futuresKlineMapper.selectByTimeRange(KlineUtils.tableName(symbol), interval, from, to);
    }

    // ------------------------------------------------------------------ 转换

    private Kline toEntity(String period, JsonNode k) {
        Kline e = new Kline();
        e.setPeriod(period);
        e.setBeginTime(k.get("t").asLong());
        e.setEndTime(k.get("T").asLong());
        e.setOpen(new BigDecimal(k.get("o").asText()));
        e.setHigh(new BigDecimal(k.get("h").asText()));
        e.setLow(new BigDecimal(k.get("l").asText()));
        e.setClose(new BigDecimal(k.get("c").asText()));
        e.setVolume(new BigDecimal(k.get("v").asText()));
        e.setTurnover(new BigDecimal(k.get("q").asText()));
        return e;
    }
}
