package com.goldenchart.service;

import com.fasterxml.jackson.databind.JsonNode;
import com.goldenchart.storage.Ta4jStorage;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.ta4j.core.BarSeries;
import org.ta4j.core.Indicator;
import org.ta4j.core.Rule;
import org.ta4j.core.TradingRecord;
import org.ta4j.core.indicators.*;
import org.ta4j.core.indicators.adx.*;
import org.ta4j.core.indicators.averages.*;
import org.ta4j.core.indicators.bollinger.*;
import org.ta4j.core.indicators.helpers.*;
import org.ta4j.core.indicators.donchian.DonchianChannelLowerIndicator;
import org.ta4j.core.indicators.donchian.DonchianChannelMiddleIndicator;
import org.ta4j.core.indicators.donchian.DonchianChannelUpperIndicator;
import org.ta4j.core.indicators.ichimoku.*;
import org.ta4j.core.indicators.statistics.StandardDeviationIndicator;
import org.ta4j.core.indicators.CachedIndicator;
import org.ta4j.core.indicators.helpers.VolumeIndicator;
import org.ta4j.core.indicators.volume.OnBalanceVolumeIndicator;
import org.ta4j.core.indicators.numeric.NumericIndicator;
import org.ta4j.core.indicators.numeric.UnaryOperationIndicator;
import org.ta4j.core.num.Num;
import org.ta4j.core.rules.*;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;

/**
 * frontend DSL(LogicNode 트리) → Ta4j Rule 변환 어댑터.
 *
 * <h3>DSL 타입 → 레지스트리(DB) 키 매핑</h3>
 * <p>frontend strategyTypes.ts 의 PaletteItem.value (DSL indicatorType) 는
 * 대문자 스네이크케이스이지만, indicatorRegistry.ts 의 type 키(=DB 저장 키)는
 * PascalCase 이므로 반드시 변환 후 DB 파라미터를 조회해야 한다.</p>
 *
 * <h3>DSL 구조</h3>
 * <pre>
 * {
 *   "type": "AND" | "OR" | "NOT" | "CONDITION",
 *   "children": [ ...LogicNode ],     // AND/OR/NOT 인 경우
 *   "condition": {                    // CONDITION 인 경우
 *     "indicatorType":  "CCI",
 *     "conditionType":  "CROSS_UP",
 *     "leftField":      "CCI_VALUE",
 *     "rightField":     "OVERBOUGHT_100",
 *     "targetValue":    null,
 *     "slopePeriod":    3,
 *     "holdDays":       3,
 *     "candleRange":    1
 *   }
 * }
 * </pre>
 */
@Component
@Slf4j
public class StrategyDslToTa4jAdapter {

    /**
     * DSL indicatorType(대문자 스네이크) → indicatorRegistry type(PascalCase, DB 키).
     * frontend strategyTypes.ts DSL_TO_REGISTRY 와 동기화.
     */
    private static final Map<String, String> DSL_TO_REGISTRY = Map.ofEntries(
        Map.entry("RSI",                  "RSI"),
        Map.entry("MACD",                 "MACD"),
        Map.entry("CCI",                  "CCI"),
        Map.entry("ADX",                  "ADX"),
        Map.entry("DMI",                  "DMI"),
        Map.entry("OBV",                  "OBV"),
        Map.entry("TRIX",                 "TRIX"),
        Map.entry("EMA",                  "EMA"),
        Map.entry("MA",                   "SMA"),
        Map.entry("BOLLINGER",            "BollingerBands"),
        Map.entry("STOCHASTIC",           "Stochastic"),
        Map.entry("WILLIAMS_R",           "WilliamsPercentRange"),
        Map.entry("ICHIMOKU",             "IchimokuCloud"),
        Map.entry("VOLUME_OSC",           "VolumeOscillator"),
        Map.entry("PSYCHOLOGICAL",        "Psychological"),
        Map.entry("NEW_PSYCHOLOGICAL",    "Psychological"),
        Map.entry("INVEST_PSYCHOLOGICAL", "InvestPsychological"),
        Map.entry("BWI",                  "BBBandWidth"),
        Map.entry("VR",                   "VR"),
        Map.entry("DISPARITY",            "Disparity"),
        Map.entry("DONCHIAN",             "DonchianChannels"),
        Map.entry("VOLUME",               "VOLUME")
    );

    /**
     * DSL indicatorType 을 DB 저장 키(레지스트리 type)로 변환.
     * 매핑 없으면 원본 반환 (이미 레지스트리 형식인 경우).
     */
    private static String toRegistryKey(String dslType) {
        return DSL_TO_REGISTRY.getOrDefault(dslType, dslType);
    }

