package com.goldenchart.service;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.goldenchart.entity.GcLiveStrategySettings;
import com.goldenchart.entity.GcStrategy;
import com.goldenchart.repository.GcLiveStrategySettingsRepository;
import com.goldenchart.repository.GcStrategyRepository;
import com.goldenchart.storage.Ta4jStorage;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.ta4j.core.*;
import org.ta4j.core.rules.BooleanRule;

import org.springframework.stereotype.Service;

import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;

/**
 * 실시간 전략 체크 평가기.
 *
 * <p>두 가지 방식으로 전략 만족 여부를 판정한다:
 * <ul>
 *   <li>방식 A (CANDLE_CLOSE): 봉 마감 직후 BarBuilder 에서 트리거</li>
 *   <li>방식 B (REALTIME_TICK): 3초 스케줄러에서 현재 진행 중인 캔들 인덱스로 판정</li>
 * </ul>
 *
 * <p>포지션 종속성 모드:
 * <ul>
 *   <li><b>LONG_ONLY</b>: TradingRecord 에 열린 포지션이 있어야 매도 시그널 발생.
 *       시장 진입/청산을 {@code strategy.shouldEnter/shouldExit} 로 판정.</li>
 *   <li><b>SIGNAL_ONLY</b>: 포지션 관계없이 지표 규칙만 충족되면 시그널 확정.
 *       {@code rule.isSatisfied(index)} 를 직접 호출.</li>
 * </ul>
 *
 * <p>동시성 안전:
 * <ul>
 *   <li>ConcurrentHashMap 으로 market×candleType 키에 대한 Strategy 캐시 관리</li>
 *   <li>LONG_ONLY 모드용 TradingRecord 캐시도 ConcurrentHashMap 으로 관리</li>
 *   <li>Ta4jStorage.getOrCreate/updateLastBar 는 내부 synchronized 처리</li>
 * </ul>
 */
@Service
@RequiredArgsConstructor
@Slf4j
public class LiveStrategyEvaluator {

    private final GcLiveStrategySettingsRepository settingsRepo;
    private final GcStrategyRepository             strategyRepo;
    private final Ta4jStorage                      ta4jStorage;
    private final StrategyDslToTa4jAdapter         adapter;
    private final IndicatorSettingsService         indicatorSettingsService;
    private final ObjectMapper                     objectMapper;
    private final StrategySignalDeterminer         determiner;
    private final StrategyConditionTimeframeService conditionTimeframes;

    /**
     * Strategy 캐시: "market:candleType:strategyId" → Strategy
     * (entryRule / exitRule 포함)
     */
    private final ConcurrentHashMap<String, Strategy> strategyCache = new ConcurrentHashMap<>();

    /**
     * LONG_ONLY 모드용 TradingRecord 캐시: "market:candleType:strategyId" → TradingRecord
     * 시그널이 발생할 때마다 Record 를 갱신하여 포지션 상태를 유지한다.
     */
    private final ConcurrentHashMap<String, BaseTradingRecord> tradingRecordCache  = new ConcurrentHashMap<>();

    /**
     * LONG_ONLY 모드용 포지션 오픈 여부 추적: Ta4j API 버전 차이 대응.
     */
    private final ConcurrentHashMap<String, Boolean>           positionOpenCache   = new ConcurrentHashMap<>();

    /**
     * REALTIME_TICK 중복 시그널 방지: "market:candleType" → 마지막으로 시그널을 낸 seriesIndex.
     * 동일 provisional 봉에서 3초 스케줄러가 반복 실행되더라도 이미 시그널을 낸 인덱스면 무시.
     * 새 provisional 봉(다음 분)이 시작되면 자동으로 다른 index 이므로 갱신된다.
     */
    private final ConcurrentHashMap<String, Integer>           realtimeSignaledIdx = new ConcurrentHashMap<>();

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

