/** Shared strategy editor utilities — extracted from StrategyPage */
import React from 'react';
import type { IndicatorConfig } from '../types/index';
import { getIndicatorDef, getHLineLabel, INDICATOR_REGISTRY, type PlotDef, type HLineDef } from '../utils/indicatorRegistry';
import type { LogicNode, LogicNodeType, ConditionDSL, CandleRangeMode } from '../utils/strategyTypes';
import {
  CONDITION_LABEL,
  INDICATOR_CONDITIONS,
  resolveCandleRangeMode,
  candleRangeWindowBars,
  formatCandleRangeClause,
} from '../utils/strategyTypes';
import {
  appendLegacyMaFieldOption,
  buildEmaFieldOptions,
  buildMaFieldOptions,
} from './maDslField';
import {
  COMPOSITE_INDICATOR_ITEMS,
  compositeDisplayName,
  compositeElementLabel,
  compositeFieldLabel,
  makeCompositeCondition,
  normalizeCompositeCondition,
  parseDonchianPeriod,
  parsePeriodFromCompositeField,
  switchCompositeIndicatorType,
  syncCompositeFields,
} from '../utils/compositeIndicators';
import {
  isPriceExtremeIndicator,
  nhPriorField,
  nlPriorField,
  parsePriorExtremePeriod,
  syncPriceExtremeSimpleFields,
} from '../utils/priceExtremeIndicators';
import { getStrategyIndicatorDisplayName } from '../utils/strategyPaletteStorage';
import {
  buildInflection33Tree,
  isInflection33PairRoot,
  isInflection33PaletteValue,
  inflection33DisplayName,
  inflection33SummaryText,
  inferInflection33FilterLevel,
  inflection33FilterLevelLabel,
} from '../utils/inflection33Strategy';
import {
  buildStableStrategyTree,
  isStableStrategyPairRoot,
  isStableStrategyPaletteValue,
  stableStrategyDisplayName,
  stableStrategySummaryText,
  inferStableStrategyFilterLevel,
  stableStrategyFilterLevelLabel,
  type StableStrategyId,
} from '../utils/stableStrategyPairs';
import {
  buildPriceExtremeBreakoutTree,
  isPriceExtremeBreakoutPairPaletteValue,
  isPriceExtremeBreakoutPairRoot,
  priceExtremePairDisplayName,
  priceExtremePairSummaryText,
  inferPriceExtremeFilterLevel,
  filterLevelLabel,
} from '../utils/priceExtremeBreakoutPair';
import {
  buildStochOverboughtPairTree,
  isStochOverboughtPairPaletteValue,
  isStochOverboughtPairRoot,
  STOCH_PAIR_SECONDARY_OPTIONS,
  stochPairDisplayName,
  stochPairSummaryText,
} from '../utils/stochOverboughtPair';
import ComboFieldSelect from '../components/strategyEditor/ComboFieldSelect';
import {
  getCompositeFieldOpts,
  getCompositePeriodPresetOptions,
  getConditionRightPeriod,
  getConditionThreshold,
  getChartReferenceThreshold,
  getConditionValuePeriod,
  getDefaultIndicatorPeriod,
  getPeriodPresetOptions,
  getThresholdBounds,
  getThresholdPresetOptions,
  initConditionPeriodsInherit,
  isThresholdOverridden,
  isValuePeriodFieldStored,
  parseThresholdField,
  resolveFieldCustomKind,
  getCompositeLeftCandleType,
  getCompositeRightCandleType,
  setCompositeLeftCandleType,
  setCompositeRightCandleType,
  setConditionThreshold,
  setConditionRightPeriod,
  setConditionValuePeriod,
} from '../utils/conditionPeriods';
import {
  THRESHOLD_HL_MID,
  THRESHOLD_HL_OVER,
  THRESHOLD_HL_UNDER,
  isThresholdFieldOrSymbol,
  isThresholdSymbol,
  resolveInheritedThresholdField,
} from '../utils/thresholdSymbols';
import { STRATEGY_CANDLE_TYPE_OPTIONS } from '../utils/strategyStartNodes';

import type { StrategyFlowLayoutStore } from './strategyEditorLayoutStorage';
import { genId } from './strategyNodeIds';

export { genId };

export interface StrategyDto {
  id:            number;
  name:          string;
  description?:  string;
  buyCondition?:  LogicNode | null;
  sellCondition?: LogicNode | null;
  flowLayout?:   StrategyFlowLayoutStore;
  enabled:       boolean;
  createdAt?:    string;
  updatedAt?:    string;
}

export interface ValidationResult {
  isValid:   boolean;
  errors:    { message: string }[];
  warnings:  { message: string }[];
}

export type DefType = typeof DEF_DEFAULTS;

/** @deprecated DB(gc_strategy)만 사용 */
export const loadStratsLocal = (): StrategyDto[] => [];
/** @deprecated */
export const saveStratsLocal = (_s: StrategyDto[]) => { /* no-op */ };

// ─── 기본 파라미터 (IndicatorSettings 미사용 → 하드코딩 기본값) ─────────────
/** 지표별 hline 임계값 (과열선/중앙선/침체선) */
interface HLThresh { over?: number; mid?: number; under?: number; }

/** 하드코딩 기본값 — activeIndicators가 없을 때 폴백 */
export const DEF_DEFAULTS = {
  rsiPeriod: 9,
  macdFast: 12, macdSlow: 26, macdSignal: 9,
  cciPeriod: 13,
  stochK: 12, stochD: 5,
  adxPeriod: 14,
  trixPeriod: 12, trixSignal: 9,
  dmiPeriod: 14,
  obvPeriod: 9, obvSignal: 9,
  bbPeriod: 20, bbStdDev: 2,
  ichTenkan: 9, ichKijun: 26, ichSenkouB: 52,
  newPsy: 10, psyPeriod: 12, investPsy: 10,
  williamsR: 14,
  bwiPeriod: 20,
  vrPeriod: 10,
  volOscShort: 5, volOscLong: 10,
  dispUltra: 5, dispShort: 10, dispMid: 20, dispLong: 60,
  maLines: [5, 10, 20, 60, 120] as number[],
  /** 지표별 hline 임계값 (과열선/중앙선/침체선) */
  hlThresh: {
    RSI:                 { over: 70,   mid: 50,  under: 30   },
    STOCHASTIC:          { over: 80,   mid: 50,  under: 20   },
    CCI:                 { over: 100,  mid: 0,   under: -100 },
    WILLIAMS_R:          { over: -20,  mid: -50, under: -80  },
    BWI:                 { over: 80,   mid: 50,  under: 20   },
    VR:                  { over: 200,  mid: 100, under: 50   },
    PSYCHOLOGICAL:         { over: 75,   mid: 50,  under: 25   },
    NEW_PSYCHOLOGICAL:     { over: 50,   mid: 0,   under: -50  },
    INVEST_PSYCHOLOGICAL:  { over: 75,   mid: 50,  under: 25   },
    MACD:                { mid: 0                             },
    ADX:                 { over: 40,   mid: 25,  under: 20   },
    DMI:                 { over: 30,   mid: 20,  under: 10   },
    TRIX:                { mid: 0                             },
    VOLUME_OSC:          { mid: 0                             },
  } as Record<string, HLThresh>,
};
/**
 * activeIndicators에서 파라미터를 추출해 DEF를 구성.
 * 레지스트리 type명 / params 키는 indicatorRegistry.ts 의 defaultParams 기준.
 * (레거시 투자전략 화면용 — 차트 활성 지표와 동기화)
 */
