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.service.KlineService;
import com.market.service.KlineSyncService;
import com.market.utils.KlineUtils;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.math.BigDecimal;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.time.Duration;
import java.util.*;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors;

@Slf4j
@DS("market")
@Service
public class KlineSyncServiceImpl implements KlineSyncService {

    private static final String BINANCE_KLINES_URL = "https://fapi.binance.com/fapi/v1/klines";
    private static final int    BATCH_LIMIT        = 1500;
    /** 每次请求后休眠，避免触发币安限流 */
    private static final long   REQUEST_DELAY_MS   = 300;

    private final HttpClient httpClient = HttpClient.newBuilder()
            .connectTimeout(Duration.ofSeconds(10))
            .build();
    private final ObjectMapper objectMapper = new ObjectMapper();

    @Autowired
    private KlineService klineService;

    @Autowired
    private FuturesKlineMapper futuresKlineMapper;

    // ------------------------------------------------------------------ sync

    @Override
    public Map<String, Object> sync(long from, long to, String symbol, String interval,List<String> symbols) {
        AtomicInteger total  = new AtomicInteger();
        List<String>  errors = new ArrayList<>();
        List<String> intervals = resolveIntervals(interval);

        for (String sym : symbols) {
            klineService.ensureTable(sym);
            for (String itv : intervals) {
                try {
                    int count = syncOne(sym, itv, from, to);
                    total.addAndGet(count);
                    log.info("同步完成 {}/{}: {} 条", sym, itv, count);
                } catch (Exception e) {
                    String msg = sym + "/" + itv + ": " + e.getMessage();
                    errors.add(msg);
                    log.error("同步失败 {}", msg, e);
                }
            }
        }

        Map<String, Object> result = new LinkedHashMap<>();
        result.put("total",   total.get());
        result.put("symbols", symbols);
        result.put("intervals", intervals);
        if (!errors.isEmpty()) {
            result.put("errors", errors);
        }
        return result;
    }

    // ------------------------------------------------------------------ 单个 symbol+interval 同步

    /**
     * 分页拉取币安 K 线并 upsert，返回本次写库总条数。
     * 每次最多拉 1500 条，循环直到数据不足 1500 条或超出 to 时间。
     */
    private int syncOne(String symbol, String interval, long from, long to) throws Exception {
        String tableName = KlineUtils.tableName(symbol);
        int count = 0;
        long startTime = from;

        while (true) {
            String url = buildUrl(symbol.toUpperCase(), interval, startTime, to);
            String body = get(url);
            JsonNode rows = objectMapper.readTree(body);

            if (!rows.isArray() || rows.size() == 0) break;

            List<Kline> batch = new ArrayList<>(rows.size());
            for (JsonNode row : rows) {
                batch.add(toEntity(row, interval));
            }

            List<Long> beginTimes = batch.stream()
                    .map(Kline::getBeginTime)
                    .collect(Collectors.toList());
            Set<Long> existing = new HashSet<>(
                    futuresKlineMapper.selectExistingBeginTimes(tableName, interval, beginTimes));

            List<Kline> toInsert = batch.stream()
                    .filter(k -> !existing.contains(k.getBeginTime()))
                    .collect(Collectors.toList());
            List<Kline> toUpdate = batch.stream()
                    .filter(k -> existing.contains(k.getBeginTime()))
                    .collect(Collectors.toList());

            if (!toInsert.isEmpty()) futuresKlineMapper.batchInsert(tableName, toInsert);
            if (!toUpdate.isEmpty()) futuresKlineMapper.batchUpdate(tableName, toUpdate);
            count += batch.size();

            if (rows.size() < BATCH_LIMIT) break;

            // 下一批从最后一条的开始时间 + 1ms 开始
            startTime = batch.get(batch.size() - 1).getBeginTime() + 1;
            if (startTime > to) break;
        }
        return count;
    }

    // ------------------------------------------------------------------ 辅助

    private String buildUrl(String symbol, String interval, long startTime, long endTime) {
        return BINANCE_KLINES_URL
                + "?symbol="    + symbol
                + "&interval="  + KlineUtils.toExchangeInterval(interval)
                + "&startTime=" + startTime
                + "&endTime="   + endTime
                + "&limit="     + BATCH_LIMIT;
    }

    private String get(String url) throws Exception {
        HttpRequest request = HttpRequest.newBuilder()
                .uri(URI.create(url))
                .timeout(Duration.ofSeconds(15))
                .GET()
                .build();
        HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
        Thread.sleep(REQUEST_DELAY_MS);
        if (response.statusCode() == 429 || response.statusCode() == 418) {
            throw new RuntimeException("HTTP " + response.statusCode() + ": " + response.body());
        }
        if (response.statusCode() != 200) {
            throw new RuntimeException("HTTP " + response.statusCode() + ": " + response.body());
        }
        return response.body();
    }

    /**
     * 将币安 klines 数组行转换为 Kline 实体。
     * 格式：[openTime, open, high, low, close, volume, closeTime, quoteVolume, ...]
     */
    private Kline toEntity(JsonNode row, String interval) {
        Kline k = new Kline();
        k.setPeriod(interval);
        k.setBeginTime(row.get(0).asLong());
        k.setOpen(new BigDecimal(row.get(1).asText()));
        k.setHigh(new BigDecimal(row.get(2).asText()));
        k.setLow(new BigDecimal(row.get(3).asText()));
        k.setClose(new BigDecimal(row.get(4).asText()));
        k.setVolume(new BigDecimal(row.get(5).asText()));
        k.setEndTime(row.get(6).asLong());
        k.setTurnover(new BigDecimal(row.get(7).asText()));
        return k;
    }

    private List<String> resolveIntervals(String interval) {
        if (StringUtils.isNotBlank(interval)) {
            return Collections.singletonList(interval);
        }
        return Arrays.asList(KlineUtils.PERIODS);
    }
}