    /**
     * 방식 A — 봉 마감 직후 호출.
     *
     * @param market        마켓 코드
     * @param candleType    캔들 타입
     * @param maturedIndex  확정된 봉의 BarSeries 인덱스
     * @return "BUY" | "SELL" | "NONE"
     */
    public String evaluateCandleClose(String market, String candleType, int maturedIndex) {
        List<GcLiveStrategySettings> settings = settingsRepo.findActiveByMarket(market);
        if (settings.isEmpty()) return "NONE";

        if (!ta4jStorage.exists(market, candleType)) return "NONE";
        log.debug("[Evaluator] CANDLE_CLOSE 평가: {} {} idx={} barCount={}",
                market, candleType, maturedIndex,
                ta4jStorage.getOrCreate(market, candleType).getBarCount());

        for (GcLiveStrategySettings s : settings) {
            if (!Boolean.TRUE.equals(s.getIsLiveCheck())) continue;
            if (!"CANDLE_CLOSE".equals(s.getExecutionType())) continue;
            if (s.getStrategyId() == null) continue;
            if (!conditionTimeframes.usesTimeframe(s.getStrategyId(), candleType)) continue;

            String result = evaluate(market, candleType, s.getStrategyId(),
                    s.getPositionMode(), maturedIndex,
                    s.getDeviceId(), s.getUserId());
            if (!"NONE".equals(result)) {
                log.info("[Evaluator] CANDLE_CLOSE {} {} idx={} mode={} → {}",
                        market, candleType, maturedIndex, s.getPositionMode(), result);
                return result;
            }
        }
        return "NONE";
    }

    /**
     * 방식 B — 3초 스케줄러에서 호출.
     *
     * <p>동일 provisional 봉(endIndex)에서 CROSS 시그널이 중복 발화되지 않도록
     * 마지막 시그널 인덱스를 추적한다. 새 분이 시작되면 endIndex가 증가하므로
     * 자동으로 다음 교차 이벤트를 감지할 수 있다.
     *
     * <p><b>캐시 무효화 정책 (Ta4j CachedIndicator memoization 문제 대응)</b><br>
     * Ta4j 의 {@code CachedIndicator} 구현체(CCIIndicator, RSIIndicator 등)는
     * {@code getValue(index)} 결과를 내부 리스트에 영구 저장한다.
     * {@code Ta4jStorage.updateLastBar} 가 provisional 봉의 close price 를 갱신해도
     * 이미 캐시된 값은 변하지 않으므로, CrossedDown/Up Rule 이 구 가격으로 판정한다.
     * 이를 방지하기 위해 REALTIME_TICK 평가마다 {@code strategyCache} 를 지워
     * 인디케이터 인스턴스를 강제 재생성한다.
     *
     * @param market     마켓 코드
     * @param candleType 캔들 타입
     * @return "BUY" | "SELL" | "NONE"
     */
    public String evaluateRealtimeTick(String market, String candleType) {
        List<GcLiveStrategySettings> settings = settingsRepo.findActiveByMarket(market);
        if (settings.isEmpty()) return "NONE";

        if (!ta4jStorage.exists(market, candleType)) return "NONE";
        BarSeries series = ta4jStorage.getOrCreate(market, candleType);
        if (series.isEmpty()) return "NONE";
        int currentIndex = series.getEndIndex();

        // 동일 provisional 봉에서 이미 시그널을 냈으면 중복 발화 방지
        String dedupeKey = market + ":" + candleType;
        Integer lastSignaledIdx = realtimeSignaledIdx.get(dedupeKey);
        if (lastSignaledIdx != null && lastSignaledIdx == currentIndex) return "NONE";

        for (GcLiveStrategySettings s : settings) {
            if (!Boolean.TRUE.equals(s.getIsLiveCheck())) continue;
            if (!"REALTIME_TICK".equals(s.getExecutionType())) continue;
            if (s.getStrategyId() == null) continue;
            if (!conditionTimeframes.usesTimeframe(s.getStrategyId(), candleType)) continue;

            // ★ Ta4j CachedIndicator 캐시 무효화:
            //   updateLastBar 로 provisional 봉 가격이 바뀌어도 CCI 등 CachedIndicator 는
            //   이미 캐시된 구 값을 반환한다. 전략을 재빌드하면 새 인디케이터 인스턴스를
            //   생성(빈 캐시)하므로 getValue(currentIndex) 가 현재 가격으로 재계산된다.
            String cacheKey = market + ":" + candleType + ":" + s.getStrategyId();
            strategyCache.remove(cacheKey);

            String result = evaluate(market, candleType, s.getStrategyId(),
                    s.getPositionMode(), currentIndex,
                    s.getDeviceId(), s.getUserId());
            if (!"NONE".equals(result)) {
                log.info("[Evaluator] REALTIME_TICK {} {} idx={} mode={} → {}",
                        market, candleType, currentIndex, s.getPositionMode(), result);
                realtimeSignaledIdx.put(dedupeKey, currentIndex);
                return result;
            }
        }
        return "NONE";
    }

