package com.goldenchart.service;

import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.goldenchart.dto.CandleBarDto;
import com.goldenchart.storage.Ta4jStorage;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.web.reactive.function.client.WebClient;
import org.ta4j.core.*;
import org.ta4j.core.bars.TimeBarBuilderFactory;
import org.ta4j.core.indicators.RSIIndicator;
import org.ta4j.core.indicators.averages.EMAIndicator;
import org.ta4j.core.indicators.helpers.ClosePriceIndicator;
import org.ta4j.core.num.DoubleNumFactory;

import java.time.Duration;
import java.time.Instant;
import java.time.ZonedDateTime;
import java.util.*;
import java.util.stream.Collectors;

/**
 * 과거 캔들 데이터 페이징 중계기 (명세서 3.4).
 *
 * 동작 메커니즘:
 *  1. 요청한 기준 시간(to) 이 Ta4jStorage 인메모리(최근 300개) 범위 내 → 메모리 슬라이싱 반환
 *  2. 인메모리 범위 초과(먼 과거) → 업비트 REST API 포워딩
 *  3. 응답 데이터 + 워밍업 캔들을 임시 BarSeries 에 로드 → Ta4j RSI 계산 후 첨부 반환
 */
@Service
@RequiredArgsConstructor
@Slf4j
public class HistoricalDataService {

    private final Ta4jStorage ta4jStorage;
    private final WebClient.Builder webClientBuilder;
    private final ObjectMapper objectMapper;

    @Value("${upbit.api.base-url:https://api.upbit.com}")
    private String upbitBaseUrl;

    @Value("${upbit.api.candle-limit:200}")
    private int candleLimit;

    @Value("${upbit.api.request-delay-ms:110}")
    private long requestDelayMs;

    /** RSI 계산 기본 기간 */
    private static final int RSI_PERIOD = 14;

    /** RSI 수렴에 필요한 워밍업 캔들 수 (실제 반환하지 않음) */
    private static final int RSI_WARMUP = 50;

    // ── 공개 API ──────────────────────────────────────────────────────────────

    /**
     * 과거 캔들 조회.
     *
     * @param market     업비트 마켓 코드 (e.g. "KRW-BTC")
     * @param candleType 캔들 타입 (e.g. "1m", "1d")
     * @param to         기준 시간 이전(exclusive) ISO-8601 문자열 (e.g. "2026-05-20T10:00:00")
     * @param count      요청 캔들 수 (최대 200)
     * @return CandleBarDto 리스트 (시간 오름차순)
     */
    public List<CandleBarDto> getHistory(String market, String candleType, String to, int count) {
        int safeCount = Math.min(count, candleLimit);

        // ── 1. to 시간 파싱 ───────────────────────────────────────────────────
        ZonedDateTime toTime = parseToTime(to);

        // ── 2. 인메모리 → 부족 시 업비트 REST 폴백 ─────────────────────────────
        List<UpbitCandleRaw> rawBars = Collections.emptyList();
        Optional<ZonedDateTime> oldestMemory = ta4jStorage.getOldestTime(market, candleType);
        boolean inMemory = oldestMemory.isPresent() && !toTime.isBefore(oldestMemory.get());

        if (inMemory) {
            log.debug("[HistoricalDataService] 인메모리에서 조회: {} / {} / to={}", market, candleType, to);
            rawBars = sliceFromMemory(market, candleType, toTime, safeCount);
        }

        if (rawBars.isEmpty()) {
            log.debug("[HistoricalDataService] 업비트 REST 프록시: {} / {} / to={}", market, candleType, to);
            rawBars = fetchFromUpbit(market, candleType, to, safeCount);
        }

        if (rawBars.isEmpty()) return Collections.emptyList();

        // ── 3. RSI 계산 (실패 시 OHLCV만 반환) ─────────────────────────────────
        return attachRsi(rawBars, market, candleType);
    }

    // ── 인메모리 슬라이싱 ─────────────────────────────────────────────────────

    private List<UpbitCandleRaw> sliceFromMemory(String market, String candleType,
                                                  ZonedDateTime toTime, int count) {
        List<Bar> bars = ta4jStorage.getBars(market, candleType);
        Instant toInstant = toTime.toInstant();
        // toTime 이전 Bar 만 필터링, 최신부터 count 개 역순 선택 후 다시 시간순 정렬
        List<UpbitCandleRaw> result = bars.stream()
                .filter(b -> !b.getEndTime().isAfter(toInstant))
                .sorted(Comparator.comparing(Bar::getEndTime).reversed())
                .limit(count)
                .map(UpbitCandleRaw::of)
                .collect(Collectors.toList());
        Collections.reverse(result);
        return result;
    }