    // ── Public API ────────────────────────────────────────────────────────────

    public record RuleBuildContext(
            BarSeries primarySeries,
            Map<String, Map<String, Object>> indicatorParams,
            String market,
            Ta4jStorage storage
    ) {
        BarSeries resolveSeries(String candleType) {
            String ct = LiveStrategyTimeframeService.normalize(candleType);
            if (storage != null && market != null && !market.isBlank()
                    && storage.exists(market, ct)) {
                return storage.getOrCreate(market, ct);
            }
            return primarySeries;
        }
    }

    public Rule toRule(JsonNode node, BarSeries series,
                       Map<String, Map<String, Object>> indicatorParams) {
        return toRule(node, new RuleBuildContext(series, indicatorParams, null, null));
    }

    public Rule toRule(JsonNode node, BarSeries series,
                       Map<String, Map<String, Object>> indicatorParams,
                       String market, Ta4jStorage storage) {
        return toRule(node, new RuleBuildContext(series, indicatorParams, market, storage));
    }

    public Rule toRule(JsonNode node, RuleBuildContext ctx) {
        if (node == null || node.isNull()) return new BooleanRule(false);
        String type = node.path("type").asText("CONDITION");

        return switch (type) {
            case "AND"       -> buildAndRule(node, ctx);
            case "OR"        -> buildOrRule(node, ctx);
            case "NOT"       -> buildNotRule(node, ctx);
            case "TIMEFRAME" -> buildTimeframeRule(node, ctx);
            default          -> buildConditionRule(node.path("condition"), ctx.primarySeries(), ctx.indicatorParams());
        };
    }

    private Rule buildTimeframeRule(JsonNode node, RuleBuildContext ctx) {
        String ct = node.path("candleType").asText("1m");
        BarSeries branchSeries = ctx.resolveSeries(ct);
        RuleBuildContext branchCtx = new RuleBuildContext(
                branchSeries, ctx.indicatorParams(), ctx.market(), ctx.storage());
        List<Rule> rules = childRules(node, branchCtx);
        if (rules.isEmpty()) return new BooleanRule(false);
        Rule inner = rules.get(0);
        if (branchSeries == ctx.primarySeries()) return inner;
        return new CrossSeriesRule(inner, branchSeries, ctx.primarySeries());
    }

    /** 다른 시간봉 시리즈로 빌드된 Rule — 트리거 시리즈 인덱스에서 해당 분봉 최신봉으로 평가 */
    private static final class CrossSeriesRule implements Rule {
        private final Rule inner;
        private final BarSeries branchSeries;
        private final BarSeries triggerSeries;

        CrossSeriesRule(Rule inner, BarSeries branchSeries, BarSeries triggerSeries) {
            this.inner = inner;
            this.branchSeries = branchSeries;
            this.triggerSeries = triggerSeries;
        }

        @Override
        public boolean isSatisfied(int index, TradingRecord tradingRecord) {
            int evalIndex = branchSeries == triggerSeries
                    ? index
                    : branchSeries.getEndIndex();
            if (evalIndex < 0 || branchSeries.getBarCount() == 0) return false;
            return inner.isSatisfied(evalIndex, tradingRecord);
        }
    }

    // ── AND / OR / NOT ────────────────────────────────────────────────────────

    private Rule buildAndRule(JsonNode node, RuleBuildContext ctx) {
        List<Rule> rules = childRules(node, ctx);
        if (rules.isEmpty()) return new BooleanRule(false);
        Rule result = rules.get(0);
        for (int i = 1; i < rules.size(); i++) result = new AndRule(result, rules.get(i));
        return result;
    }

    private Rule buildOrRule(JsonNode node, RuleBuildContext ctx) {
        List<Rule> rules = childRules(node, ctx);
        if (rules.isEmpty()) return new BooleanRule(false);
        Rule result = rules.get(0);
        for (int i = 1; i < rules.size(); i++) result = new OrRule(result, rules.get(i));
        return result;
    }

    private Rule buildNotRule(JsonNode node, RuleBuildContext ctx) {
        List<Rule> rules = childRules(node, ctx);
        if (rules.isEmpty()) return new BooleanRule(false);
        return new NotRule(rules.get(0));
    }

    private List<Rule> childRules(JsonNode node, RuleBuildContext ctx) {
        List<Rule> list = new ArrayList<>();
        JsonNode children = node.path("children");
        if (children.isArray()) {
            for (JsonNode child : children) list.add(toRule(child, ctx));
        }
        return list;
    }