export function buildDef(activeIndicators: IndicatorConfig[]): DefType {
  // params 객체 참조 공유 방지
  const p = (registryType: string) => {
    const raw = activeIndicators.find(i => i.type === registryType)?.params;
    return raw ? { ...raw } : {};
  };
  const num = (params: Record<string, number | string | boolean>, key: string, fallback: number) =>
    typeof params[key] === 'number' ? (params[key] as number) : fallback;

  // ── 각 지표별 실제 레지스트리 type명 + params 키 ──────────────────────────
  // RSI           → type:'RSI',              params.length
  const rsi    = p('RSI');
  // MACD          → type:'MACD',             params.fastLength / slowLength / signalLength
  const macd   = p('MACD');
  // CCI           → type:'CCI',              params.length
  const cci    = p('CCI');
  // Stochastic    → type:'Stochastic',       params.kLength / dSmoothing
  const stoch  = p('Stochastic');
  // ADX           → type:'ADX',              params.adxSmoothing / diLength
  const adx    = p('ADX');
  // TRIX          → type:'TRIX',             params.length / signalLength
  const trix   = p('TRIX');
  // DMI           → type:'DMI',              params.diLength / adxSmoothing
  const dmi    = p('DMI');
  // BollingerBands→ type:'BollingerBands',   params.length / mult
  const bb     = p('BollingerBands');
  // IchimokuCloud → type:'IchimokuCloud',    params.conversionPeriods / basePeriods / laggingSpan2Periods
  const ich    = p('IchimokuCloud');
  // Williams %R   → type:'WilliamsPercentRange', params.length
  const wr     = p('WilliamsPercentRange');
  const vo     = p('VolumeOscillator');
  const vrInd  = p('VR');
  const disp   = p('Disparity');
  const psy    = p('Psychological');
  const nPsy   = p('NewPsychological');
  const iPsy   = p('InvestPsychological');
  const obvP   = p('OBV');
  // SMA (MA lines)→ type:'SMA', params keys depend on smaConfig
  const sma    = p('SMA');

  // MA 기간 배열: SMA 설정에서 period1~period11 추출 (smaConfig 구조)
  let maLines = DEF_DEFAULTS.maLines;
  const smaPeriods = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]
    .map(i => sma[`period${i}`])
    .filter(v => typeof v === 'number' && (v as number) > 0) as number[];
  if (smaPeriods.length >= 2) maLines = smaPeriods;

  // ── hline 임계값 추출 ───────────────────────────────────────────────────────
  // DSL 지표타입 → 레지스트리 type명 매핑
  const DSL_TO_REGISTRY: Record<string, string> = {
    RSI:                  'RSI',
    STOCHASTIC:           'Stochastic',
    CCI:                  'CCI',
    WILLIAMS_R:           'WilliamsPercentRange',
    MACD:                 'MACD',
    DMI:                  'DMI',
    ADX:                  'ADX',
    TRIX:                 'TRIX',
    VOLUME_OSC:           'VolumeOscillator',
    VR:                   'VR',
    DISPARITY:            'Disparity',
    PSYCHOLOGICAL:        'Psychological',
    NEW_PSYCHOLOGICAL:    'NewPsychological',
    INVEST_PSYCHOLOGICAL: 'InvestPsychological',
    OBV:                  'OBV',
    BOLLINGER:            'BollingerBands',
    DONCHIAN:             'DonchianChannels',
    NEW_HIGH:             'PriceExtreme',
    NEW_LOW:              'PriceExtreme',
  };

  const extractThresh = (dslType: string): HLThresh => {
    const regType = DSL_TO_REGISTRY[dslType];
    if (!regType) return DEF_DEFAULTS.hlThresh[dslType] ?? {};
    const config  = activeIndicators.find(i => i.type === regType);
    const defHl   = getIndicatorDef(regType)?.hlines ?? [];
    const hlines  = config?.hlines ?? defHl;
    if (hlines.length === 0) return DEF_DEFAULTS.hlThresh[dslType] ?? {};

    const prices = hlines.map(h => h.price);
    const find = (lbl: string) => hlines.find(h =>
      (h.label ?? getHLineLabel(h.price, prices)) === lbl
    );
    const result: HLThresh = {};
    const ov = find('과열선'); if (ov) result.over  = ov.price;
    const md = find('중앙선') ?? find('기준선'); if (md) result.mid   = md.price;
    const un = find('침체선'); if (un) result.under = un.price;
    // 값이 하나도 없으면 기본값 사용
    if (result.over === undefined && result.mid === undefined && result.under === undefined)
      return DEF_DEFAULTS.hlThresh[dslType] ?? {};
    return {
      ...DEF_DEFAULTS.hlThresh[dslType],
      ...result,
    };
  };

  const hlThresh: Record<string, HLThresh> = {};
  for (const dslType of Object.keys(DEF_DEFAULTS.hlThresh)) {
    hlThresh[dslType] = extractThresh(dslType);
  }

  return {
    rsiPeriod:   num(rsi,   'length',              DEF_DEFAULTS.rsiPeriod),
    macdFast:    num(macd,  'fastLength',           DEF_DEFAULTS.macdFast),
    macdSlow:    num(macd,  'slowLength',           DEF_DEFAULTS.macdSlow),
    macdSignal:  num(macd,  'signalLength',         DEF_DEFAULTS.macdSignal),
    cciPeriod:   num(cci,   'length',              DEF_DEFAULTS.cciPeriod),
    stochK:      num(stoch, 'kLength',             DEF_DEFAULTS.stochK),
    stochD:      num(stoch, 'dSmoothing',          DEF_DEFAULTS.stochD),
    adxPeriod:   num(adx,   'adxSmoothing',        DEF_DEFAULTS.adxPeriod),
    trixPeriod:  num(trix,  'length',              DEF_DEFAULTS.trixPeriod),
    trixSignal:  num(trix,  'signalLength',        DEF_DEFAULTS.trixSignal),
    dmiPeriod:   num(dmi,   'diLength',            num(dmi, 'length', DEF_DEFAULTS.dmiPeriod)),
    obvPeriod:   num(obvP,  'maLength',            DEF_DEFAULTS.obvPeriod),
    obvSignal:   num(obvP,  'maLength',            DEF_DEFAULTS.obvSignal),
    bbPeriod:    num(bb,    'length',              DEF_DEFAULTS.bbPeriod),
    bbStdDev:    num(bb,    'mult',                DEF_DEFAULTS.bbStdDev),
    ichTenkan:   num(ich,   'conversionPeriods',   DEF_DEFAULTS.ichTenkan),
    ichKijun:    num(ich,   'basePeriods',         DEF_DEFAULTS.ichKijun),
    ichSenkouB:  num(ich,   'laggingSpan2Periods', DEF_DEFAULTS.ichSenkouB),
    psyPeriod:   num(psy,   'length',              DEF_DEFAULTS.psyPeriod),
    newPsy:      num(nPsy,  'length',              DEF_DEFAULTS.newPsy),
    investPsy:   num(iPsy,  'length',              DEF_DEFAULTS.investPsy),
    williamsR:   num(wr,    'length',              DEF_DEFAULTS.williamsR),
    bwiPeriod:   DEF_DEFAULTS.bwiPeriod,   // BWI는 레지스트리 미등록
    vrPeriod:    num(vrInd, 'length',              DEF_DEFAULTS.vrPeriod),
    volOscShort: num(vo,    'shortLength',         DEF_DEFAULTS.volOscShort),
    volOscLong:  num(vo,    'longLength',          DEF_DEFAULTS.volOscLong),
    dispUltra:   num(disp,  'ultraLength',         DEF_DEFAULTS.dispUltra),
    dispShort:   num(disp,  'shortLength',         DEF_DEFAULTS.dispShort),
    dispMid:     num(disp,  'midLength',           DEF_DEFAULTS.dispMid),
    dispLong:    num(disp,  'longLength',          DEF_DEFAULTS.dispLong),
    maLines,
    hlThresh,
  };
}

/**
 * 전략편집기 DEF — DB 보조지표 설정(파라미터·hline)에서 구성.
 * 조건 노드 기본값은 valuePeriodOverride/thresholdOverride=false 일 때 여기서 상속.
 */
export function buildStrategyEditorDefFromSettings(
  getParams: (
    type: string,
    defaults?: Record<string, number | string | boolean>,
  ) => Record<string, number | string | boolean>,
  getVisualConfig: (
    type: string,
    defaultPlots?: PlotDef[],
    defaultHlines?: HLineDef[],
  ) => { plots: PlotDef[]; hlines: HLineDef[]; cloudColors?: unknown },
): DefType {
  const configs: IndicatorConfig[] = [];
  for (const regDef of INDICATOR_REGISTRY) {
    const type = regDef.type;
    const params = getParams(type, regDef.defaultParams as Record<string, number | string | boolean>);
    const { plots, hlines, cloudColors } = getVisualConfig(type, regDef.plots, regDef.hlines);
    configs.push({
      id: `se-def-${type}`,
      type,
      params: { ...params },
      plots: plots.map(p => ({ ...p })),
      hlines: hlines.map(h => ({ ...h })),
      ...(cloudColors ? { cloudColors } : {}),
    } as IndicatorConfig);
  }
  return buildDef(configs);
}

/** @deprecated buildStrategyEditorDefFromSettings 사용 — 하드코딩 폴백 */
export function buildDefFromGetParams(
  getParams: (type: string, defaults?: Record<string, number | string | boolean>) => Record<string, number | string | boolean>,
): DefType {
  const types = [
    'RSI', 'MACD', 'CCI', 'Stochastic', 'ADX', 'TRIX', 'DMI', 'BollingerBands',
    'IchimokuCloud', 'WilliamsPercentRange', 'VolumeOscillator', 'VR', 'Disparity',
    'Psychological', 'NewPsychological', 'InvestPsychological', 'OBV', 'SMA', 'EMA',
  ] as const;
  const configs: IndicatorConfig[] = types.map(type => ({
    id: `eval-def-${type}`,
    type,
    params: getParams(type, getIndicatorDef(type)?.defaultParams as Record<string, number | string | boolean>),
    plots: getIndicatorDef(type)?.plots ?? [],
  }));
  return buildDef(configs);
}

export function buildStrategyEditorDef(): DefType {
  return buildDef([]);
}