    // ── 업비트 REST 프록시 ────────────────────────────────────────────────────

    private List<UpbitCandleRaw> fetchFromUpbit(String market, String candleType,
                                                 String to, int count) {
        String path = Ta4jStorage.CANDLE_TYPE_MAP.getOrDefault(candleType, "minutes/1");
        String url = upbitBaseUrl + "/v1/candles/" + path
                + "?market=" + market
                + "&count=" + count
                + (to != null && !to.isBlank() ? "&to=" + to + "Z" : "");

        try {
            Thread.sleep(requestDelayMs); // 업비트 요청 속도 제한 준수
            String json = webClientBuilder.build()
                    .get()
                    .uri(url)
                    .retrieve()
                    .bodyToMono(String.class)
                    .block();

            if (json == null || json.isBlank()) return Collections.emptyList();

            List<Map<String, Object>> raw =
                    objectMapper.readValue(json, new TypeReference<>() {});

            // 업비트 응답은 최신→과거 순 → 시간 오름차순으로 뒤집기
            List<UpbitCandleRaw> list = raw.stream()
                    .map(this::parseUpbitCandle)
                    .sorted(Comparator.comparingLong(c -> c.time))
                    .collect(Collectors.toList());
            return list;

        } catch (InterruptedException ie) {
            Thread.currentThread().interrupt();
            log.error("[HistoricalDataService] 업비트 요청 인터럽트", ie);
        } catch (Exception e) {
            log.error("[HistoricalDataService] 업비트 REST 호출 실패: {}", e.getMessage());
        }
        return Collections.emptyList();
    }

    private UpbitCandleRaw parseUpbitCandle(Map<String, Object> m) {
        // candle_date_time_utc: "2026-05-20T10:00:00"
        String dtStr = (String) m.get("candle_date_time_utc");
        long epochSec = ZonedDateTime.parse(dtStr + "Z",
                java.time.format.DateTimeFormatter.ISO_DATE_TIME).toEpochSecond();
        return new UpbitCandleRaw(
                epochSec,
                toDouble(m.get("opening_price")),
                toDouble(m.get("high_price")),
                toDouble(m.get("low_price")),
                toDouble(m.get("trade_price")),
                toDouble(m.get("candle_acc_trade_volume"))
        );
    }

    // ── RSI 계산 (Ta4j 임시 BarSeries) ────────────────────────────────────────

    /**
     * rawBars 앞에 워밍업용 캔들을 추가(메모리에서 보충)하여 임시 BarSeries 구성 후
     * RSI 를 계산. 워밍업 구간은 결과에서 제외하고 원래 rawBars 구간만 반환.
     */
    private List<CandleBarDto> attachRsi(List<UpbitCandleRaw> rawBars,
                                          String market, String candleType) {
        // 워밍업 캔들: 인메모리에서 rawBars 의 가장 오래된 시간 이전 데이터를 추가
        List<UpbitCandleRaw> warmup = getWarmupBars(market, candleType,
                Ta4jStorage.epochToZdt(rawBars.get(0).time), RSI_WARMUP);

        // 임시 BarSeries 구성 (워밍업 + 대상 bars)
        BarSeries tmpSeries = buildTempSeries(warmup, rawBars, candleType);

        if (tmpSeries.isEmpty()) {
            return rawBars.stream()
                    .map(HistoricalDataService::toDtoWithoutRsi)
                    .collect(Collectors.toList());
        }

        ClosePriceIndicator close = new ClosePriceIndicator(tmpSeries);
        RSIIndicator rsi = new RSIIndicator(close, RSI_PERIOD);

        int warmupSize = warmup.size();
        int endIdx = tmpSeries.getEndIndex();
        List<CandleBarDto> result = new ArrayList<>();
        for (int i = 0; i < rawBars.size(); i++) {
            int seriesIdx = warmupSize + i;
            UpbitCandleRaw raw = rawBars.get(i);
            Double rsiVal = null;
            if (seriesIdx >= RSI_PERIOD && seriesIdx <= endIdx) {
                try {
                    double v = rsi.getValue(seriesIdx).doubleValue();
                    if (!Double.isNaN(v) && !Double.isInfinite(v)) rsiVal = v;
                } catch (Exception e) {
                    log.trace("[HistoricalDataService] RSI 스킵 idx={}: {}", seriesIdx, e.getMessage());
                }
            }
            result.add(CandleBarDto.builder()
                    .time(raw.time)
                    .open(raw.open)
                    .high(raw.high)
                    .low(raw.low)
                    .close(raw.close)
                    .volume(raw.volume)
                    .rsi(rsiVal)
                    .build());
        }
        return result;
    }