    private Rule buildAndRule(JsonNode node, BarSeries series,
                              Map<String, Map<String, Object>> p) {
        return buildAndRule(node, new RuleBuildContext(series, p, null, null));
    }

    private Rule buildOrRule(JsonNode node, BarSeries series,
                             Map<String, Map<String, Object>> p) {
        return buildOrRule(node, new RuleBuildContext(series, p, null, null));
    }

    private Rule buildNotRule(JsonNode node, BarSeries series,
                              Map<String, Map<String, Object>> p) {
        return buildNotRule(node, new RuleBuildContext(series, p, null, null));
    }

    private List<Rule> childRules(JsonNode node, BarSeries series,
                                  Map<String, Map<String, Object>> p) {
        return childRules(node, new RuleBuildContext(series, p, null, null));
    }

    // ── CONDITION ─────────────────────────────────────────────────────────────

    private Rule buildConditionRule(JsonNode cond, BarSeries series,
                                    Map<String, Map<String, Object>> params) {
        if (cond == null || cond.isNull()) return new BooleanRule(false);

        String indType    = cond.path("indicatorType").asText("");
        String condType   = cond.path("conditionType").asText("GT");
        String leftField  = cond.path("leftField").asText("NONE");
        String rightField = cond.path("rightField").asText("NONE");
        int    condPeriod = cond.path("period").asInt(-1);
        int    leftPeriod = cond.path("leftPeriod").asInt(-1);
        int    rightPeriod = cond.path("rightPeriod").asInt(-1);
        int    slopePeriod = cond.path("slopePeriod").asInt(3);
        int    holdDays    = cond.path("holdDays").asInt(3);

        // DSL 타입(STOCHASTIC 등) → DB 레지스트리 키(Stochastic 등) 변환 후 파라미터 조회
        String registryKey = toRegistryKey(indType);
        Map<String, Object> indParams = params != null
                ? params.getOrDefault(registryKey, Map.of())
                : Map.of();

        try {
            Indicator<Num> left  = resolveField(leftField,  indType, indParams, series, condPeriod, leftPeriod);
            Indicator<Num> right = resolveField(rightField, indType, indParams, series, condPeriod, rightPeriod);

            return switch (condType) {
                case "GT"       -> new OverIndicatorRule(left, right);
                case "LT"       -> new UnderIndicatorRule(left, right);
                // GTE/LTE: OverIndicator OR 동일 (epsilon 비교)
                case "GTE"      -> new OrRule(new OverIndicatorRule(left, right),
                                              buildEqRule(left, right, series));
                case "LTE"      -> new OrRule(new UnderIndicatorRule(left, right),
                                              buildEqRule(left, right, series));
                case "EQ"       -> buildEqRule(left, right, series);
                case "NEQ"      -> new NotRule(buildEqRule(left, right, series));
                // CrossedUp/Down: ta4j 0.22 에서는 2인자 생성자만 있음
                case "CROSS_UP"   -> new CrossedUpIndicatorRule(left, right);
                case "CROSS_DOWN" -> new CrossedDownIndicatorRule(left, right);
                case "SLOPE_UP"   -> new IsRisingRule(left, slopePeriod);
                case "SLOPE_DOWN" -> new IsFallingRule(left, slopePeriod);
                case "DIFF_GT"  -> {
                    double v = cond.path("compareValue").asDouble(0);
                    yield new OverIndicatorRule(
                            NumericIndicator.of(left).minus(right).abs(),
                            new ConstantIndicator<>(series, series.numFactory().numOf(v)));
                }
                case "DIFF_LT"  -> {
                    double v = cond.path("compareValue").asDouble(0);
                    yield new UnderIndicatorRule(
                            NumericIndicator.of(left).minus(right).abs(),
                            new ConstantIndicator<>(series, series.numFactory().numOf(v)));
                }
                case "HOLD_N_DAYS" -> buildHoldRule(new OverIndicatorRule(left, right), holdDays);
                // ── 일목균형표 전용 ────────────────────────────────────────────
                case "ABOVE_CLOUD"             -> buildCloudAboveRule(series, indParams);
                case "BELOW_CLOUD"             -> buildCloudBelowRule(series, indParams);
                case "IN_CLOUD"                -> buildInCloudRule(series, indParams);
                case "CLOUD_BREAK_UP"          -> buildCloudBreakRule(series, indParams, true);
                case "CLOUD_BREAK_DOWN"        -> buildCloudBreakRule(series, indParams, false);
                case "SPAN1_GT_SPAN2"          -> new OverIndicatorRule(ichimokuSpanA(series, indParams), ichimokuSpanB(series, indParams));
                case "SPAN1_LT_SPAN2"          -> new UnderIndicatorRule(ichimokuSpanA(series, indParams), ichimokuSpanB(series, indParams));
                case "SPAN1_CROSS_UP_SPAN2"    -> new CrossedUpIndicatorRule(ichimokuSpanA(series, indParams), ichimokuSpanB(series, indParams));
                case "SPAN1_CROSS_DOWN_SPAN2"  -> new CrossedDownIndicatorRule(ichimokuSpanA(series, indParams), ichimokuSpanB(series, indParams));
                case "LAGGING_GT_PRICE"        -> new OverIndicatorRule(ichimokuLagging(series, indParams), new ClosePriceIndicator(series));
                case "LAGGING_LT_PRICE"        -> new UnderIndicatorRule(ichimokuLagging(series, indParams), new ClosePriceIndicator(series));
                default -> {
                    log.warn("[Adapter] 미지원 conditionType: {}", condType);
                    yield new BooleanRule(false);
                }
            };
        } catch (Exception e) {
            log.warn("[Adapter] 조건 빌드 실패 ind={} cond={}: {}", indType, condType, e.getMessage());
            return new BooleanRule(false);
        }
    }

