package com.goldenchart.websocket;

import com.goldenchart.dto.CandleBarDto;
import com.goldenchart.entity.GcLiveStrategySettings;
import com.goldenchart.repository.GcLiveStrategySettingsRepository;
import com.goldenchart.service.LiveStrategyEvaluator;
import com.goldenchart.service.LiveStrategyTimeframeService;
import com.goldenchart.service.StrategyConditionTimeframeService;
import com.goldenchart.service.TradeSignalService;
import com.goldenchart.trading.pipeline.OrderExecutionQueue;
import com.goldenchart.storage.Ta4jStorage;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.ta4j.core.Bar;
import org.ta4j.core.BarSeries;

import org.ta4j.core.bars.TimeBarBuilderFactory;
import org.ta4j.core.num.DoubleNumFactory;

import java.time.Duration;
import java.time.Instant;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.ZoneOffset;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;

/**
 * 업비트 WebSocket 틱(Trade) 데이터를 수신하여 분봉 단위 캔들을 조립하는 빌더.
 *
 * 명세서 3.2 준수:
 *  - 동일 분(minute) 내 틱 → series.addPrice(price, volume) 로 현재 Bar 갱신
 *  - 분이 바뀌는 틱 → 이전 Bar 를 확정하고 series.addBar(newBar) 로 새 캔들 열기
 *  - 1분봉 확정 시 상위 타임프레임(3m, 5m, 15m, 30m, 1h, 4h, 1d)으로 집계 전파
 *
 * 지원 캔들 타입: 1m, 3m, 5m, 15m, 30m, 1h, 4h, 1d
 */
@Component
@RequiredArgsConstructor
@Slf4j
public class BarBuilder {

    private final Ta4jStorage ta4jStorage;
    private final TradingWebSocketBroker broker;
    private final LiveStrategyEvaluator liveStrategyEvaluator;
    private final TradeSignalService tradeSignalService;
    private final OrderExecutionQueue orderExecutionQueue;
    private final GcLiveStrategySettingsRepository liveSettingsRepo;
    private final StrategyConditionTimeframeService conditionTimeframes;

    /** 상위 집계 타임프레임 (분 단위 집계 주기: 3, 5, 15, 30, 60, 240, 1440) */
    private static final int[] UPPER_MINUTES = {3, 5, 15, 30, 60, 240, 1440};
    private static final String[] UPPER_TYPES = {"3m", "5m", "15m", "30m", "1h", "4h", "1d"};

    /** 진행 중인 1분봉 상태 (market → PartialBar) */
    private final ConcurrentHashMap<String, PartialBar> partialBars  = new ConcurrentHashMap<>();

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

    /**
     * 업비트 trade 틱을 처리하여 1분봉을 조립한다.
     *
     * @param market        마켓 코드 (e.g. "KRW-BTC")
     * @param tradePrice    체결가
     * @param tradeVolume   체결량
     * @param tradeTimeMs   체결 Unix 밀리초
     */
    public void onTick(String market, double tradePrice, double tradeVolume, long tradeTimeMs) {
        ZonedDateTime tickTime = ZonedDateTime.ofInstant(
                java.time.Instant.ofEpochMilli(tradeTimeMs), ZoneOffset.UTC);

        // 해당 분의 시작 시간 (초 이하 제거)
        ZonedDateTime barStart = tickTime.withSecond(0).withNano(0);

        PartialBar current = partialBars.get(market);

        if (current == null || !current.barStart.equals(barStart)) {
            // ── 분 변경: 이전 Bar 확정 및 신규 Bar 열기 ─────────────────────
            if (current != null) {
                commitBar(market, current);  // 이전 Bar 확정
            }
            // 신규 Bar 시작
            PartialBar newBar = new PartialBar(barStart, tradePrice, tradeVolume);
            partialBars.put(market, newBar);

            // 새 분이 시작되는 첫 틱을 Ta4jStorage 에 provisional bar 로 추가.
            // 이렇게 해야 이후 동일 분 틱에서 updateLastBar 가 직전 확정봉을 오염시키지 않는다.
            Duration d1m = Duration.ofMinutes(1);
            ZonedDateTime endTime = barStart.plus(d1m);
            Bar provisionalBar = buildTa4jBar(newBar, d1m, endTime);
            ta4jStorage.addBar(market, "1m", provisionalBar);

        } else {
            // ── 동일 분: 진행 중인 Bar 갱신 ────────────────────────────────
            current.update(tradePrice, tradeVolume);
            // Ta4jStorage 마지막 봉(현재 진행 중인 provisional bar)을 실시간 갱신
            ta4jStorage.updateLastBar(market, "1m", tradePrice, tradeVolume);
        }

        // 현재 1분봉 상태를 STOMP 로 즉시 발행 (실시간 틱 렌더링용)
        publishCurrentBar(market);
    }