// ─── 공통 조건 옵션 ───────────────────────────────────────────────────────────
const COND_OPTIONS = [
  { v: 'GT',           l: '초과 (>)'        },
  { v: 'LT',           l: '미만 (<)'        },
  { v: 'GTE',          l: '이상 (>=)'       },
  { v: 'LTE',          l: '이하 (<=)'       },
  { v: 'EQ',           l: '같음 (==)'       },
  { v: 'NEQ',          l: '다름 (!=)'       },
  { v: 'CROSS_UP',     l: '상향 돌파'       },
  { v: 'CROSS_DOWN',   l: '하향 돌파'       },
  { v: 'SLOPE_UP',     l: '상승 기울기'     },
  { v: 'SLOPE_DOWN',   l: '하락 기울기'     },
  { v: 'DIFF_GT',      l: '차이 > 값'       },
  { v: 'DIFF_LT',      l: '차이 < 값'       },
  { v: 'HOLD_N_DAYS',  l: 'N일 연속 유지'  },
  { v: 'LOWEST_LTE',   l: 'N봉 최저 ≤ 값'  },
  { v: 'LOWEST_GTE',   l: 'N봉 최저 ≥ 값'  },
];

const condLabelFromRegistry = (ind: string, v: string): string => {
  const fromCond = COND_OPTIONS.concat(ICHIMOKU_CONDS).find(o => o.v === v)?.l;
  if (fromCond) return fromCond;
  return CONDITION_LABEL[v] ?? v;
};

type CondTypeOpt = { v: string; l: string };

/** 지표별 조건 타입 드롭다운 — 오실레이터는 룩백 조건 포함 */
export const getCondOptionsForIndicator = (ind: string): CondTypeOpt[] => {
  if (ind === 'ICHIMOKU') return ICHIMOKU_CONDS;
  const keys = INDICATOR_CONDITIONS[ind] ?? COMMON_COND_KEYS;
  return keys.map(v => ({ v, l: condLabelFromRegistry(ind, v) }));
};

const COMMON_COND_KEYS = [
  'GT', 'LT', 'GTE', 'LTE', 'EQ', 'NEQ', 'CROSS_UP', 'CROSS_DOWN',
  'SLOPE_UP', 'SLOPE_DOWN', 'DIFF_GT', 'DIFF_LT', 'HOLD_N_DAYS',
];

const ICHIMOKU_CONDS = [
  ...COND_OPTIONS,
  { v: 'ABOVE_CLOUD',              l: '구름 위'                    },
  { v: 'BELOW_CLOUD',              l: '구름 아래'                  },
  { v: 'IN_CLOUD',                 l: '구름 안'                    },
  { v: 'CLOUD_BREAK_UP',           l: '구름 상향 돌파'             },
  { v: 'CLOUD_BREAK_DOWN',         l: '구름 하향 돌파'             },
  { v: 'SPAN1_GT_SPAN2',           l: '선행스팬1 > 선행스팬2'      },
  { v: 'SPAN1_LT_SPAN2',           l: '선행스팬1 < 선행스팬2'      },
  { v: 'SPAN1_CROSS_UP_SPAN2',     l: '선행스팬1 상향돌파 선행스팬2' },
  { v: 'SPAN1_CROSS_DOWN_SPAN2',   l: '선행스팬1 하향돌파 선행스팬2' },
  { v: 'LAGGING_GT_PRICE',         l: '후행스팬 > 가격'            },
  { v: 'LAGGING_LT_PRICE',         l: '후행스팬 < 가격'            },
];

// ─── 지표별 fieldOptions 빌더 ────────────────────────────────────────────────
type Opt = { value: string; label: string };
const NONE: Opt = { value: 'NONE', label: '선택안함' };