    /**
     * 봉 마감 직후 REALTIME_TICK 설정에 대해 확정봉 인덱스로 전략을 평가한다.
     *
     * <p>배경: CrossedDown/Up 교차 이벤트가 봉 마감 시점에 발생하면, 3초 스케줄러는
     * 이미 다음 분의 provisional 봉(index N+1)을 평가하기 때문에 교차를 감지하지 못한다.
     * (CrossedDown(N+1) → CCI(N) < threshold, CCI(N+1) < threshold → 교차 없음)
     *
     * <p>이 메서드를 {@code BarBuilder.commitBar} 에서 호출하면 CANDLE_CLOSE 와 동일한
     * 타이밍에 REALTIME_TICK 전략도 확정봉 인덱스(N)로 평가하여 누락을 방지한다.
     *
     * @param market       마켓 코드
     * @param candleType   캔들 타입
     * @param maturedIndex 확정된 봉의 BarSeries 인덱스
     * @return "BUY" | "SELL" | "NONE"
     */
    public String evaluateRealtimeAtClose(String market, String candleType, int maturedIndex) {
        List<GcLiveStrategySettings> settings = settingsRepo.findActiveByMarket(market);
        if (settings.isEmpty()) return "NONE";
        if (!ta4jStorage.exists(market, candleType)) return "NONE";

        for (GcLiveStrategySettings s : settings) {
            if (!Boolean.TRUE.equals(s.getIsLiveCheck())) continue;
            if (!"REALTIME_TICK".equals(s.getExecutionType())) continue;
            if (s.getStrategyId() == null) continue;
            if (!conditionTimeframes.usesTimeframe(s.getStrategyId(), candleType)) continue;

            // CachedIndicator 캐시 무효화 — 확정봉의 최종 close 로 재계산
            String cacheKey = market + ":" + candleType + ":" + s.getStrategyId();
            strategyCache.remove(cacheKey);

            String result = evaluate(market, candleType, s.getStrategyId(),
                    s.getPositionMode(), maturedIndex,
                    s.getDeviceId(), s.getUserId());
            if (!"NONE".equals(result)) {
                log.info("[Evaluator] REALTIME_TICK @candle_close {} {} idx={} mode={} → {}",
                        market, candleType, maturedIndex, s.getPositionMode(), result);
                realtimeSignaledIdx.put(market + ":" + candleType, maturedIndex);
                return result;
            }
        }
        return "NONE";
    }

    /** 설정 변경 시 해당 마켓 캐시 무효화 */
    public void invalidateCache(String market) {
        strategyCache.keySet().removeIf(k -> k.startsWith(market + ":"));
        tradingRecordCache.keySet().removeIf(k -> k.startsWith(market + ":"));
        positionOpenCache.keySet().removeIf(k -> k.startsWith(market + ":"));
        realtimeSignaledIdx.keySet().removeIf(k -> k.startsWith(market + ":"));
    }

    // ── Private ───────────────────────────────────────────────────────────────