    // ── 필드 Resolver ─────────────────────────────────────────────────────────

    private Indicator<Num> resolveField(String field, String indType,
                                        Map<String, Object> p, BarSeries s,
                                        int condPeriod, int sidePeriod) {
        if (field == null || field.isBlank() || field.equals("NONE")) {
            return new ConstantIndicator<>(s, s.numFactory().numOf(0));
        }
        return switch (field) {
            case "CLOSE_PRICE"  -> new ClosePriceIndicator(s);
            case "OPEN_PRICE"   -> new OpenPriceIndicator(s);
            case "HIGH_PRICE"   -> new HighPriceIndicator(s);
            case "LOW_PRICE"    -> new LowPriceIndicator(s);
            case "VOLUME_VALUE" -> new VolumeIndicator(s, 1);
            default -> {
                double constant = resolveConstantField(field);
                if (!Double.isNaN(constant)) {
                    yield new ConstantIndicator<>(s, s.numFactory().numOf(constant));
                }
                yield resolveIndicatorField(field, indType, p, s, condPeriod, sidePeriod);
            }
        };
    }

    private double resolveConstantField(String field) {
        // K_ 접두사: 프론트엔드가 hline 실제값으로 생성한 상수 필드
        // 예) K_60 → 60, K_-100 → -100, K_0 → 0
        if (field.startsWith("K_")) {
            try { return Double.parseDouble(field.substring(2)); } catch (NumberFormatException e) { /* fall */ }
        }
        // NEG 접두사 (레거시): OVERSOLD_NEG80 → -80
        if (field.contains("_NEG")) {
            String[] parts = field.split("_NEG");
            try { return -Double.parseDouble(parts[parts.length - 1]); } catch (NumberFormatException e) { /* fall */ }
        }
        if (field.equals("ZERO_LINE")) return 0.0;
        // 마지막 _ 이후 숫자 추출 (레거시): OVERBOUGHT_70 → 70, ADX_25 → 25
        int idx = field.lastIndexOf('_');
        if (idx >= 0 && idx < field.length() - 1) {
            try { return Double.parseDouble(field.substring(idx + 1)); } catch (NumberFormatException e) { /* fall */ }
        }
        return Double.NaN;
    }

