package com.goldenchart.storage;

import com.goldenchart.trading.pipeline.TradingPipelineProperties;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.ta4j.core.Bar;
import org.ta4j.core.BarSeries;
import org.ta4j.core.BaseBarSeriesBuilder;
import org.ta4j.core.bars.TimeBarBuilderFactory;
import org.ta4j.core.num.DoubleNumFactory;

import java.time.Duration;
import java.time.Instant;
import java.time.ZoneOffset;
import java.time.ZonedDateTime;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;

/**
 * 인메모리 실시간 시세 저장소 (싱글톤 서비스).
 *
 * 구조: ConcurrentHashMap<market, Map<candleType, BarSeries>>
 * - market  : "KRW-BTC", "KRW-ETH" 등 업비트 종목 코드
 * - candleType: "1m", "3m", "5m", "15m", "30m", "1h", "4h", "1d"
 *
 * 슬라이딩 윈도우: 모든 BarSeries 에 setMaximumBarCount(300) 적용.
 * 301번째 캔들이 들어오면 가장 오래된 캔들이 자동 소멸.
 */
@Service
@Slf4j
public class Ta4jStorage {

    /** 인메모리 최대 캔들 보관 수 (기본 — 파이프라인 비활성 시) */
    public static final int MAX_BAR_COUNT = 1800;

    private final TradingPipelineProperties pipelineProperties;

    public Ta4jStorage(TradingPipelineProperties pipelineProperties) {
        this.pipelineProperties = pipelineProperties;
    }

    /** 외부 캔들 요청 1회당 기본 배치 크기 */
    public static final int FETCH_BATCH = 200;

    /** 지원 캔들 타입 → 업비트 API 경로 세그먼트 매핑 */
    public static final Map<String, String> CANDLE_TYPE_MAP = Map.of(
            "1m",  "minutes/1",
            "3m",  "minutes/3",
            "5m",  "minutes/5",
            "15m", "minutes/15",
            "30m", "minutes/30",
            "1h",  "minutes/60",
            "4h",  "minutes/240",
            "1d",  "days"
    );

    // ── 인메모리 저장소 ───────────────────────────────────────────────────────
    // 2중 ConcurrentHashMap: market → (candleType → BarSeries)
    private final ConcurrentHashMap<String, ConcurrentHashMap<String, BarSeries>> storage =
            new ConcurrentHashMap<>();

    // ── BarSeries 접근 ────────────────────────────────────────────────────────

    /**
     * 종목·타입의 BarSeries 를 반환. 없으면 새로 생성한다.
     */
    public BarSeries getOrCreate(String market, String candleType) {
        return storage
                .computeIfAbsent(market, k -> new ConcurrentHashMap<>())
                .computeIfAbsent(candleType, k -> createSeries(market, candleType));
    }

    /** 종목·타입의 BarSeries 가 이미 존재하는지 확인 */
    public boolean exists(String market, String candleType) {
        Map<String, BarSeries> inner = storage.get(market);
        return inner != null && inner.containsKey(candleType);
    }

    /** 종목·타입의 BarSeries 삭제 (GC 시 사용) */
    public void remove(String market, String candleType) {
        Map<String, BarSeries> inner = storage.get(market);
        if (inner != null) {
            inner.remove(candleType);
            if (inner.isEmpty()) storage.remove(market);
            log.info("[Ta4jStorage] 종목 제거: {} / {}", market, candleType);
        }
    }

    // ── 캔들 추가 ─────────────────────────────────────────────────────────────

    /**
     * Bar 를 시리즈에 추가한다.
     *
     * <p>Ta4j {@code addBar(bar, replace=true)} 는 endTime 비교 없이 무조건 마지막 슬롯을
     * 덮어쓴다. 이 동작은 진행 중 봉(provisional)을 추가할 때 방금 확정된 봉을 파괴하는
     * 버그를 유발한다. 따라서 마지막 봉의 endTime 을 직접 비교해 교체 여부를 결정한다.
     *
     * <ul>
     *   <li>endTime 동일 → replace (진행 중 봉 → 확정봉으로 교체, 또는 warm-up 재추가)</li>
     *   <li>endTime 다름 → append (신규 봉 추가)</li>
     * </ul>
     */
    public synchronized void addBar(String market, String candleType, Bar bar) {
        BarSeries series = getOrCreate(market, candleType);
        try {
            boolean sameEndTime = !series.isEmpty()
                    && series.getLastBar().getEndTime().equals(bar.getEndTime());
            series.addBar(bar, sameEndTime);
        } catch (Exception e) {
            log.warn("[Ta4jStorage] addBar 오류 ({}/{}): {}", market, candleType, e.getMessage());
        }
    }

    /**
     * 현재 진행 중인 Bar 의 가격·거래량 실시간 갱신. (틱 수신 시)
     */
    public synchronized void updateLastBar(String market, String candleType,
                                           double price, double volume) {
        BarSeries series = getOrCreate(market, candleType);
        if (series.isEmpty()) return;
        try {
            // Ta4j 0.22: addPrice(Num) — 종가 갱신 (고가/저가/거래량은 내부에서 자동 처리)
            series.addPrice(DoubleNumFactory.getInstance().numOf(price));
        } catch (Exception e) {
            log.warn("[Ta4jStorage] updateLastBar 오류 ({}/{}): {}", market, candleType, e.getMessage());
        }
    }