    private static CandleBarDto toDtoWithoutRsi(UpbitCandleRaw raw) {
        return CandleBarDto.builder()
                .time(raw.time)
                .open(raw.open)
                .high(raw.high)
                .low(raw.low)
                .close(raw.close)
                .volume(raw.volume)
                .build();
    }

    /** 인메모리에서 워밍업 캔들 추출 */
    private List<UpbitCandleRaw> getWarmupBars(String market, String candleType,
                                                 ZonedDateTime before, int maxCount) {
        if (!ta4jStorage.exists(market, candleType)) return Collections.emptyList();
        List<Bar> bars = ta4jStorage.getBars(market, candleType);
        Instant beforeInstant = before.toInstant();
        return bars.stream()
                .filter(b -> b.getEndTime().isBefore(beforeInstant))
                .sorted(Comparator.comparing(Bar::getEndTime).reversed())
                .limit(maxCount)
                .map(UpbitCandleRaw::of)
                .sorted(Comparator.comparingLong(c -> c.time))
                .collect(Collectors.toList());
    }

    /** 워밍업 + 대상 캔들 배열로 임시 BarSeries 생성 */
    private BarSeries buildTempSeries(List<UpbitCandleRaw> warmup,
                                       List<UpbitCandleRaw> target,
                                       String candleType) {
        BarSeries series = new BaseBarSeriesBuilder()
                .withName("tmp")
                .withBarBuilderFactory(new TimeBarBuilderFactory())
                .withNumFactory(DoubleNumFactory.getInstance())  // Ta4j 0.22
                .build();
        Duration duration = Ta4jStorage.parseDuration(candleType);

        for (UpbitCandleRaw r : warmup)  addRawToSeries(series, r, duration);
        for (UpbitCandleRaw r : target)  addRawToSeries(series, r, duration);
        return series;
    }

    private void addRawToSeries(BarSeries series, UpbitCandleRaw r, Duration duration) {
        try {
            // Ta4j 0.22: barBuilder().endTime() 은 Instant 파라미터
            Instant endInstant = Ta4jStorage.epochToInstant(r.time).plus(duration);
            Bar bar = series.barBuilder()
                    .timePeriod(duration)
                    .endTime(endInstant)
                    .openPrice(r.open)
                    .highPrice(r.high)
                    .lowPrice(r.low)
                    .closePrice(r.close)
                    .volume(r.volume)
                    .build();
            series.addBar(bar, true);
        } catch (Exception e) {
            log.trace("[HistoricalDataService] Bar 추가 스킵: {}", e.getMessage());
        }
    }

    // ── 유틸 ──────────────────────────────────────────────────────────────────

    private ZonedDateTime parseToTime(String to) {
        if (to == null || to.isBlank()) return ZonedDateTime.now(java.time.ZoneOffset.UTC);
        try {
            // ISO-8601 without Z 또는 with Z 모두 처리
            String s = to.endsWith("Z") ? to : to + "Z";
            return ZonedDateTime.parse(s, java.time.format.DateTimeFormatter.ISO_DATE_TIME);
        } catch (Exception e) {
            return ZonedDateTime.now(java.time.ZoneOffset.UTC);
        }
    }

    private static double toDouble(Object v) {
        if (v == null) return 0.0;
        return ((Number) v).doubleValue();
    }

    // ── 내부 레코드 ───────────────────────────────────────────────────────────

    /** 업비트 캔들 / 인메모리 Bar 공통 내부 표현 */
    record UpbitCandleRaw(long time, double open, double high, double low,
                          double close, double volume) {
        static UpbitCandleRaw of(Bar b) {
            return new UpbitCandleRaw(
                    b.getEndTime().getEpochSecond() - b.getTimePeriod().getSeconds(),
                    b.getOpenPrice().doubleValue(),
                    b.getHighPrice().doubleValue(),
                    b.getLowPrice().doubleValue(),
                    b.getClosePrice().doubleValue(),
                    b.getVolume().doubleValue()
            );
        }
    }
}