    // ── 내부 처리 ──────────────────────────────────────────────────────────────

    /** 완성된 1분봉을 Ta4jStorage 에 저장하고 상위 타임프레임에 전파 */
    private void commitBar(String market, PartialBar partial) {
        Duration d1m = Duration.ofMinutes(1);
        ZonedDateTime endTime = partial.barStart.plus(d1m);

        // Ta4j Bar 생성
        Bar bar = buildTa4jBar(partial, d1m, endTime);

        // 1분봉 Ta4jStorage 저장
        ta4jStorage.addBar(market, "1m", bar);
        log.debug("[BarBuilder] 1m Bar 확정: {} @ {}", market, endTime);

        if (shouldEvaluateOnClose(market, "1m")) {
            BarSeries series1m = ta4jStorage.getOrCreate(market, "1m");
            evaluateStrategyOnBarClose(market, "1m", series1m.getEndIndex(), bar);
        }

        // 상위 타임프레임 전파
        for (int i = 0; i < UPPER_MINUTES.length; i++) {
            int minutes = UPPER_MINUTES[i];
            String type = UPPER_TYPES[i];
            // 해당 상위 봉의 시작 시간 계산
            ZonedDateTime upperStart = alignToInterval(partial.barStart, minutes);
            ZonedDateTime upperEnd = upperStart.plusMinutes(minutes);

            BarSeries upperSeries = ta4jStorage.getOrCreate(market, type);
                if (upperSeries.isEmpty()) {
                    // 새 상위봉 열기
                    Bar upperBar = buildTa4jBar(partial, Duration.ofMinutes(minutes), upperEnd);
                    ta4jStorage.addBar(market, type, upperBar);
                } else {
                    Bar lastUpper = upperSeries.getLastBar();
                    // Ta4j 0.22: getEndTime() → Instant
                    Instant lastUpperEndInstant = lastUpper.getEndTime();
                    Instant upperEndInstant = upperEnd.toInstant();
                    if (lastUpperEndInstant.equals(upperEndInstant)) {
                        // 동일 구간 → 가격 갱신
                        ta4jStorage.updateLastBar(market, type, partial.close, partial.volume);
                    } else if (lastUpperEndInstant.isBefore(upperEndInstant)) {
                        // 다음 구간 → 새 Bar 추가 (이전 상위봉 마감)
                        Bar upperBar = buildTa4jBar(partial, Duration.ofMinutes(minutes), upperEnd);
                        ta4jStorage.addBar(market, type, upperBar);
                        BarSeries upperAfter = ta4jStorage.getOrCreate(market, type);
                        if (shouldEvaluateOnClose(market, type)) {
                            int maturedUpper = Math.max(upperAfter.getBeginIndex(), upperAfter.getEndIndex() - 1);
                            Bar maturedUpperBar = upperAfter.getBar(maturedUpper);
                            evaluateStrategyOnBarClose(market, type, maturedUpper, maturedUpperBar);
                        }
                    }
                }
        }
    }