    private Indicator<Num> resolveIndicatorField(String field, String indType,
                                                  Map<String, Object> p, BarSeries s,
                                                  int condPeriod, int sidePeriod) {
        ClosePriceIndicator close = new ClosePriceIndicator(s);
        int periodOverride = sidePeriod > 0 ? sidePeriod : (condPeriod > 0 ? condPeriod : -1);

        // CCI — 기간 접미사(CCI_VALUE_13) 또는 기본 CCI_VALUE
        if (field.startsWith("CCI_VALUE_")) {
            int period = parseTrailingInt(field, "CCI_VALUE_", intP(p, "length", 13));
            return new CCIIndicator(s, period);
        }
        if (field.equals("CCI_VALUE") || indType.equals("CCI"))
            return new CCIIndicator(s, periodOverride > 0 ? periodOverride : intP(p, "length", 13));
        // RSI — 기간 접미사(RSI_VALUE_9) 또는 기본 RSI_VALUE
        if (field.startsWith("RSI_VALUE_")) {
            int period = parseTrailingInt(field, "RSI_VALUE_", intP(p, "length", 14));
            return new RSIIndicator(close, period);
        }
        if (field.equals("RSI_VALUE"))
            return new RSIIndicator(close, periodOverride > 0 ? periodOverride : intP(p, "length", 14));
        // MACD
        if (field.equals("MACD_LINE")) {
            return new MACDIndicator(close, intP(p, "fastLength", 12), intP(p, "slowLength", 26));
        }
        if (field.equals("SIGNAL_LINE")) {
            return new EMAIndicator(
                    new MACDIndicator(close, intP(p, "fastLength", 12), intP(p, "slowLength", 26)),
                    intP(p, "signalLength", 9));
        }
        if (field.equals("HISTOGRAM")) {
            MACDIndicator macd = new MACDIndicator(close, intP(p, "fastLength", 12), intP(p, "slowLength", 26));
            return NumericIndicator.of(macd).minus(new EMAIndicator(macd, intP(p, "signalLength", 9)));
        }
        // ADX / DMI
        if (field.startsWith("ADX_VALUE_")) {
            int period = parseTrailingInt(field, "ADX_VALUE_", intP(p, "adxSmoothing", 14));
            return new ADXIndicator(s, intP(p, "diLength", 14), period);
        }
        if (field.equals("ADX_VALUE"))
            return new ADXIndicator(s, intP(p, "diLength", 14), intP(p, "adxSmoothing", 14));
        if (field.equals("PDI") || field.equals("PLUS_DI")) {
            int diLen = intP(p, "diLength", intP(p, "length", 14));
            return new PlusDIIndicator(s, diLen);
        }
        if (field.equals("MDI") || field.equals("MINUS_DI")) {
            int diLen = intP(p, "diLength", intP(p, "length", 14));
            return new MinusDIIndicator(s, diLen);
        }
        // Stochastic Slow — IndicatorService.calcStochastic 과 동일한 로직:
        //   raw %K = StochasticOscillatorKIndicator(kLen)
        //   slow %K = SMA(raw %K, smooth)
        //   %D      = SMA(slow %K, dSmoothing)
        if (field.equals("STOCH_K")) {
            int kLen   = intP(p, "kLength",    14);
            int smooth = intP(p, "smooth",      3);
            return new SMAIndicator(new StochasticOscillatorKIndicator(s, kLen), smooth);
        }
        if (field.equals("STOCH_D")) {
            int kLen    = intP(p, "kLength",    14);
            int smooth  = intP(p, "smooth",      3);
            int dSmooth = intP(p, "dSmoothing",  3);
            SMAIndicator slowK = new SMAIndicator(new StochasticOscillatorKIndicator(s, kLen), smooth);
            return new SMAIndicator(slowK, dSmooth);
        }
        // TRIX — ln(종가) → 3×EMA → Δ×10000 (업비트)
        if (field.startsWith("TRIX_VALUE_")) {
            int len = parseTrailingInt(field, "TRIX_VALUE_", intP(p, "length", 12));
            EMAIndicator e3 = tripleLogEma(close, len);
            PreviousValueIndicator prev3 = new PreviousValueIndicator(e3);
            return NumericIndicator.of(e3).minus(prev3).multipliedBy(10_000);
        }
        if (field.equals("TRIX_VALUE")) {
            int len = intP(p, "length", 12);
            EMAIndicator e3 = tripleLogEma(close, len);
            PreviousValueIndicator prev3 = new PreviousValueIndicator(e3);
            return NumericIndicator.of(e3).minus(prev3).multipliedBy(10_000);
        }
        if (field.equals("TRIX_SIGNAL")) {
            int len = intP(p, "length", 12);
            EMAIndicator e3 = tripleLogEma(close, len);
            PreviousValueIndicator prev3 = new PreviousValueIndicator(e3);
            NumericIndicator trix = NumericIndicator.of(e3).minus(prev3).multipliedBy(10_000);
            return new EMAIndicator(trix, intP(p, "signalLength", 9));
        }
        // OBV
        if (field.equals("OBV_LINE"))   return new OnBalanceVolumeIndicator(s);
        if (field.equals("OBV_SIGNAL")) {
            OnBalanceVolumeIndicator obv = new OnBalanceVolumeIndicator(s);
            String maType = p != null ? p.getOrDefault("maType", "SMA").toString() : "SMA";
            int maLen = intP(p, "maLength", 9);
            return switch (maType) {
                case "EMA" -> new EMAIndicator(obv, maLen);
                case "WMA" -> new WMAIndicator(obv, maLen);
                default    -> new SMAIndicator(obv, maLen);
            };
        }
        // VR
        if (field.startsWith("VR_VALUE_")) {
            int period = parseTrailingInt(field, "VR_VALUE_", intP(p, "length", 10));
            return vrIndicator(s, period);
        }
        if (field.equals("VR_VALUE") || indType.equals("VR"))
            return vrIndicator(s, intP(p, "length", 10));
        // 이격도 DISPARITY5 → period 5
        if (field.startsWith("DISPARITY")) {
            int period = parseTrailingInt(field, "DISPARITY", 20);
            return disparityIndicator(s, period, p);
        }
        if (field.startsWith("PSY_VALUE_")) {
            int period = parseTrailingInt(field, "PSY_VALUE_", intP(p, "length", 12));
            return newPsychIndicator(s, period, false);
        }
        if (field.equals("PSY_VALUE") || field.equals("NEW_PSY_VALUE")
                || indType.equals("PSYCHOLOGICAL") || indType.equals("NEW_PSYCHOLOGICAL"))
            return newPsychIndicator(s, intP(p, "length", 12), false);
        if (field.startsWith("INVEST_PSY_VALUE_")) {
            int period = parseTrailingInt(field, "INVEST_PSY_VALUE_", intP(p, "length", 10));
            return newPsychIndicator(s, period, true);
        }
        if (field.equals("INVEST_PSY_VALUE") || indType.equals("INVEST_PSYCHOLOGICAL"))
            return newPsychIndicator(s, intP(p, "length", 10), true);
        // Williams %R
        if (field.startsWith("WILLIAMS_R_VALUE_")) {
            int period = parseTrailingInt(field, "WILLIAMS_R_VALUE_", intP(p, "length", 14));
            return new WilliamsRIndicator(s, period);
        }
        if (field.equals("WILLIAMS_R_VALUE")) return new WilliamsRIndicator(s, intP(p, "length", 14));
        // Donchian Channel — DC_UPPER_20 / DC_LOWER_10 등 기간 접미사
        if (field.startsWith("DC_UPPER_")) {
            int period = parseTrailingInt(field, "DC_UPPER_", intP(p, "length", 20));
            return new DonchianChannelUpperIndicator(s, period);
        }
        if (field.startsWith("DC_LOWER_")) {
            int period = parseTrailingInt(field, "DC_LOWER_", intP(p, "length", 20));
            return new DonchianChannelLowerIndicator(s, period);
        }
        if (field.startsWith("DC_MIDDLE_")) {
            int period = parseTrailingInt(field, "DC_MIDDLE_", intP(p, "length", 20));
            return new DonchianChannelMiddleIndicator(s, period);
        }
        // Bollinger Bands
        if (field.equals("UPPER_BAND") || field.equals("LOWER_BAND") || field.equals("MIDDLE_BAND")) {
            int len = intP(p, "length", 20);
            double mult = dblP(p, "mult", 2.0);
            String maType = p.getOrDefault("maType", "SMA").toString();
            Indicator<Num> basis = maOfSource(close, s, maType, len);
            StandardDeviationIndicator sd = StandardDeviationIndicator.ofSample(close, len);
            BollingerBandsMiddleIndicator mid = new BollingerBandsMiddleIndicator(basis);
            Num k = s.numFactory().numOf(mult);
            return switch (field) {
                case "UPPER_BAND" -> new BollingerBandsUpperIndicator(mid, sd, k);
                case "LOWER_BAND" -> new BollingerBandsLowerIndicator(mid, sd, k);
                default           -> mid;
            };
        }
        // MA (SMA)
        if (field.startsWith("MA") && !field.startsWith("MACD")) {
            try { return new SMAIndicator(close, Integer.parseInt(field.substring(2))); }
            catch (NumberFormatException ignored) { /* fall */ }
        }
        if (field.startsWith("EMA")) {
            try { return new EMAIndicator(close, Integer.parseInt(field.substring(3))); }
            catch (NumberFormatException ignored) { /* fall */ }
        }
        // 일목균형표
        if (field.equals("CONVERSION_LINE")) return ichimokuTenkan(s, p);
        if (field.equals("BASE_LINE"))       return ichimokuKijun(s, p);
        if (field.equals("LEADING_SPAN1"))   return ichimokuSpanA(s, p);
        if (field.equals("LEADING_SPAN2"))   return ichimokuSpanB(s, p);
        if (field.equals("LAGGING_SPAN"))    return ichimokuLagging(s, p);
        // 거래량 MA
        if (field.equals("VOLUME_MA")) return new SMAIndicator(new VolumeIndicator(s, 1), intP(p, "length", 20));

        log.warn("[Adapter] 미지원 필드: {} (indicatorType={})", field, indType);
        return new ClosePriceIndicator(s);
    }