export const getFieldOpts = (
  ind: string,
  signalType: 'buy'|'sell',
  DEF: DefType,
  cond?: ConditionDSL,
): Opt[] => {
  const overTerm  = signalType === 'buy' ? '과열진입' : '과열이탈';
  const underTerm = signalType === 'buy' ? '침체이탈' : '침체진입';
  const condOpts  = (conds: Opt[]): Opt[] => [NONE, ...conds];

  /** 해당 지표의 hline 임계값 — 없으면 기본값 */
  const th = (defaults: HLThresh): Required<HLThresh> => {
    const saved = DEF.hlThresh[ind] ?? {};
    return {
      over:  saved.over  ?? defaults.over  ?? 100,
      mid:   saved.mid   ?? defaults.mid   ?? 0,
      under: saved.under ?? defaults.under ?? -100,
    };
  };

  switch(ind) {
    case 'RSI': {
      const { over, mid, under } = th({ over:70, mid:50, under:30 });
      const rsiP = cond ? getConditionValuePeriod({ ...cond, indicatorType: 'RSI' }, DEF) : DEF.rsiPeriod;
      return condOpts([
        { value:'RSI_VALUE',  label:`RSI 라인(${rsiP}일)` },
        { value:THRESHOLD_HL_OVER,  label:`${overTerm}(${over})` },
        { value:THRESHOLD_HL_MID,   label:`중앙선(${mid})` },
        { value:THRESHOLD_HL_UNDER, label:`${underTerm}(${under})` },
      ]);
    }
    case 'STOCHASTIC': {
      const { over, mid, under } = th({ over:80, mid:50, under:20 });
      return condOpts([
        { value:'STOCH_K',    label:`Stochastic %K(${DEF.stochK}일)` },
        { value:'STOCH_D',    label:`Stochastic %D(${DEF.stochD}일)` },
        { value:THRESHOLD_HL_OVER,  label:`${overTerm}(${over})` },
        { value:THRESHOLD_HL_MID,   label:`중앙선(${mid})` },
        { value:THRESHOLD_HL_UNDER, label:`${underTerm}(${under})` },
      ]);
    }
    case 'CCI': {
      const { over, mid, under } = th({ over:100, mid:0, under:-100 });
      const cciP = cond ? getConditionValuePeriod({ ...cond, indicatorType: 'CCI' }, DEF) : DEF.cciPeriod;
      return condOpts([
        { value:'CCI_VALUE',  label:`CCI 라인(${cciP}일)` },
        { value:THRESHOLD_HL_OVER,  label:`${overTerm}(${over})` },
        { value:THRESHOLD_HL_MID,   label:`중앙선(${mid})` },
        { value:THRESHOLD_HL_UNDER, label:`${underTerm}(${under})` },
      ]);
    }
    case 'ADX': {
      const { over, mid, under } = th({ over: 40, mid: 25, under: 20 });
      const adxP = cond ? getConditionValuePeriod({ ...cond, indicatorType: 'ADX' }, DEF) : DEF.adxPeriod;
      return condOpts([
        { value:'ADX_VALUE', label:`ADX 라인(${adxP}일)` },
        { value:THRESHOLD_HL_OVER,    label:`${overTerm}(${over})` },
        { value:THRESHOLD_HL_MID,     label:`중앙선(${mid})` },
        { value:THRESHOLD_HL_UNDER,   label:`${underTerm}(${under})` },
      ]);
    }
    case 'TRIX': {
      const { mid } = th({ mid:0 });
      const trixP = cond ? getConditionValuePeriod({ ...cond, indicatorType: 'TRIX' }, DEF) : DEF.trixPeriod;
      return condOpts([
        { value:'TRIX_VALUE',  label:`TRIX 라인(${trixP}일)` },
        { value:'TRIX_SIGNAL', label:`TRIX 시그널(${DEF.trixSignal}일)` },
        { value:THRESHOLD_HL_MID,       label:`중앙선(${mid})` },
      ]);
    }
    case 'DMI': {
      const { over, mid, under } = th({ over:30, mid:20, under:10 });
      return condOpts([
        { value:'PDI',         label:`DI+(${DEF.dmiPeriod}일)` },
        { value:'MDI',         label:`DI-(${DEF.dmiPeriod}일)` },
        { value:THRESHOLD_HL_OVER,      label:`과열(${over})` },
        { value:THRESHOLD_HL_MID,       label:`중간값(${mid})` },
        { value:THRESHOLD_HL_UNDER,     label:`침체(${under})` },
      ]);
    }
    case 'OBV': return condOpts([
      { value:'OBV_LINE',    label:`OBV선(${DEF.obvPeriod}일)` },
      { value:'OBV_SIGNAL',  label:`신호선(${DEF.obvSignal}일)` },
    ]);
    case 'VOLUME': return condOpts([
      { value:'VOLUME_VALUE', label:'거래량' },
      { value:'VOLUME_MA',    label:'거래량 이동평균' },
    ]);
    case 'MA': {
      let opts = [
        NONE,
        ...buildMaFieldOptions(DEF.maLines),
        { value: 'CLOSE_PRICE', label: '종가' },
      ];
      if (cond?.leftField) opts = appendLegacyMaFieldOption(opts, cond.leftField);
      if (cond?.rightField) opts = appendLegacyMaFieldOption(opts, cond.rightField);
      return opts;
    }
    case 'EMA': {
      let opts = [
        NONE,
        ...buildEmaFieldOptions(DEF.maLines, 4),
        { value: 'CLOSE_PRICE', label: '종가' },
      ];
      if (cond?.leftField) opts = appendLegacyMaFieldOption(opts, cond.leftField);
      if (cond?.rightField) opts = appendLegacyMaFieldOption(opts, cond.rightField);
      return opts;
    }
    case 'DISPARITY': {
      const { mid } = th({ mid: 100 });
      return condOpts([
      { value:`DISPARITY${DEF.dispUltra}`,  label:`초단기 이격도(${DEF.dispUltra}일)` },
      { value:`DISPARITY${DEF.dispShort}`,  label:`단기 이격도(${DEF.dispShort}일)` },
      { value:`DISPARITY${DEF.dispMid}`,    label:`중기 이격도(${DEF.dispMid}일)` },
      { value:`DISPARITY${DEF.dispLong}`,   label:`장기 이격도(${DEF.dispLong}일)` },
      { value:THRESHOLD_HL_MID,         label:`중앙선(${mid})` },
    ]);
    }
    case 'PSYCHOLOGICAL': {
      const { over, mid, under } = th({ over:75, mid:50, under:25 });
      const psyP = cond
        ? getConditionValuePeriod({ ...cond, indicatorType: ind }, DEF)
        : DEF.psyPeriod;
      return condOpts([
        { value:'PSY_VALUE', label:`심리도 라인(${psyP}일)` },
        { value:THRESHOLD_HL_OVER,    label:`${overTerm}(${over})` },
        { value:THRESHOLD_HL_MID,     label:`중앙선(${mid})` },
        { value:THRESHOLD_HL_UNDER,   label:`${underTerm}(${under})` },
      ]);
    }
    case 'NEW_PSYCHOLOGICAL': {
      const { over, mid, under } = th({ over:50, mid:0, under:-50 });
      const nPsyP = cond
        ? getConditionValuePeriod({ ...cond, indicatorType: ind }, DEF)
        : DEF.newPsy;
      return condOpts([
        { value:'NEW_PSY_VALUE', label:`신심리도 라인(${nPsyP}일)` },
        { value:THRESHOLD_HL_OVER,    label:`${overTerm}(${over})` },
        { value:THRESHOLD_HL_MID,     label:`중앙선(${mid})` },
        { value:THRESHOLD_HL_UNDER,   label:`${underTerm}(${under})` },
      ]);
    }
    case 'INVEST_PSYCHOLOGICAL': {
      const { over, mid, under } = th({ over:75, mid:50, under:25 });
      const ipsyP = cond
        ? getConditionValuePeriod({ ...cond, indicatorType: 'INVEST_PSYCHOLOGICAL' }, DEF)
        : DEF.investPsy;
      return condOpts([
        { value:'INVEST_PSY_VALUE', label:`투자심리도 라인(${ipsyP}일)` },
        { value:THRESHOLD_HL_OVER,    label:`${overTerm}(${over})` },
        { value:THRESHOLD_HL_MID,     label:`중앙선(${mid})` },
        { value:THRESHOLD_HL_UNDER,   label:`${underTerm}(${under})` },
      ]);
    }
    case 'WILLIAMS_R': {
      const { over, mid, under } = th({ over:-20, mid:-50, under:-80 });
      const wrP = cond ? getConditionValuePeriod({ ...cond, indicatorType: 'WILLIAMS_R' }, DEF) : DEF.williamsR;
      return condOpts([
        { value:'WILLIAMS_R_VALUE', label:`Williams %R 라인(${wrP}일)` },
        { value:THRESHOLD_HL_OVER,    label:`${overTerm}(${over})` },
        { value:THRESHOLD_HL_MID,     label:`중앙선(${mid})` },
        { value:THRESHOLD_HL_UNDER,   label:`${underTerm}(${under})` },
      ]);
    }
    case 'BWI': {
      const { over, mid, under } = th({ over:80, mid:50, under:20 });
      const bwiP = DEF.bwiPeriod;
      return condOpts([
        { value:'BWI_VALUE',  label:`BWI 라인(${bwiP}일)` },
        { value:THRESHOLD_HL_OVER,  label:`${overTerm}(${over})` },
        { value:THRESHOLD_HL_MID,   label:`중앙선(${mid})` },
        { value:THRESHOLD_HL_UNDER, label:`${underTerm}(${under})` },
      ]);
    }
    case 'VR': {
      const { over, mid, under } = th({ over:200, mid:100, under:50 });
      const vrP = cond ? getConditionValuePeriod({ ...cond, indicatorType: 'VR' }, DEF) : DEF.vrPeriod;
      return condOpts([
        { value:'VR_VALUE',   label:`VR 라인(${vrP}일)` },
        { value:THRESHOLD_HL_OVER,  label:`${overTerm}(${over})` },
        { value:THRESHOLD_HL_MID,   label:`중앙선(${mid})` },
        { value:THRESHOLD_HL_UNDER, label:`${underTerm}(${under})` },
      ]);
    }
    case 'VOLUME_OSC': {
      const { mid } = th({ mid:0 });
      return condOpts([
        { value:'VOLUME_OSC_VALUE', label:`거래량 오실레이터 값(${DEF.volOscShort}일/${DEF.volOscLong}일)` },
        { value:THRESHOLD_HL_MID,     label:`중앙선(${mid})` },
      ]);
    }
    case 'MACD': {
      const { mid } = th({ mid:0 });
      return condOpts([
        { value:'MACD_LINE',   label:`MACD 라인(${DEF.macdFast}일/${DEF.macdSlow}일)` },
        { value:'SIGNAL_LINE', label:`시그널 라인(${DEF.macdSignal}일)` },
        { value:'HISTOGRAM',   label:'히스토그램' },
        { value:THRESHOLD_HL_MID,       label:`중앙선(${mid})` },
      ]);
    }
    case 'BOLLINGER': return condOpts([
      { value:'UPPER_BAND',  label:`상단밴드(${DEF.bbPeriod}일, σ${DEF.bbStdDev})` },
      { value:'MIDDLE_BAND', label:`중심선(${DEF.bbPeriod}일)` },
      { value:'LOWER_BAND',  label:`하단밴드(${DEF.bbPeriod}일, σ${DEF.bbStdDev})` },
      { value:'CLOSE_PRICE', label:'종가' },
      { value:'OPEN_PRICE',  label:'시가' },
      { value:'HIGH_PRICE',  label:'고가' },
      { value:'LOW_PRICE',   label:'저가' },
    ]);
    case 'NEW_HIGH': {
      const p = cond?.period ?? 9;
      const periods = [...new Set([p, 5, 9, 10, 20, 55])].sort((a, b) => a - b);
      return condOpts([
        { value: 'CLOSE_PRICE', label: '종가' },
        { value: 'HIGH_PRICE', label: '고가' },
        { value: 'OPEN_PRICE', label: '시가' },
        ...periods.map(n => ({ value: nhPriorField(n), label: `${n}일 신고가선(직전 N봉)` })),
      ]);
    }
    case 'NEW_LOW': {
      const p = cond?.period ?? 9;
      const periods = [...new Set([p, 5, 9, 10, 20, 55])].sort((a, b) => a - b);
      return condOpts([
        { value: 'CLOSE_PRICE', label: '종가' },
        { value: 'LOW_PRICE', label: '저가' },
        { value: 'OPEN_PRICE', label: '시가' },
        ...periods.map(n => ({ value: nlPriorField(n), label: `${n}일 신저가선(직전 N봉)` })),
      ]);
    }
    case 'DONCHIAN': return condOpts([
      { value:'DC_UPPER_9',   label:'상단(9일 최고가·신고가)' },
      { value:'DC_UPPER_10',  label:'상단(10일 최고가)' },
      { value:'DC_UPPER_20',  label:'상단(20일 최고가·신고가)' },
      { value:'DC_UPPER_55',  label:'상단(55일 최고가)' },
      { value:'DC_MIDDLE_20', label:'중심(20일)' },
      { value:'DC_LOWER_9',   label:'하단(9일 최저가·신저가)' },
      { value:'DC_LOWER_10',  label:'하단(10일 최저가)' },
      { value:'DC_LOWER_20',  label:'하단(20일 최저가·신저가)' },
      { value:'DC_LOWER_55',  label:'하단(55일 최저가)' },
      { value:'CLOSE_PRICE',  label:'종가' },
      { value:'LOW_PRICE',    label:'저가' },
      { value:'HIGH_PRICE',   label:'고가' },
    ]);
    case 'ICHIMOKU': return condOpts([
      { value:'CONVERSION_LINE', label:`전환선(${DEF.ichTenkan}일)` },
      { value:'BASE_LINE',       label:`기준선(${DEF.ichKijun}일)` },
      { value:'LEADING_SPAN1',   label:'선행스팬1' },
      { value:'LEADING_SPAN2',   label:`선행스팬2(${DEF.ichSenkouB}일)` },
      { value:'LAGGING_SPAN',    label:'후행스팬' },
      { value:'CLOSE_PRICE',     label:'종가' },
      { value:'OPEN_PRICE',      label:'시가' },
      { value:'HIGH_PRICE',      label:'고가' },
      { value:'LOW_PRICE',       label:'저가' },
    ]);
    default: return [NONE];
  }
};