    /** 활성 실시간 전략 설정 중 전략 DSL에 해당 분봉이 포함된 항목이 있는지 */
    private boolean shouldEvaluateOnClose(String market, String candleType) {
        String ct = LiveStrategyTimeframeService.normalize(candleType);
        return liveSettingsRepo.findActiveByMarket(market).stream()
                .filter(s -> Boolean.TRUE.equals(s.getIsLiveCheck()) && s.getStrategyId() != null)
                .anyMatch(s -> conditionTimeframes.usesTimeframe(s.getStrategyId(), ct));
    }

    /**
     * 봉 마감 시점 전략 평가 + 시그널 STOMP 발행(평가 분봉 채널) + DB/주문 큐 적재.
     */
    private void evaluateStrategyOnBarClose(String market, String candleType, int maturedIndex,
                                            Bar signalBar) {
        String closeSignal = liveStrategyEvaluator.evaluateCandleClose(market, candleType, maturedIndex);
        String signalExecType = "CANDLE_CLOSE";

        if ("NONE".equals(closeSignal)) {
            closeSignal = liveStrategyEvaluator.evaluateRealtimeAtClose(market, candleType, maturedIndex);
            signalExecType = "REALTIME_TICK";
        }

        if ("BUY".equals(closeSignal) || "SELL".equals(closeSignal)) {
            publishStrategySignal(market, candleType, signalBar, closeSignal);
        }

        if (!"BUY".equals(closeSignal) && !"SELL".equals(closeSignal)) return;

        try {
            final String execType = signalExecType;
            GcLiveStrategySettings activeSetting = liveSettingsRepo.findActiveByMarket(market)
                    .stream().filter(s -> Boolean.TRUE.equals(s.getIsLiveCheck())
                            && execType.equals(s.getExecutionType())).findFirst().orElse(null);
            long candleTimeEpoch = signalBar.getEndTime().getEpochSecond()
                    - signalBar.getTimePeriod().getSeconds();
            String devId = activeSetting != null ? activeSetting.getDeviceId() : null;
            Long uid = activeSetting != null ? activeSetting.getUserId() : null;
            Long stratId = activeSetting != null ? activeSetting.getStrategyId() : null;
            tradeSignalService.save(
                    devId, uid, market, stratId, null,
                    closeSignal, signalBar.getClosePrice().doubleValue(),
                    candleTimeEpoch, candleType, execType);
            if (devId != null) {
                orderExecutionQueue.submitSignal(devId, uid, market, stratId, closeSignal,
                        signalBar.getClosePrice().doubleValue());
            }
        } catch (Exception e) {
            log.warn("[BarBuilder] 시그널 처리 실패 ({}/{}): {}", market, candleType, e.getMessage());
        }
    }

    /** 평가 분봉 STOMP 채널로 봉 마감 시그널 발행 */
    private void publishStrategySignal(String market, String candleType, Bar bar, String signal) {
        long barStartEpoch = bar.getEndTime().getEpochSecond() - bar.getTimePeriod().getSeconds();
        CandleBarDto dto = CandleBarDto.builder()
                .time(barStartEpoch)
                .open(bar.getOpenPrice().doubleValue())
                .high(bar.getHighPrice().doubleValue())
                .low(bar.getLowPrice().doubleValue())
                .close(bar.getClosePrice().doubleValue())
                .volume(bar.getVolume().doubleValue())
                .signal(signal)
                .build();
        broker.publish(market, candleType, dto);
        log.info("[BarBuilder] bar-close signal={} market={} candleType={}", signal, market, candleType);
    }

    /** 현재 진행 중인 1분봉을 STOMP 로 발행 (실시간 틱 렌더링용, 시그널 없음). */
    private void publishCurrentBar(String market) {
        PartialBar p = partialBars.get(market);
        if (p == null) return;

        CandleBarDto dto = CandleBarDto.builder()
                .time(p.barStart.toEpochSecond())
                .open(p.open)
                .high(p.high)
                .low(p.low)
                .close(p.close)
                .volume(p.volume)
                .build();
        broker.publish(market, "1m", dto);
        publishUpperTimeframes(market);
    }