    // ── 일목균형표 지표 빌더 ──────────────────────────────────────────────────

    private IchimokuTenkanSenIndicator ichimokuTenkan(BarSeries s, Map<String, Object> p) {
        return new IchimokuTenkanSenIndicator(s, intP(p, "conversionPeriods", 9));
    }
    private IchimokuKijunSenIndicator ichimokuKijun(BarSeries s, Map<String, Object> p) {
        return new IchimokuKijunSenIndicator(s, intP(p, "basePeriods", 26));
    }
    private IchimokuSenkouSpanAIndicator ichimokuSpanA(BarSeries s, Map<String, Object> p) {
        return new IchimokuSenkouSpanAIndicator(s, ichimokuTenkan(s, p), ichimokuKijun(s, p),
                intP(p, "displacement", 26));
    }
    private IchimokuSenkouSpanBIndicator ichimokuSpanB(BarSeries s, Map<String, Object> p) {
        // IndicatorService 와 동일하게 displacement 파라미터 반영
        return new IchimokuSenkouSpanBIndicator(s, intP(p, "laggingSpan2Periods", 52),
                intP(p, "displacement", 26));
    }
    private IchimokuChikouSpanIndicator ichimokuLagging(BarSeries s, Map<String, Object> p) {
        return new IchimokuChikouSpanIndicator(s, intP(p, "displacement", 26));
    }