const getDefaultConditionFields = (ind: string, signalType: 'buy'|'sell', DEF: DefType): { l: string; r: string } => {
  const map: Record<string, {l:string;r:string}> = {
    RSI:             { l:'RSI_VALUE',        r: THRESHOLD_HL_OVER  },
    STOCHASTIC:      { l:'STOCH_K',          r:'STOCH_D'          },
    CCI:             { l:'CCI_VALUE',        r: THRESHOLD_HL_OVER  },
    ADX:             { l:'ADX_VALUE',        r: THRESHOLD_HL_MID   },
    TRIX:            { l:'TRIX_VALUE',       r:'TRIX_SIGNAL'      },
    DMI:             { l:'PDI',              r:'MDI'              },
    OBV:             { l:'OBV_LINE',         r:'OBV_SIGNAL'       },
    VOLUME:          { l:'VOLUME_VALUE',     r:'VOLUME_MA'        },
    MA:              { l: 'MA1',              r: 'MA2'              },
    EMA:             { l: 'EMA1',             r: 'EMA2'             },
    DISPARITY:       { l:`DISPARITY${DEF.dispUltra}`, r: THRESHOLD_HL_MID },
    PSYCHOLOGICAL:        { l:'PSY_VALUE',        r: THRESHOLD_HL_OVER },
    NEW_PSYCHOLOGICAL:    { l:'NEW_PSY_VALUE',  r: THRESHOLD_HL_OVER },
    INVEST_PSYCHOLOGICAL: { l:'INVEST_PSY_VALUE', r: THRESHOLD_HL_OVER },
    WILLIAMS_R:      { l:'WILLIAMS_R_VALUE',  r: THRESHOLD_HL_OVER },
    BWI:             { l:'BWI_VALUE',         r: THRESHOLD_HL_OVER },
    VR:              { l:'VR_VALUE',          r: THRESHOLD_HL_OVER },
    VOLUME_OSC:      { l:'VOLUME_OSC_VALUE',  r: THRESHOLD_HL_MID },
    MACD:            { l:'MACD_LINE',         r:'SIGNAL_LINE'     },
    BOLLINGER:       { l:'CLOSE_PRICE',       r:'UPPER_BAND'      },
    DONCHIAN:        signalType === 'buy'
      ? { l:'CLOSE_PRICE', r:'DC_UPPER_20' }
      : { l:'CLOSE_PRICE', r:'DC_LOWER_20' },
    NEW_HIGH:        { l:'CLOSE_PRICE', r: nhPriorField(9) },
    NEW_LOW:         { l:'CLOSE_PRICE', r: nlPriorField(9) },
    ICHIMOKU:        { l:'CONVERSION_LINE',   r:'BASE_LINE'       },
  };
  return map[ind] ?? { l:'NONE', r:'NONE' };
};

export { getDefaultConditionFields };

// conditionType 변경 시 자동 필드 조정
const applyCondTypeDefaults = (cond: ConditionDSL, newCondType: string, DEF: DefType): ConditionDSL => {
  const updated = { ...cond, conditionType: newCondType };
  if (cond.composite) {
    return syncCompositeFields({
      ...updated,
      composite: true,
      leftPeriod: cond.leftPeriod,
      rightPeriod: cond.rightPeriod,
    });
  }
  const fieldOpts = getFieldOpts(cond.indicatorType, 'buy', DEF);
  const isValid = (v: string|undefined) => !!v && fieldOpts.some(o => o.value === v);
  const def = getDefaultConditionFields(cond.indicatorType, 'buy', DEF);

  if (['GT','LT','GTE','LTE','EQ','NEQ','CROSS_UP','CROSS_DOWN','DIFF_GT','DIFF_LT'].includes(newCondType)) {
    if (!isValid(updated.leftField))  updated.leftField  = def.l;
    if (!isValid(updated.rightField)) updated.rightField = def.r;
    if (['DIFF_GT','DIFF_LT'].includes(newCondType)) updated.compareValue = updated.compareValue ?? 0;
  } else if (['SLOPE_UP','SLOPE_DOWN'].includes(newCondType)) {
    if (!isValid(updated.leftField)) updated.leftField = def.l;
    updated.rightField = 'NONE';
    updated.slopePeriod = updated.slopePeriod ?? 3;
  } else if (newCondType === 'HOLD_N_DAYS') {
    updated.leftField = 'NONE'; updated.rightField = 'NONE';
    updated.holdDays = updated.holdDays ?? 3;
  } else if (['LOWEST_LTE', 'LOWEST_GTE'].includes(newCondType)) {
    if (!isValid(updated.leftField)) updated.leftField = def.l;
    if (!isValid(updated.rightField)) updated.rightField = THRESHOLD_HL_UNDER;
    updated.lookbackPeriod = updated.lookbackPeriod ?? 20;
  }

  // Ichimoku 특수처리
  if (cond.indicatorType === 'ICHIMOKU') {
    if (['ABOVE_CLOUD','BELOW_CLOUD','IN_CLOUD','CLOUD_BREAK_UP','CLOUD_BREAK_DOWN'].includes(newCondType)) {
      updated.leftField = 'NONE'; updated.rightField = 'NONE';
    } else if (['SPAN1_GT_SPAN2','SPAN1_LT_SPAN2','SPAN1_CROSS_UP_SPAN2','SPAN1_CROSS_DOWN_SPAN2'].includes(newCondType)) {
      updated.leftField = 'LEADING_SPAN1'; updated.rightField = 'LEADING_SPAN2';
    } else if (['LAGGING_GT_PRICE','LAGGING_LT_PRICE'].includes(newCondType)) {
      updated.leftField = 'LAGGING_SPAN'; updated.rightField = 'CLOSE_PRICE';
    }
  }
  if (isPriceExtremeIndicator(updated.indicatorType)) {
    return syncPriceExtremeSimpleFields(updated);
  }
  return updated;
};

// ─── 자연어 변환 ──────────────────────────────────────────────────────────────
const condLabel = (v: string) => COND_OPTIONS.concat(ICHIMOKU_CONDS as any).find(o => o.v === v)?.l ?? v;

/** RSI_VALUE_9 → RSI_VALUE (옵션 조회용) */
export function resolveFieldOptionValue(indicatorType: string, field?: string): string | undefined {
  if (!field) return field;
  const bases: Record<string, string> = {
    RSI: 'RSI_VALUE',
    CCI: 'CCI_VALUE',
    ADX: 'ADX_VALUE',
    WILLIAMS_R: 'WILLIAMS_R_VALUE',
    TRIX: 'TRIX_VALUE',
    VR: 'VR_VALUE',
    PSYCHOLOGICAL: 'PSY_VALUE',
    NEW_PSYCHOLOGICAL: 'NEW_PSY_VALUE',
    INVEST_PSYCHOLOGICAL: 'INVEST_PSY_VALUE',
  };
  const base = bases[indicatorType];
  if (base && (field === base || field.startsWith(`${base}_`))) return base;
  return field;
}