    /** 상위 분봉 구독자용 — Ta4jStorage 마지막 봉 스냅샷 (틱마다 1m 과 함께 발행) */
    private void publishUpperTimeframes(String market) {
        for (String type : UPPER_TYPES) {
            try {
                BarSeries series = ta4jStorage.getOrCreate(market, type);
                if (series.isEmpty()) continue;
                Bar last = series.getLastBar();
                long barStart = last.getEndTime().getEpochSecond() - last.getTimePeriod().getSeconds();
                broker.publish(market, type, CandleBarDto.builder()
                        .time(barStart)
                        .open(last.getOpenPrice().doubleValue())
                        .high(last.getHighPrice().doubleValue())
                        .low(last.getLowPrice().doubleValue())
                        .close(last.getClosePrice().doubleValue())
                        .volume(last.getVolume().doubleValue())
                        .build());
            } catch (Exception e) {
                log.trace("[BarBuilder] upper publish skip {}/{}: {}", market, type, e.getMessage());
            }
        }
    }

    /** PartialBar → Ta4j Bar 변환 */
    private Bar buildTa4jBar(PartialBar partial, Duration duration, ZonedDateTime endZdt) {
        BarSeries tmp = new org.ta4j.core.BaseBarSeriesBuilder()
                .withBarBuilderFactory(new TimeBarBuilderFactory())
                .withNumFactory(DoubleNumFactory.getInstance())  // Ta4j 0.22
                .build();
        // Ta4j 0.22: barBuilder().endTime() 은 Instant 파라미터
        Instant endInstant = endZdt.toInstant();
        return tmp.barBuilder()
                .timePeriod(duration)
                .endTime(endInstant)
                .openPrice(partial.open)
                .highPrice(partial.high)
                .lowPrice(partial.low)
                .closePrice(partial.close)
                .volume(partial.volume)
                .build();
    }

    /** 업비트 일봉 기준 시각: KST (UTC+9) */
    private static final ZoneId ZONE_KST = ZoneId.of("Asia/Seoul");

    /**
     * 분 단위 시간을 상위 interval 의 시작으로 정렬.
     *
     * 일봉(minutes == 1440)은 업비트와 동일하게 KST 00:00 기준으로 정렬한다.
     * 그 외 모든 타임프레임은 UTC epoch 기준 정렬(기존 방식 유지).
     */
    private ZonedDateTime alignToInterval(ZonedDateTime time, int minutes) {
        if (minutes == 1440) {
            // 일봉: KST 자정(00:00) 기준 — 업비트 일봉 경계와 일치
            ZonedDateTime kst = time.withZoneSameInstant(ZONE_KST);
            return kst.toLocalDate().atStartOfDay(ZONE_KST);
        }
        long epochMin = time.toEpochSecond() / 60;
        long aligned  = (epochMin / minutes) * minutes;
        return Instant.ofEpochSecond(aligned * 60).atZone(ZoneOffset.UTC);
    }

    // ── 내부 상태 ──────────────────────────────────────────────────────────────

    /** 진행 중인 단일 캔들 상태 */
    static class PartialBar {
        final ZonedDateTime barStart;
        final double open;
        double high;
        double low;
        double close;
        double volume;

        PartialBar(ZonedDateTime barStart, double price, double volume) {
            this.barStart = barStart;
            this.open  = price;
            this.high  = price;
            this.low   = price;
            this.close = price;
            this.volume = volume;
        }

        void update(double price, double vol) {
            if (price > this.high) this.high = price;
            if (price < this.low)  this.low  = price;
            this.close  = price;
            this.volume += vol;
        }
    }
}