    private String evaluate(String market, String candleType, long strategyId,
                             String positionMode, int index,
                             String deviceId, Long userId) {
        String cacheKey = market + ":" + candleType + ":" + strategyId;

        // computeIfAbsent 는 device context 전달이 불가하므 수동 분기
        Strategy strategy = strategyCache.get(cacheKey);
        if (strategy == null) {
            strategy = buildStrategy(market, candleType, strategyId, deviceId, userId);
            if (strategy != null) strategyCache.put(cacheKey, strategy);
        }

        if (strategy == null) return "NONE";

        try {
            String mode = positionMode != null ? positionMode : "LONG_ONLY";

            if ("SIGNAL_ONLY".equals(mode)) {
                // 포지션 락 우회 — 순수 Rule 충족 여부만 판단
                return determiner.determineSignal(strategy, null, index, "SIGNAL_ONLY");
            }

            // LONG_ONLY — TradingRecord 상태를 유지하여 포지션 검증
            BaseTradingRecord record = tradingRecordCache.computeIfAbsent(
                    cacheKey, k -> new BaseTradingRecord());
            boolean isOpen = Boolean.TRUE.equals(positionOpenCache.get(cacheKey));
            String signal = determiner.determineSignal(strategy, record, index, "LONG_ONLY");

            // 시그널에 따라 TradingRecord 갱신 및 포지션 상태 추적
            if ("BUY".equals(signal) && !isOpen) {
                BarSeries series = ta4jStorage.getOrCreate(market, candleType);
                org.ta4j.core.num.Num price = series.getBar(index).getClosePrice();
                record.enter(index, price, series.numFactory().numOf(1));
                positionOpenCache.put(cacheKey, true);
            } else if ("SELL".equals(signal) && isOpen) {
                BarSeries series = ta4jStorage.getOrCreate(market, candleType);
                org.ta4j.core.num.Num price = series.getBar(index).getClosePrice();
                record.exit(index, price, series.numFactory().numOf(1));
                positionOpenCache.put(cacheKey, false);
            } else if ("BUY".equals(signal) || "SELL".equals(signal)) {
                // 포지션 상태와 불일치 (이미 매수 중인데 또 BUY 등) → 신호 무시
                signal = "NONE";
            }
            return signal;

        } catch (Exception e) {
            log.warn("[Evaluator] 전략 판정 오류 key={} idx={}: {}", cacheKey, index, e.getMessage());
            strategyCache.remove(cacheKey);
            tradingRecordCache.remove(cacheKey);
        }
        return "NONE";
    }

    private Strategy buildStrategy(String market, String candleType, long strategyId,
                                    String deviceId, Long userId) {
        Optional<GcStrategy> strategyOpt = strategyRepo.findById(strategyId);
        if (strategyOpt.isEmpty()) return null;

        GcStrategy strategy = strategyOpt.get();
        if (!ta4jStorage.exists(market, candleType)) return null;

        BarSeries series = ta4jStorage.getOrCreate(market, candleType);
        // 사용자·장치별 지표 파라미터 우선 로드 (없으면 빈 맵 → adapter 기본값 사용)
        Map<String, Map<String, Object>> indicatorParams =
                indicatorSettingsService.getAll(userId, deviceId);

        int barCount = series.getBarCount();
        if (barCount < 2) {
            log.warn("[Evaluator] 바 수 부족 strategyId={} market={} barCount={} (최소 2 이상 필요)",
                    strategyId, market, barCount);
            return null;
        }
        log.debug("[Evaluator] Strategy 빌드: strategyId={} market={} barCount={}", strategyId, market, barCount);

        try {
            Rule entryRule = buildRule(strategy.getBuyConditionJson(),  series, indicatorParams, market);
            Rule exitRule  = buildRule(strategy.getSellConditionJson(), series, indicatorParams, market);
            BaseStrategy builtStrategy = new BaseStrategy(entryRule, exitRule);
            return builtStrategy;
        } catch (Exception e) {
            log.error("[Evaluator] Strategy 빌드 실패 strategyId={}: {}", strategyId, e.getMessage());
            return null;
        }
    }

    private Rule buildRule(String conditionJson, BarSeries series,
                           Map<String, Map<String, Object>> params,
                           String market) {
        if (conditionJson == null || conditionJson.isBlank()) return new BooleanRule(false);
        try {
            JsonNode node = objectMapper.readTree(conditionJson);
            return adapter.toRule(node, series, params, market, ta4jStorage);
        } catch (Exception e) {
            log.warn("[Evaluator] 조건 JSON 파싱 실패: {}", e.getMessage());
            return new BooleanRule(false);
        }
    }
}