    /** 종가가 구름 위 */
    private Rule buildCloudAboveRule(BarSeries s, Map<String, Object> p) {
        NumericIndicator cloudTop = NumericIndicator.of(ichimokuSpanA(s, p)).max(ichimokuSpanB(s, p));
        return new OverIndicatorRule(new ClosePriceIndicator(s), cloudTop);
    }
    /** 종가가 구름 아래 */
    private Rule buildCloudBelowRule(BarSeries s, Map<String, Object> p) {
        NumericIndicator cloudBottom = NumericIndicator.of(ichimokuSpanA(s, p)).min(ichimokuSpanB(s, p));
        return new UnderIndicatorRule(new ClosePriceIndicator(s), cloudBottom);
    }
    /** 종가가 구름 안 */
    private Rule buildInCloudRule(BarSeries s, Map<String, Object> p) {
        ClosePriceIndicator close = new ClosePriceIndicator(s);
        NumericIndicator cloudTop    = NumericIndicator.of(ichimokuSpanA(s, p)).max(ichimokuSpanB(s, p));
        NumericIndicator cloudBottom = NumericIndicator.of(ichimokuSpanA(s, p)).min(ichimokuSpanB(s, p));
        return new AndRule(
                new OrRule(new OverIndicatorRule(close, cloudBottom),
                           buildEqRule(close, cloudBottom, s)),
                new OrRule(new UnderIndicatorRule(close, cloudTop),
                           buildEqRule(close, cloudTop, s)));
    }
    /** 구름 상향(true)/하향(false) 돌파 */
    private Rule buildCloudBreakRule(BarSeries s, Map<String, Object> p, boolean up) {
        NumericIndicator cloudTop    = NumericIndicator.of(ichimokuSpanA(s, p)).max(ichimokuSpanB(s, p));
        NumericIndicator cloudBottom = NumericIndicator.of(ichimokuSpanA(s, p)).min(ichimokuSpanB(s, p));
        ClosePriceIndicator close = new ClosePriceIndicator(s);
        if (up) return new CrossedUpIndicatorRule(close, cloudTop);
        else    return new CrossedDownIndicatorRule(close, cloudBottom);
    }

    // ── 범용 Rule 빌더 ────────────────────────────────────────────────────────

    /** |a - b| < epsilon → equal */
    private Rule buildEqRule(Indicator<Num> a, Indicator<Num> b, BarSeries series) {
        NumericIndicator diff = NumericIndicator.of(a).minus(b).abs();
        return new UnderIndicatorRule(diff,
                new ConstantIndicator<>(series, series.numFactory().numOf(1e-9)));
    }

    /** N봉 연속으로 inner 규칙이 성립 */
    private Rule buildHoldRule(Rule inner, int n) {
        if (n <= 1) return inner;
        return new Rule() {
            @Override
            public boolean isSatisfied(int index, TradingRecord record) {
                if (index < n - 1) return false;
                for (int i = index - n + 1; i <= index; i++) {
                    if (!inner.isSatisfied(i, record)) return false;
                }
                return true;
            }
        };
    }

    // ── TRIX 헬퍼 ─────────────────────────────────────────────────────────────

    private EMAIndicator tripleLogEma(ClosePriceIndicator close, int len) {
        Indicator<Num> logClose = UnaryOperationIndicator.log(close);
        return new EMAIndicator(new EMAIndicator(new EMAIndicator(logClose, len), len), len);
    }