export const nodeToText = (node: LogicNode, DEF: DefType = DEF_DEFAULTS): string => {
  if (!node) return '';
  if (node.type === 'CONDITION' && node.condition) {
    const c = normalizeCompositeCondition(node.condition);
    const indName = getStrategyIndicatorDisplayName(c.indicatorType);
    const opts = getFieldOpts(c.indicatorType, 'buy', DEF, c);
    const C = condLabel(c.conditionType);
    if (c.composite && c.leftPeriod && c.rightPeriod) {
      const leftOpts = getCompositeFieldOpts(c.indicatorType, 1, DEF, c);
      const rightOpts = getCompositeFieldOpts(c.indicatorType, 2, DEF, c);
      const L = leftOpts.find(o => o.value === c.leftField)?.label
        ?? compositeFieldLabel(c.indicatorType, 1, c.leftPeriod);
      const R = rightOpts.find(o => o.value === c.rightField)?.label
        ?? compositeFieldLabel(c.indicatorType, 2, c.rightPeriod);
      const lCt = getCompositeLeftCandleType(c);
      const rCt = getCompositeRightCandleType(c);
      const tf = lCt !== rCt ? ` [${lCt}/${rCt}]` : ` [${lCt}]`;
      return `${indName} - ${L} ${C} ${R}${tf}`;
    }
    const defaults = getDefaultConditionFields(c.indicatorType, 'buy', DEF);
    const lv = resolveFieldOptionValue(c.indicatorType, c.leftField);
    const rvRaw = c.rightField;
    const rv = isThresholdOverridden(c)
      ? resolveFieldOptionValue(c.indicatorType, rvRaw)
      : (resolveInheritedThresholdField(rvRaw, c.indicatorType, DEF.hlThresh)
        ?? resolveFieldOptionValue(c.indicatorType, rvRaw));
    const L = opts.find(o => o.value === lv)?.label ?? c.leftField ?? indName;
    const chartRef = getChartReferenceThreshold(c, 'right', defaults.r, DEF);
    const R = opts.find(o => o.value === rv)?.label
      ?? (chartRef != null && !isThresholdOverridden(c) ? `기준(${chartRef})` : null)
      ?? (rv && parseThresholdField(rv) != null ? `임계(${parseThresholdField(rv)})` : rv ?? '');
    if (['LOWEST_LTE', 'LOWEST_GTE'].includes(c.conditionType)) {
      const n = c.lookbackPeriod ?? 20;
      const op = c.conditionType === 'LOWEST_LTE' ? '≤' : '≥';
      return `${indName} - 최근 ${n}봉 ${L} 최저 ${op} ${R || '침체선'}`;
    }
    const rangeClause = formatCandleRangeClause(c);
    if (R && R !== '선택안함') return `${indName} - ${rangeClause}${L} ${C} ${R}`;
    return `${indName} - ${rangeClause}${L} ${C}`;
  }
  if (node.type === 'AND') {
    if (isStableStrategyPairRoot(node) && node.stableStrategyPair?.mode === 'buy') {
      const { strategyId, mode } = node.stableStrategyPair;
      const level = inferStableStrategyFilterLevel(node);
      return `${stableStrategyDisplayName(strategyId as StableStrategyId, mode)} [필터:${stableStrategyFilterLevelLabel(level)}]\n${stableStrategySummaryText(strategyId as StableStrategyId, mode, level, DEF)}`;
    }
    if (isInflection33PairRoot(node) && node.inflection33Pair?.mode === 'buy') {
      const { mode, period } = node.inflection33Pair;
      const level = inferInflection33FilterLevel(node);
      return `${inflection33DisplayName(mode, period)} [필터:${inflection33FilterLevelLabel(level)}]\n${inflection33SummaryText(mode, period, level)}`;
    }
    if (isPriceExtremeBreakoutPairRoot(node) && node.priceExtremePair) {
      const { mode, shortPeriod, longPeriod } = node.priceExtremePair;
      const level = inferPriceExtremeFilterLevel(node);
      return `${priceExtremePairDisplayName(mode, shortPeriod, longPeriod)} [필터:${filterLevelLabel(level)}]\n${priceExtremePairSummaryText(mode, shortPeriod, longPeriod, level)}`;
    }
    const parts = (node.children ?? []).map(c => nodeToText(c, DEF));
    return parts.length === 0 ? '(AND 그룹)' : parts.join('\nAND ');
  }
  if (node.type === 'OR') {
    if (isStableStrategyPairRoot(node) && node.stableStrategyPair?.mode === 'sell') {
      const { strategyId, mode } = node.stableStrategyPair;
      const level = inferStableStrategyFilterLevel(node);
      return `${stableStrategyDisplayName(strategyId as StableStrategyId, mode)} [필터:${stableStrategyFilterLevelLabel(level)}]\n${stableStrategySummaryText(strategyId as StableStrategyId, mode, level, DEF)}`;
    }
    if (isInflection33PairRoot(node) && node.inflection33Pair?.mode === 'sell') {
      const { mode, period } = node.inflection33Pair;
      const level = inferInflection33FilterLevel(node);
      return `${inflection33DisplayName(mode, period)} [필터:${inflection33FilterLevelLabel(level)}]\n${inflection33SummaryText(mode, period, level)}`;
    }
    if (isStochOverboughtPairRoot(node)) {
      const sec = node.stochPair!.secondaryIndicatorType;
      return `${stochPairDisplayName(sec)}\n${stochPairSummaryText(sec)}`;
    }
    const parts = (node.children ?? []).map(c => nodeToText(c, DEF));
    return parts.length === 0 ? '(OR 그룹)' : parts.join('\nOR ');
  }
  if (node.type === 'NOT') {
    const c = node.children?.[0];
    return c ? `NOT (${nodeToText(c, DEF)})` : '(NOT 그룹)';
  }
  return '';
};

export const nodeToFormula = (node: LogicNode, DEF: DefType = DEF_DEFAULTS): string => {
  if (!node) return '';
  if (node.type === 'CONDITION') return `(${nodeToText(node, DEF)})`;
  if (node.type === 'AND') {
    const parts = (node.children ?? []).map(c => nodeToFormula(c, DEF));
    return parts.length === 0 ? '(빈 AND)' : `(${parts.join(' AND ')})`;
  }
  if (node.type === 'OR') {
    const parts = (node.children ?? []).map(c => nodeToFormula(c, DEF));
    return parts.length === 0 ? '(빈 OR)' : `(${parts.join(' OR ')})`;
  }
  if (node.type === 'NOT') {
    const c = node.children?.[0];
    return c ? `NOT ${nodeToFormula(c, DEF)}` : '(빈 NOT)';
  }
  return '';
};

// ─── 트리 조작 ────────────────────────────────────────────────────────────────
export const updateNode = (root: LogicNode, id: string, fn: (n: LogicNode) => LogicNode): LogicNode => {
  if (root.id === id) return fn(root);
  if (!root.children) return root;
  return { ...root, children: root.children.map(c => updateNode(c, id, fn)) };
};

export const deleteNode = (root: LogicNode, id: string): LogicNode | null => {
  if (root.id === id) return null;
  if (!root.children) return root;
  const children = root.children.map(c => deleteNode(c, id)).filter(Boolean) as LogicNode[];
  return { ...root, children };
};

export const addChild = (root: LogicNode, parentId: string, child: LogicNode): LogicNode => {
  if (root.id === parentId) {
    if (root.type === 'NOT' && (root.children?.length ?? 0) >= 1) { alert('NOT는 자식 1개만 가능합니다.'); return root; }
    return { ...root, children: [...(root.children ?? []), child] };
  }
  if (!root.children) return root;
  return { ...root, children: root.children.map(c => addChild(c, parentId, child)) };
};

/** 팔레트·터치로 루트에 추가할 때 — OR/AND 그룹이면 자식으로 붙이고, 단일 조건만 AND로 묶음 */
export const mergeAtRoot = (root: LogicNode | null, newNode: LogicNode, isOperator: boolean): LogicNode => {
  if (!root) return newNode;
  if (isOperator) return { ...newNode, children: [root] };
  if (root.type === 'AND' || root.type === 'OR') {
    return { ...root, children: [...(root.children ?? []), newNode] };
  }
  return { id: genId(), type: 'AND', children: [root, newNode] };
};

// ─── 검증 ─────────────────────────────────────────────────────────────────────
export const validateTree = (node: LogicNode): ValidationResult => {
  const errors: { message: string }[] = [];
  const warnings: { message: string }[] = [];
  const check = (n: LogicNode) => {
    if (n.type === 'AND' || n.type === 'OR') {
      if (!n.children || n.children.length === 0) errors.push({ message: `${n.type} 노드에 자식 조건이 없습니다.` });
      else if (n.children.length === 1) warnings.push({ message: `${n.type} 노드에 자식이 1개뿐입니다.` });
      n.children?.forEach(check);
    } else if (n.type === 'NOT') {
      if (!n.children || n.children.length === 0) errors.push({ message: 'NOT 노드에 자식 조건이 없습니다.' });
      else if (n.children.length > 1) errors.push({ message: 'NOT 노드는 자식이 1개여야 합니다.' });
      n.children?.forEach(check);
    } else if (n.type === 'CONDITION') {
      if (!n.condition) errors.push({ message: '조건 내용이 비어있습니다.' });
    }
  };
  check(node);
  return { isValid: errors.length === 0, errors, warnings };
};

// ─── 조건 편집 UI (인라인 4컬럼) ─────────────────────────────────────────────

interface CondEditorProps {
  cond: ConditionDSL;
  signalType: 'buy'|'sell';
  onChange: (c: ConditionDSL) => void;
  def: DefType;
  /** Stoch 과열×보조 복합 — 캔들범위 대신 보조지표 선택 */
  stochPairEdit?: {
    secondaryIndicator: string;
    onSecondaryChange: (indicator: string) => void;
    stochLocked?: boolean;
  };
}