    // ── 조회 ──────────────────────────────────────────────────────────────────

    /**
     * 인메모리 캔들 전체를 시간순(오래된 순) 리스트로 반환.
     */
    public List<Bar> getBars(String market, String candleType) {
        if (!exists(market, candleType)) return Collections.emptyList();
        BarSeries series = getOrCreate(market, candleType);
        if (series.isEmpty()) return Collections.emptyList();
        List<Bar> bars = new ArrayList<>();
        for (int i = series.getBeginIndex(); i <= series.getEndIndex(); i++) {
            bars.add(series.getBar(i));
        }
        return bars;
    }

    /** 인메모리에 실제 봉이 1개 이상 있는지 */
    public boolean hasBars(String market, String candleType) {
        return !getBars(market, candleType).isEmpty();
    }

    /**
     * 인메모리에서 가장 오래된 캔들 시간 반환. 없으면 empty.
     * Ta4j 0.22: Bar.getEndTime() 은 Instant 반환 → ZonedDateTime 변환
     */
    public Optional<ZonedDateTime> getOldestTime(String market, String candleType) {
        if (!exists(market, candleType)) return Optional.empty();
        BarSeries series = getOrCreate(market, candleType);
        if (series.isEmpty()) return Optional.empty();
        return Optional.of(series.getFirstBar().getEndTime().atZone(ZoneOffset.UTC));
    }

    /**
     * 인메모리에서 가장 최신 캔들 시간 반환. 없으면 empty.
     */
    public Optional<ZonedDateTime> getLatestTime(String market, String candleType) {
        if (!exists(market, candleType)) return Optional.empty();
        BarSeries series = getOrCreate(market, candleType);
        if (series.isEmpty()) return Optional.empty();
        return Optional.of(series.getLastBar().getEndTime().atZone(ZoneOffset.UTC));
    }

    /**
     * 인메모리 캔들 수 반환.
     */
    public int getBarCount(String market, String candleType) {
        if (!exists(market, candleType)) return 0;
        return getOrCreate(market, candleType).getBarCount();
    }

    /**
     * 현재 구독(메모리) 중인 종목 목록 반환.
     */
    public List<String> getSubscribedMarkets() {
        return new ArrayList<>(storage.keySet());
    }

    /** 대시보드·파이프라인 모니터링용 인메모리 저장소 통계 */
    public Map<String, Object> storageMetrics() {
        int markets = storage.size();
        int series = 0;
        int totalBars = 0;
        for (var inner : storage.values()) {
            series += inner.size();
            for (BarSeries s : inner.values()) {
                totalBars += s.getBarCount();
            }
        }
        int maxBars = pipelineProperties.isEnabled()
                ? Math.max(200, pipelineProperties.getMaxBarsPerSeries())
                : MAX_BAR_COUNT;
        return Map.of(
                "markets", markets,
                "series", series,
                "totalBars", totalBars,
                "maxBarsPerSeries", maxBars
        );
    }

    // ── 내부 유틸 ─────────────────────────────────────────────────────────────

    private BarSeries createSeries(String market, String candleType) {
        int maxBars = pipelineProperties.isEnabled()
                ? Math.max(200, pipelineProperties.getMaxBarsPerSeries())
                : MAX_BAR_COUNT;
        BarSeries series = new BaseBarSeriesBuilder()
                .withName(market + ":" + candleType)
                .withBarBuilderFactory(new TimeBarBuilderFactory())
                .withNumFactory(DoubleNumFactory.getInstance())  // Ta4j 0.22: withNumFactory
                .withMaxBarCount(maxBars)
                .build();
        log.info("[Ta4jStorage] BarSeries 생성: {} / {} (maxBarCount={})", market, candleType, maxBars);
        return series;
    }

    /** 캔들 타입 문자열 → Duration 변환 (package→public) */
    public static Duration parseDuration(String candleType) {
        return switch (candleType) {
            case "1m"  -> Duration.ofMinutes(1);
            case "3m"  -> Duration.ofMinutes(3);
            case "5m"  -> Duration.ofMinutes(5);
            case "15m" -> Duration.ofMinutes(15);
            case "30m" -> Duration.ofMinutes(30);
            case "1h"  -> Duration.ofHours(1);
            case "4h"  -> Duration.ofHours(4);
            case "1d"  -> Duration.ofDays(1);
            default    -> Duration.ofMinutes(1);
        };
    }

    /** Unix 초 타임스탬프 → ZonedDateTime (UTC) */
    public static ZonedDateTime epochToZdt(long epochSeconds) {
        return Instant.ofEpochSecond(epochSeconds).atZone(ZoneOffset.UTC);
    }

    /** Unix 초 타임스탬프 → Instant */
    public static Instant epochToInstant(long epochSeconds) {
        return Instant.ofEpochSecond(epochSeconds);
    }
}