    private Indicator<Num> maOfSource(Indicator<Num> source, BarSeries s, String maType, int len) {
        return switch (maType) {
            case "EMA"  -> new EMAIndicator(source, len);
            case "WMA"  -> new WMAIndicator(source, len);
            case "SMMA (RMA)", "RMA" -> new WildersMAIndicator(source, len);
            default     -> new SMAIndicator(source, len);
        };
    }

    private int parseTrailingInt(String field, String prefix, int def) {
        if (!field.startsWith(prefix)) return def;
        try {
            return Integer.parseInt(field.substring(prefix.length()));
        } catch (NumberFormatException e) {
            return def;
        }
    }

    /** 이격도 = 종가 / SMA(period) * 100 */
    private Indicator<Num> disparityIndicator(BarSeries s, int period, Map<String, Object> p) {
        ClosePriceIndicator close = new ClosePriceIndicator(s);
        SMAIndicator sma = new SMAIndicator(close, period);
        return NumericIndicator.of(close).dividedBy(sma).multipliedBy(100);
    }

    private Indicator<Num> vrIndicator(BarSeries s, int period) {
        return new VrIndicator(s, period);
    }

    private Indicator<Num> newPsychIndicator(BarSeries s, int period, boolean volumeWeighted) {
        return new PsychologicalIndicator(s, period, volumeWeighted);
    }

    /** VR(거래량비율) = 상승일 거래량합 / 하락일 거래량합 × 100 */
    private static final class VrIndicator extends CachedIndicator<Num> {
        private final int period;
        private final ClosePriceIndicator close;
        private final VolumeIndicator volume;

        VrIndicator(BarSeries series, int period) {
            super(series);
            this.period = period;
            this.close = new ClosePriceIndicator(series);
            this.volume = new VolumeIndicator(series, 1);
        }

        @Override
        protected Num calculate(int index) {
            if (index < period) return null;
            double upVol = 0, downVol = 0;
            for (int j = index - period + 1; j <= index; j++) {
                Num c = close.getValue(j);
                Num cPrev = close.getValue(j - 1);
                Num v = volume.getValue(j);
                if (c == null || cPrev == null || v == null) continue;
                if (c.isGreaterThan(cPrev)) upVol += v.doubleValue();
                else if (c.isLessThan(cPrev)) downVol += v.doubleValue();
            }
            if (downVol == 0) return null;
            return getBarSeries().numFactory().numOf(upVol / downVol * 100.0);
        }

        @Override
        public int getCountOfUnstableBars() {
            return period + 1;
        }
    }

    /** 신심리도 / 투자심리도 */
    private static final class PsychologicalIndicator extends CachedIndicator<Num> {
        private final int period;
        private final boolean volumeWeighted;
        private final ClosePriceIndicator close;
        private final VolumeIndicator volume;

        PsychologicalIndicator(BarSeries series, int period, boolean volumeWeighted) {
            super(series);
            this.period = period;
            this.volumeWeighted = volumeWeighted;
            this.close = new ClosePriceIndicator(series);
            this.volume = new VolumeIndicator(series, 1);
        }

        @Override
        protected Num calculate(int index) {
            if (index < period) return null;
            if (!volumeWeighted) {
                int up = 0;
                for (int j = index - period + 1; j <= index; j++) {
                    Num c = close.getValue(j);
                    Num cPrev = close.getValue(j - 1);
                    if (c != null && cPrev != null && c.isGreaterThan(cPrev)) up++;
                }
                return getBarSeries().numFactory().numOf((double) up / period * 100.0);
            }
            double upVol = 0, total = 0;
            for (int j = index - period + 1; j <= index; j++) {
                Num c = close.getValue(j);
                Num cPrev = close.getValue(j - 1);
                Num v = volume.getValue(j);
                if (v == null) continue;
                total += v.doubleValue();
                if (c != null && cPrev != null && c.isGreaterThan(cPrev)) upVol += v.doubleValue();
            }
            if (total == 0) return null;
            return getBarSeries().numFactory().numOf(upVol / total * 100.0);
        }

        @Override
        public int getCountOfUnstableBars() {
            return period + 1;
        }
    }

    // ── 파라미터 헬퍼 ─────────────────────────────────────────────────────────

    private int intP(Map<String, Object> p, String k, int def) {
        if (p == null) return def;
        Object v = p.get(k);
        return v == null ? def : ((Number) v).intValue();
    }

    private double dblP(Map<String, Object> p, String k, double def) {
        if (p == null) return def;
        Object v = p.get(k);
        return v == null ? def : ((Number) v).doubleValue();
    }
}