export const CondEditor: React.FC<CondEditorProps> = ({
  cond, signalType, onChange, def, stochPairEdit,
}) => {
  const normalized = normalizeCompositeCondition(cond);
  const fieldOpts = getFieldOpts(normalized.indicatorType, signalType, def, normalized);
  const condOpts  = getCondOptionsForIndicator(normalized.indicatorType);

  const isValid = (v: string|undefined) => !!v && fieldOpts.some(o => o.value === v);
  const defaults = getDefaultConditionFields(normalized.indicatorType, signalType, def);
  const inheritedThresholdField = defaults.r;

  const getLeftValue  = () => {
    const v = resolveFieldOptionValue(normalized.indicatorType, normalized.leftField);
    return isValid(v) ? v! : defaults.l;
  };
  const getRightValue = () => {
    const raw = normalized.rightField;
    const v = isThresholdOverridden(normalized)
      ? resolveFieldOptionValue(normalized.indicatorType, raw)
      : (isThresholdFieldOrSymbol(raw)
        ? (resolveInheritedThresholdField(raw, normalized.indicatorType, def.hlThresh)
          ?? resolveFieldOptionValue(normalized.indicatorType, raw))
        : resolveFieldOptionValue(normalized.indicatorType, raw));
    return isValid(v) ? v! : defaults.r;
  };

  const isSlopeType = ['SLOPE_UP','SLOPE_DOWN'].includes(normalized.conditionType);
  const isHoldType  = normalized.conditionType === 'HOLD_N_DAYS';
  const isLookbackType = ['LOWEST_LTE', 'LOWEST_GTE'].includes(normalized.conditionType);
  const isDiffType  = ['DIFF_GT','DIFF_LT'].includes(normalized.conditionType);

  const rightValue = isSlopeType ? 'NONE' : isHoldType ? 'NONE' : getRightValue();

  const handleCondType = (newType: string) => onChange(applyCondTypeDefaults(normalized, newType, def));
  const handleRight = (v: string) => {
    if (isThresholdSymbol(v)) {
      const { targetValue: _t, ...rest } = normalized;
      onChange({ ...rest, rightField: v, thresholdOverride: false });
      return;
    }
    const thresholds: Record<string,number> = {
      OVERBOUGHT_70: 70, NEUTRAL_50: 50, OVERSOLD_30: 30,
      OVERBOUGHT_80: 80, OVERSOLD_20: 20,
      OVERBOUGHT_100: 100, NEUTRAL_0: 0, OVERSOLD_NEG100: -100,
      ADX_25: 25, ADX_50: 50,
      OVERBOUGHT_75: 75, OVERSOLD_25: 25,
      OVERBOUGHT_NEG20: -20, NEUTRAL_NEG50: -50, OVERSOLD_NEG80: -80,
      OVERBOUGHT_200: 200, NEUTRAL_100: 100, OVERSOLD_50: 50,
      ZERO_LINE: 0,
    };
    let upd: ConditionDSL = { ...normalized, rightField: v };
    if (thresholds[v] !== undefined) {
      upd.targetValue = thresholds[v];
      upd.thresholdOverride = true;
    }
    const priorP = parsePriorExtremePeriod(v);
    if (priorP != null && isPriceExtremeIndicator(normalized.indicatorType)) {
      upd = syncPriceExtremeSimpleFields({ ...upd, period: priorP });
    }
    onChange(upd);
  };

  if (normalized.composite) {
    const leftOpts = getCompositeFieldOpts(normalized.indicatorType, 1, def, normalized);
    const rightOpts = getCompositeFieldOpts(normalized.indicatorType, 2, def, normalized);
    const leftField = normalized.leftField ?? leftOpts[0]?.value ?? 'NONE';
    const rightField = normalized.rightField ?? rightOpts[0]?.value ?? 'NONE';
    const leftIsPreset = leftOpts.some(o => o.value === leftField);
    const rightIsPreset = rightOpts.some(o => o.value === rightField);
    const leftPeriod = getConditionValuePeriod(normalized, def);
    const rightPeriod = getConditionRightPeriod(normalized, def);
    const periodPresetsLeft = getCompositePeriodPresetOptions(normalized.indicatorType, def, 'left');
    const periodPresetsRight = getCompositePeriodPresetOptions(normalized.indicatorType, def, 'right');

    const handleCompositeField = (side: 'left' | 'right', field: string) => {
      const p = parseDonchianPeriod(field)
        ?? parsePeriodFromCompositeField(normalized.indicatorType, field);
      if (p == null) return;
      if (side === 'left') {
        onChange(syncCompositeFields({ ...normalized, leftPeriod: p, leftField: field, valuePeriodOverride: true }));
      } else {
        onChange(syncCompositeFields({ ...normalized, rightPeriod: p, rightField: field, rightPeriodOverride: true }));
      }
    };

    const leftCt = getCompositeLeftCandleType(normalized);
    const rightCt = getCompositeRightCandleType(normalized);

    return (
      <div className="sp-cond-editor sp-cond-editor--composite">
        <div className="sp-cond-composite-badge">복합지표 · {compositeDisplayName(normalized.indicatorType)}</div>
        <div className="sp-cond-row-composite-tf">
          <div className="sp-cond-field">
            <label className="sp-cond-lbl">{compositeElementLabel(normalized.indicatorType, 1)} 시간봉</label>
            <select
              className="sp-cond-sel"
              value={leftCt}
              onChange={e => onChange(setCompositeLeftCandleType(normalized, e.target.value))}
            >
              {STRATEGY_CANDLE_TYPE_OPTIONS.map(o => (
                <option key={o.value} value={o.value}>{o.label}</option>
              ))}
            </select>
          </div>
          <div className="sp-cond-field">
            <label className="sp-cond-lbl">{compositeElementLabel(normalized.indicatorType, 2)} 시간봉</label>
            <select
              className="sp-cond-sel"
              value={rightCt}
              onChange={e => onChange(setCompositeRightCandleType(normalized, e.target.value))}
            >
              {STRATEGY_CANDLE_TYPE_OPTIONS.map(o => (
                <option key={o.value} value={o.value}>{o.label}</option>
              ))}
            </select>
          </div>
        </div>
        <div className="sp-cond-row4">
          <div className="sp-cond-field">
            <label className="sp-cond-lbl">지표</label>
            <select
              className="sp-cond-sel"
              value={normalized.indicatorType}
              onChange={e => onChange(switchCompositeIndicatorType(normalized, e.target.value, def))}
            >
              {COMPOSITE_INDICATOR_ITEMS.map(o => (
                <option key={o.value} value={o.value}>{o.label}</option>
              ))}
            </select>
          </div>
          <div className="sp-cond-field">
            <label className="sp-cond-lbl">조건대상1</label>
            <ComboFieldSelect
              options={leftOpts}
              fieldValue={leftField}
              isPresetField={leftIsPreset}
              customKind="period"
              customNumber={leftPeriod}
              customOptionLabel={compositeFieldLabel(normalized.indicatorType, 1, leftPeriod)}
              numberPresets={periodPresetsLeft}
              min={1}
              max={500}
              onFieldChange={f => handleCompositeField('left', f)}
              onCustomNumberChange={p => onChange(setConditionValuePeriod(normalized, p, def))}
            />
          </div>
          <div className="sp-cond-field">
            <label className="sp-cond-lbl">조건대상2</label>
            <ComboFieldSelect
              options={rightOpts}
              fieldValue={rightField}
              isPresetField={rightIsPreset}
              customKind="period"
              customNumber={rightPeriod}
              customOptionLabel={compositeFieldLabel(normalized.indicatorType, 2, rightPeriod)}
              numberPresets={periodPresetsRight}
              min={1}
              max={500}
              onFieldChange={f => handleCompositeField('right', f)}
              onCustomNumberChange={p => onChange(setConditionRightPeriod(normalized, p))}
            />
          </div>
          <div className="sp-cond-field">
            <label className="sp-cond-lbl">조건</label>
            <select className="sp-cond-sel"
              value={normalized.conditionType}
              onChange={e => handleCondType(e.target.value)}>
              {condOpts.map(o => <option key={o.v} value={o.v}>{o.l}</option>)}
            </select>
          </div>
        </div>
      </div>
    );
  }

  return (
    <div className="sp-cond-editor">
      {/* 4컬럼 row */}
      <div className="sp-cond-row4">
        {/* 캔들 범위 / Stoch×보조 복합 보조지표 */}
        <div className="sp-cond-field">
          {stochPairEdit ? (
            stochPairEdit.stochLocked ? (
              <>
                <label className="sp-cond-lbl">고정 지표</label>
                <output className="sp-cond-readout">Stochastic %K</output>
              </>
            ) : (
              <>
                <label className="sp-cond-lbl">보조지표</label>
                <select
                  className="sp-cond-sel"
                  value={stochPairEdit.secondaryIndicator}
                  onChange={e => stochPairEdit.onSecondaryChange(e.target.value)}
                >
                  {STOCH_PAIR_SECONDARY_OPTIONS.map(o => (
                    <option key={o.value} value={o.value}>{o.label}</option>
                  ))}
                </select>
              </>
            )
          ) : isLookbackType ? (
            <>
              <label className="sp-cond-lbl">lookback (봉)</label>
              <input
                type="number"
                className="sp-cond-num sp-cond-num--inline"
                min={2}
                max={500}
                value={cond.lookbackPeriod ?? ''}
                placeholder="예: 20"
                onChange={e => onChange({ ...cond, lookbackPeriod: parseInt(e.target.value) || 20 })}
              />
            </>
          ) : (
            <>
              <label className="sp-cond-lbl">캔들 범위</label>
              <select
                className="sp-cond-sel"
                value={resolveCandleRangeMode(normalized)}
                onChange={e => {
                  const mode = e.target.value as CandleRangeMode;
                  if (mode === 'CURRENT') {
                    onChange({ ...normalized, candleRangeMode: mode, candleRange: 1 });
                  } else {
                    const n = candleRangeWindowBars(normalized);
                    onChange({ ...normalized, candleRangeMode: mode, candleRange: n >= 2 ? n : 4 });
                  }
                }}
              >
                <option value="CURRENT">현재 캔들</option>
                <option value="EXISTS_IN">N봉 내 존재</option>
                <option value="NOT_EXISTS_IN">N봉 내 미존재</option>
              </select>
              {resolveCandleRangeMode(normalized) !== 'CURRENT' && (
                <input
                  type="number"
                  className="sp-cond-num sp-cond-num--inline"
                  min={2}
                  max={500}
                  value={candleRangeWindowBars(normalized)}
                  placeholder="N"
                  title="윈도우 봉 수"
                  onChange={e => onChange({
                    ...normalized,
                    candleRange: Math.max(2, parseInt(e.target.value, 10) || 4),
                  })}
                />
              )}
            </>
          )}
        </div>
        {/* 조건대상1 */}
        <div className="sp-cond-field">
          <label className="sp-cond-lbl">조건대상1</label>
          <ComboFieldSelect
            options={fieldOpts}
            fieldValue={isHoldType ? 'NONE' : (normalized.leftField ?? getLeftValue())}
            isPresetField={!isHoldType && (
              fieldOpts.some(o => o.value === normalized.leftField)
              || (fieldOpts.some(o => o.value === getLeftValue())
                && !isValuePeriodFieldStored(normalized.indicatorType, normalized.leftField))
            )}
            customKind={resolveFieldCustomKind(normalized.indicatorType, normalized.leftField)}
            customNumber={getConditionValuePeriod(normalized, def)}
            numberPresets={getPeriodPresetOptions(normalized.indicatorType, def)}
            min={1}
            max={500}
            disabled={isHoldType}
            onFieldChange={v => {
              const bases: Record<string, string> = {
                RSI: 'RSI_VALUE',
                CCI: 'CCI_VALUE',
                ADX: 'ADX_VALUE',
                WILLIAMS_R: 'WILLIAMS_R_VALUE',
                TRIX: 'TRIX_VALUE',
                VR: 'VR_VALUE',
                PSYCHOLOGICAL: 'PSY_VALUE',
                NEW_PSYCHOLOGICAL: 'NEW_PSY_VALUE',
                INVEST_PSYCHOLOGICAL: 'INVEST_PSY_VALUE',
              };
              const base = bases[normalized.indicatorType];
              if (base && v === base) {
                const p = getConditionValuePeriod(normalized, def);
                onChange({ ...normalized, leftField: `${base}_${p}`, period: p, valuePeriodOverride: true });
              } else {
                onChange({ ...normalized, leftField: v });
              }
            }}
            onCustomNumberChange={p => onChange(setConditionValuePeriod(normalized, p, def))}
          />
        </div>
        {/* 조건대상2 */}
        <div className="sp-cond-field">
          <label className="sp-cond-lbl">조건대상2</label>
          <ComboFieldSelect
            options={fieldOpts}
            fieldValue={rightValue}
            isPresetField={fieldOpts.some(o => o.value === normalized.rightField)}
            customKind={resolveFieldCustomKind(normalized.indicatorType, normalized.rightField)}
            customNumber={getConditionThreshold(normalized, 'right', inheritedThresholdField, def)
              ?? getChartReferenceThreshold(normalized, 'right', inheritedThresholdField, def)
              ?? parseThresholdField(normalized.rightField)
              ?? 0}
            numberPresets={getThresholdPresetOptions(normalized.indicatorType)}
            min={getThresholdBounds(normalized.indicatorType).min}
            max={getThresholdBounds(normalized.indicatorType).max}
            allowDecimal={normalized.indicatorType === 'CCI'}
            disabled={isSlopeType || isHoldType}
            onFieldChange={handleRight}
            onCustomNumberChange={v => onChange(setConditionThreshold(normalized, 'right', v, def))}
          />
        </div>
        {/* 조건 */}
        <div className="sp-cond-field">
          <label className="sp-cond-lbl">조건</label>
          <select className="sp-cond-sel"
            value={cond.conditionType}
            onChange={e => handleCondType(e.target.value)}>
            {condOpts.map(o => <option key={o.v} value={o.v}>{o.l}</option>)}
          </select>
        </div>
      </div>
      {/* 추가 필드 */}
      {isDiffType && (
        <div className="sp-cond-extra">
          <label className="sp-cond-lbl">비교값</label>
          <input type="number" className="sp-cond-num"
            value={cond.compareValue ?? ''} placeholder="예: 0.5"
            onChange={e => onChange({ ...cond, compareValue: parseFloat(e.target.value) || 0 })} />
        </div>
      )}
      {isSlopeType && (
        <div className="sp-cond-extra">
          <label className="sp-cond-lbl">기울기 기간 (일)</label>
          <input type="number" className="sp-cond-num" min={1} max={100}
            value={cond.slopePeriod ?? ''} placeholder="예: 5"
            onChange={e => onChange({ ...cond, slopePeriod: parseInt(e.target.value) || 3 })} />
        </div>
      )}
      {isHoldType && (
        <div className="sp-cond-extra">
          <label className="sp-cond-lbl">유지 일수 (일)</label>
          <input type="number" className="sp-cond-num" min={1} max={30}
            value={cond.holdDays ?? ''} placeholder="예: 3"
            onChange={e => onChange({ ...cond, holdDays: parseInt(e.target.value) || 3 })} />
        </div>
      )}
    </div>
  );
};

// ─── 노드 생성 헬퍼 ───────────────────────────────────────────────────────────
export type MakeNodeOptions = {
  composite?: boolean;
  /** 단일 보조지표 기간 오버라이드 */
  period?: number;
  /** 복합지표 단기·장기 기간 오버라이드 */
  leftPeriod?: number;
  rightPeriod?: number;
  leftCandleType?: string;
  rightCandleType?: string;
  /** Stoch 과열×보조 복합 */
  stochPair?: boolean;
  secondaryIndicator?: string;
  /** 9·20일 신고가/신저가 복합 */
  priceExtremeBreakout?: boolean;
  /** 33변곡 EMA+채널 복합 */
  inflection33?: boolean;
  /** 추천 안정형 전략 복합 */
  stableStrategy?: boolean;
};

/** 팔레트 드래그 payload → makeNode 옵션 */
export function makeNodeOptionsFromPalette(data: {
  composite?: boolean;
  stochPair?: boolean;
  priceExtremeBreakout?: boolean;
  inflection33?: boolean;
  stableStrategy?: boolean;
  secondaryIndicator?: string;
  period?: number;
  shortPeriod?: number;
  longPeriod?: number;
  leftCandleType?: string;
  rightCandleType?: string;
  value?: string;
}): MakeNodeOptions | undefined {
  if (data.stochPair || (data.value && isStochOverboughtPairPaletteValue(data.value))) {
    return {
      stochPair: true,
      secondaryIndicator: data.secondaryIndicator ?? 'CCI',
    };
  }
  if (data.priceExtremeBreakout || (data.value && isPriceExtremeBreakoutPairPaletteValue(data.value))) {
    return { priceExtremeBreakout: true };
  }
  if (data.inflection33 || (data.value && isInflection33PaletteValue(data.value))) {
    return { inflection33: true };
  }
  if (data.stableStrategy || (data.value && isStableStrategyPaletteValue(data.value))) {
    return { stableStrategy: true };
  }
  if (data.composite) {
    return {
      composite: true,
      leftCandleType: data.leftCandleType,
      rightCandleType: data.rightCandleType,
    };
  }
  return undefined;
}

export const makeNode = (
  type: string,
  value: string,
  signalType: 'buy'|'sell',
  DEF: DefType,
  options?: MakeNodeOptions,
): LogicNode => {
  if (type === 'operator') return { id: genId(), type: value as LogicNodeType, children: [] };
  if (options?.stochPair || isStochOverboughtPairPaletteValue(value)) {
    return buildStochOverboughtPairTree(options?.secondaryIndicator ?? 'CCI', DEF);
  }
  if (options?.priceExtremeBreakout || isPriceExtremeBreakoutPairPaletteValue(value)) {
    const tree = buildPriceExtremeBreakoutTree(value, DEF);
    if (tree) return tree;
  }
  if (options?.inflection33 || isInflection33PaletteValue(value)) {
    const tree = buildInflection33Tree(value, DEF);
    if (tree) return tree;
  }
  if (options?.stableStrategy || isStableStrategyPaletteValue(value)) {
    const tree = buildStableStrategyTree(value, DEF);
    if (tree) return tree;
  }
  if (options?.composite) {
    let condition = makeCompositeCondition(value, signalType, DEF);
    condition = {
      ...condition,
      valuePeriodOverride: false,
      rightPeriodOverride: false,
      thresholdOverride: false,
    };
    if (options.leftCandleType != null || options.rightCandleType != null) {
      condition = syncCompositeFields({
        ...condition,
        composite: true,
        leftCandleType: options.leftCandleType ?? condition.leftCandleType,
        rightCandleType: options.rightCandleType ?? condition.rightCandleType,
      });
    }
    return { id: genId(), type: 'CONDITION', condition };
  }
  const def = getDefaultConditionFields(value, signalType, DEF);
  let conditionType = signalType === 'buy' ? 'CROSS_UP' : 'CROSS_DOWN';
  if (value === 'NEW_HIGH' && signalType === 'buy') conditionType = 'CROSS_UP';
  if (value === 'NEW_LOW' && signalType === 'sell') conditionType = 'CROSS_DOWN';
  const baseCondition: ConditionDSL = {
    indicatorType: value,
    conditionType,
    leftField: def.l,
    rightField: def.r,
    candleRange: 1,
    valuePeriodOverride: false,
    thresholdOverride: false,
    rightPeriodOverride: false,
  };
  const condition = initConditionPeriodsInherit(value, baseCondition, DEF);
  return {
    id: genId(), type: 'CONDITION',
    condition: isPriceExtremeIndicator(value)
      ? syncPriceExtremeSimpleFields(condition)
      : condition,
  };
};
