#!/usr/bin/env node
/**
 * 과거 신호 검증 스크립트
 * 설계 문서 v3 기반 - 고가/저가 기반 판정, RR 필드 포함
 * 
 * 상태값 계약: pending | success | partial_success | fail | invalid | cancelled
 * 
 * 사용법:
 *   node signal_verifier.mjs verify    ← 수동 검증 (전체 결과 출력)
 *   node signal_verifier.mjs auto-verify ← 자동 검증 (간결 로그, 앱 내부용)
 * 
 * 2026-04-17 수정:
 *   - ROOT_DIR → DATA_DIR 경로 통일 (main.js와 완전 일치)
 *   - auto-verify 모드 추가 (자동 재평가 엔진용)
 * 
 * 2026-04-19 재발 방지 수정 (accuracy_sync_root_fix):
 *   - recordAccuracyVerificationSafe() 래퍼 추가
 *     · accuracy_tracker.mjs 동적 import 실패 시 silent 처리 제거
 *     · 실패 시 accuracy_sync_errors.jsonl 에 실패 기록 기입
 *     · ensureSnapshotExists 호출로 dataset snapshot 부재 시 자동 생성
 *   - verifyPendingSignals() catch 블록: silent → 명시 에러 파일 기록
 *   - autoVerifyPendingSignals() catch 블록: silent → 명시 에러 파일 기록
 */

import * as fs from 'fs';
import * as path from 'path';

const APP_ROOT_DIR = process.env.TRADENOVA_APP_ROOT_DIR || process.cwd();
const DATA_DIR     = process.env.TRADENOVA_DATA_DIR || path.join(process.cwd(), 'tradenova-data');
const BINANCE_BASE = 'http://130.94.65.235:8787';
const API_TIMEOUT = 30000;

// [mode_split] 분석 모드 파싱 (--mode classic | scalp)
const VERIFIER_MODES = ['classic', 'scalp'];
const VERIFIER_DEFAULT = 'classic';
const VERIFIER_MODE = (() => {
  const args = process.argv.slice(2);
  const idx = args.indexOf('--mode');
  if (idx >= 0 && VERIFIER_MODES.includes(args[idx + 1])) return args[idx + 1];
  return VERIFIER_DEFAULT;
})();

// [mode_split_v2] signal mode 정규화 헬퍼
// signal.mode 또는 signal.analysisMode가 'scalp'이면 scalp, 나머지는 classic
function normalizeSignalMode(signal) {
  if (!signal) return 'classic';
  if (signal.mode === 'scalp' || signal.analysisMode === 'scalp') return 'scalp';
  return 'classic';
}

// [mode_split_v2] mode별 파일 경로 반환
// TRACKER: 단일 파일 (signal_tracker.json) — mode 필드로 내부 구분
// PERFORMANCE/RESULT: mode별 분리 유지
function getFILES(mode) {
  const m = VERIFIER_MODES.includes(mode) ? mode : VERIFIER_DEFAULT;
  const suffix = m === VERIFIER_DEFAULT ? '' : '_' + m;
  return {
    // [mode_split_v2] TRACKER는 항상 단일 파일 — suffix 제거
    TRACKER:       path.join(DATA_DIR, 'signal_tracker.json'),
    PERFORMANCE:   path.join(DATA_DIR, 'performance_summary' + suffix + '.json'),
    RESULT:        path.join(DATA_DIR, 'analysis_result' + suffix + '.json'),
    // accuracy 에러 로그는 mode 공통 (기존 경로 유지)
    ACCURACY_ERRORS: path.join(DATA_DIR, 'accuracy_sync_errors.jsonl'),
    // [mode_split_v2] 현재 mode 식별자 (성능 집계 필터용)
    MODE: m,
  };
}

// ============================================================
// [accuracy_sync_root_fix] accuracy 기록 실패 → 에러 파일 기록
// ============================================================

/**
 * accuracy 기록 실패를 accuracy_sync_errors.jsonl에 append.
 * 실패 원인, 신호 ID, 단계(stage)를 기록하여 나중에 수동 복구 가능하게 함.
 *
 * @param {string} signalId
 * @param {string} stage       - 'import_failed' | 'snapshot_failed' | 'record_failed'
 * @param {string} errorMessage
 * @param {string} accuracyErrorsPath - ACCURACY_ERRORS 파일 경로
 */
function logAccuracyError(signalId, stage, errorMessage, accuracyErrorsPath) {
  const entry = {
    _errorAt: new Date().toISOString(),
    signalId,
    stage,
    error: errorMessage
  };
  try {
    fs.appendFileSync(accuracyErrorsPath, JSON.stringify(entry) + '\n', 'utf-8');
  } catch (writeErr) {
    // 에러 파일 기록도 실패하면 콘솔에만 남김 (무한 재귀 방지)
    console.error('[ACCURACY_ERROR_LOG] 에러 파일 기록 실패:', writeErr.message);
  }
}

/**
 * [accuracy_sync_root_fix] accuracy_tracker를 안전하게 호출하는 래퍼.
 * 
 * 재발 방지 3단계:
 *   1. accuracy_tracker.mjs 동적 import — 실패 시 ACCURACY_ERRORS에 기록
 *   2. ensureSnapshotExists 호출 — dataset에 snapshot 없으면 자동 생성
 *   3. recordAccuracyVerification 호출 — 실패 시 ACCURACY_ERRORS에 기록
 * 
 * tracker status 변경 후 accuracy 기록이 silent fail로 분리되는 문제를 원천 차단.
 *
 * [mode_split] files 파라미터 추가 — mode별 accuracy_errors 경로 지원.
 * 
 * @param {Object} signal      - signal_tracker의 신호 객체 (verifiedAt 등 포함)
 * @param {string} newStatus   - 새 상태 (success|fail|partial_success)
 * @param {string} outcome     - outcome 값 (동일 문자열)
 * @param {Object} files       - getFILES(mode) 결과 객체
 */
async function recordAccuracyVerificationSafe(signal, newStatus, outcome, files) {
  let trackerModule;

  // 1단계: 동적 import
  try {
    trackerModule = await import('./accuracy_tracker.mjs');
  } catch (importErr) {
    console.error(`[ACCURACY_SAFE] accuracy_tracker.mjs import 실패 (signalId=${signal.signalId}):`, importErr.message);
    logAccuracyError(signal.signalId, 'import_failed', importErr.message, files.ACCURACY_ERRORS);
    return; // import 실패 시 이후 단계 불가
  }

  const { ensureSnapshotExists, recordAccuracyVerification } = trackerModule;

  // 2단계: dataset snapshot 자동 보강
  if (typeof ensureSnapshotExists === 'function') {
    try {
      ensureSnapshotExists(signal);
    } catch (snapshotErr) {
      console.error(`[ACCURACY_SAFE] snapshot 보강 실패 (signalId=${signal.signalId}):`, snapshotErr.message);
      logAccuracyError(signal.signalId, 'snapshot_failed', snapshotErr.message, files.ACCURACY_ERRORS);
      // snapshot 실패해도 recordAccuracyVerification은 계속 시도
    }
  }

  // 3단계: 검증 기록
  try {
    recordAccuracyVerification(signal, newStatus, outcome);
  } catch (recordErr) {
    console.error(`[ACCURACY_SAFE] recordAccuracyVerification 실패 (signalId=${signal.signalId}):`, recordErr.message);
    logAccuracyError(signal.signalId, 'record_failed', recordErr.message, files.ACCURACY_ERRORS);
  }
}

// ============ 공통 유틸 ============

/**
 * Binance klines 조회 (고가/저가 포함)
 */
async function fetchKlines(symbol = 'BTCUSDT', interval = '1h', limit = 100) {
  const url = `${BINANCE_BASE}/api/v3/klines?symbol=${symbol}&interval=${interval}&limit=${limit}`;

  try {
    const response = await fetch(url, {
      signal: AbortSignal.timeout(API_TIMEOUT)
    });

    if (!response.ok) return null;

    return await response.json();
  } catch (error) {
    console.error('[API_ERROR] klines 조회 실패:', error.message);
    return null;
  }
}

function loadJson(filepath, defaultValue = null) {
  try {
    if (fs.existsSync(filepath)) {
      const raw = fs.readFileSync(filepath, 'utf-8');
      const parsed = JSON.parse(raw);

      // ⚠️ 호환 레이어: { signals: [...] } 객체 → 배열로 변환
      if (parsed && !Array.isArray(parsed) && Array.isArray(parsed.signals)) {
        return parsed.signals;
      }
      return parsed;
    }
  } catch (e) {
    console.error(`[FILE_ERROR] ${filepath}:`, e.message);
  }
  return defaultValue;
}

function saveJson(filepath, data) {
  try {
    // ⚠️ 호환 레이어: signal_tracker.json이면 배열 → { signals: [...] }로 래핑
    const isTracker = filepath.endsWith('signal_tracker.json');
    const toSave = isTracker && Array.isArray(data)
      ? { signals: data }
      : data;

    fs.writeFileSync(filepath, JSON.stringify(toSave, null, 2), 'utf-8');
    return true;
  } catch (e) {
    console.error(`[FILE_SAVE_ERROR] ${filepath}:`, e.message);
    return false;
  }
}

// ============ 검증 로직 ============

/**
 * 신호 발생 후 경과 시간 계산
 */
function getElapsedHours(signalTimestamp) {
  const signalTime = new Date(signalTimestamp).getTime();
  const now = Date.now();
  return (now - signalTime) / 1000 / 60 / 60;
}

/**
 * 검증 구간 내 고가/저가 조회
 * 신호 발생 시점 ~ 현재까지의 1시간봉에서 고가/저가를 구간별로 조회
 * 
 * @param {string} signalTimestamp - 신호 발생 시각 (ISO string)
 * @returns {Promise<{high: number, low: number, from: Date, to: Date}>}
 */
async function fetchHighLowSince(signalTimestamp) {
  const signalTime = new Date(signalTimestamp);
  const now = new Date();

  // 신호 발생 시점부터 현재까지 조회 (최대 500개 = ~21일)
  const klines = await fetchKlines('BTCUSDT', '1h', 500);

  if (!klines || klines.length === 0) {
    // 대체: ticker로 현재가 + 24시간 전 데이터
    const recent = await fetchKlines('BTCUSDT', '1h', 24);
    if (!recent || recent.length === 0) {
      return { high: null, low: null, from: signalTime, to: now };
    }
    const highs = recent.map(k => parseFloat(k[2]));
    const lows = recent.map(k => parseFloat(k[3]));
    return {
      high: Math.max(...highs),
      low: Math.min(...lows),
      from: new Date(parseInt(recent[0][0])),
      to: new Date(parseInt(recent[recent.length - 1][0]))
    };
  }

  // 신호 발생 시점 이후 데이터만 필터
  const sinceSignal = klines.filter(k => new Date(parseInt(k[0])) >= signalTime);

  if (sinceSignal.length === 0) {
    // 신호가 가장 마지막 봉 이후에 발생 → 가장 최근 봉의 데이터 사용
    const lastKline = klines[klines.length - 1];
    const high = parseFloat(lastKline[2]);
    const low = parseFloat(lastKline[3]);
    return {
      high,
      low,
      from: signalTime,
      to: now
    };
  }

  const highs = sinceSignal.map(k => parseFloat(k[2]));
  const lows = sinceSignal.map(k => parseFloat(k[3]));

  return {
    high: Math.max(...highs),
    low: Math.min(...lows),
    from: new Date(parseInt(sinceSignal[0][0])),
    to: new Date(parseInt(sinceSignal[sinceSignal.length - 1][0]))
  };
}

/**
 * 매수 신호 판정
 * ⚠️ 현재가 단일 점이 아니라 검증 구간의 고가/저가로 판정
 * 
 * 상태값: pending | success | partial_success | fail | invalid
 * 
 * [step3] partial_success 세분화 필드:
 *   - partialTp2Progress: TP1 도달 후 TP2까지 진행률 (0.000~0.999)
 *   - partialBand: tp1_only | mid_to_tp2 | near_tp2
 *   - verificationHigh/Low/WindowFrom/To: 공통 검증 필드
 * 
 * @param {Object} signal - 신호 객체
 * @param {number} high - 검증 구간 고가
 * @param {number} low - 검증 구간 저가
 * @param {number} currentPrice - 현재가 (체크포인트용)
 * @param {Date|string} [windowFrom] - 검증 구간 시작 시각
 * @param {Date|string} [windowTo] - 검증 구간 종료 시각
 */
function evaluateBuySignal(signal, high, low, currentPrice, windowFrom, windowTo) {
  const { stopLoss, target1, target2 } = signal;

  // --- 공통 검증 필드 (모든 판정 결과에 포함) ---
  const verificationMeta = {
    verificationHigh: high,
    verificationLow: low,
    verificationWindowFrom: windowFrom instanceof Date ? windowFrom.toISOString() : (windowFrom || null),
    verificationWindowTo: windowTo instanceof Date ? windowTo.toISOString() : (windowTo || null)
  };

  // 손절가 도달: 검증 구간 저가가 손절가 이하
  if (low <= stopLoss) {
    return {
      result: 'fail',
      reason: `손절가(${stopLoss}) 도달 - 구간 저가 ${low}`,
      failCause: determineFailCause(signal, 'stop_loss'),
      ...verificationMeta,
      partialTp2Progress: null,
      partialBand: null
    };
  }

  // 2차 목표가 도달: 검증 구간 고가가 목표가 이상
  if (high >= target2) {
    return {
      result: 'success',
      reason: `2차 목표가(${target2}) 도달 - 구간 고가 ${high.toFixed(2)}`,
      ...verificationMeta,
      partialTp2Progress: null,
      partialBand: null
    };
  }

  // 1차 목표가만 도달: 검증 구간 고가가 1차 목표가는 도달, 2차는 미도달
  if (high >= target1) {
    // --- partialTp2Progress 계산 (BUY) ---
    // BUY: progress = (high - target1) / (target2 - target1)
    // TP1 막 터치 → 0에 가까움, TP2 직전 → 1에 가까움
    let partialTp2Progress = null;
    let partialBand = null;
    const denominator = target2 - target1;
    if (denominator > 0) {
      partialTp2Progress = (high - target1) / denominator;
      partialTp2Progress = Math.max(0, Math.min(0.999, partialTp2Progress)); // clamp to 0~0.999
      partialTp2Progress = Math.round(partialTp2Progress * 1000) / 1000;     // 소수점 3자리

      if (partialTp2Progress < 0.33) {
        partialBand = 'tp1_only';
      } else if (partialTp2Progress < 0.66) {
        partialBand = 'mid_to_tp2';
      } else {
        partialBand = 'near_tp2';
      }
    }

    const bandLabel = partialBand ? ` (${partialBand})` : '';
    const progressLabel = partialTp2Progress !== null
      ? `TP2 진행률 ${partialTp2Progress.toFixed(3)}${bandLabel}`
      : 'TP2 진행률 산출 불가';

    return {
      result: 'partial_success',
      reason: `1차 목표가(${target1}) 도달, ${progressLabel}`,
      ...verificationMeta,
      partialTp2Progress,
      partialBand
    };
  }

  // 아직 도달 안 함
  return {
    result: 'pending',
    reason: `아직 목표가(${target1}/${target2}) 미도달 - 구간 고가 ${high.toFixed(2)}, 저가 ${low.toFixed(2)}`,
    ...verificationMeta,
    partialTp2Progress: null,
    partialBand: null
  };
}

/**
 * 매도 신호 판정
 * ⚠️ 현재가 단일 점이 아니라 검증 구간의 고가/저가로 판정
 * 
 * 상태값: pending | success | partial_success | fail | invalid
 * 
 * [step3] partial_success 세분화 필드:
 *   - partialTp2Progress: TP1 도달 후 TP2까지 진행률 (0.000~0.999)
 *   - partialBand: tp1_only | mid_to_tp2 | near_tp2
 *   - verificationHigh/Low/WindowFrom/To: 공통 검증 필드
 * 
 * @param {Object} signal - 신호 객체
 * @param {number} high - 검증 구간 고가
 * @param {number} low - 검증 구간 저가
 * @param {number} currentPrice - 현재가 (체크포인트용)
 * @param {Date|string} [windowFrom] - 검증 구간 시작 시각
 * @param {Date|string} [windowTo] - 검증 구간 종료 시각
 */
function evaluateSellSignal(signal, high, low, currentPrice, windowFrom, windowTo) {
  const { stopLoss, target1, target2 } = signal;

  // --- 공통 검증 필드 (모든 판정 결과에 포함) ---
  const verificationMeta = {
    verificationHigh: high,
    verificationLow: low,
    verificationWindowFrom: windowFrom instanceof Date ? windowFrom.toISOString() : (windowFrom || null),
    verificationWindowTo: windowTo instanceof Date ? windowTo.toISOString() : (windowTo || null)
  };

  // 손절가 도달: 검증 구간 고가가 손절가 이상 (상승하여 손절)
  if (high >= stopLoss) {
    return {
      result: 'fail',
      reason: `손절가(${stopLoss}) 도달 - 구간 고가 ${high.toFixed(2)}`,
      failCause: determineFailCause(signal, 'stop_loss'),
      ...verificationMeta,
      partialTp2Progress: null,
      partialBand: null
    };
  }

  // 2차 목표가 도달: 검증 구간 저가가 목표가 이하 (하락하여 목표 도달)
  if (low <= target2) {
    return {
      result: 'success',
      reason: `2차 목표가(${target2}) 도달 - 구간 저가 ${low.toFixed(2)}`,
      ...verificationMeta,
      partialTp2Progress: null,
      partialBand: null
    };
  }

  // 1차 목표가만 도달: 검증 구간 저가는 1차 목표가는 도달, 2차는 미도달
  if (low <= target1) {
    // --- partialTp2Progress 계산 (SELL) ---
    // SELL: progress = (target1 - low) / (target1 - target2)
    // TP1 막 터치 → 0에 가까움, TP2 직전 → 1에 가까움
    let partialTp2Progress = null;
    let partialBand = null;
    const denominator = target1 - target2;
    if (denominator > 0) {
      partialTp2Progress = (target1 - low) / denominator;
      partialTp2Progress = Math.max(0, Math.min(0.999, partialTp2Progress)); // clamp to 0~0.999
      partialTp2Progress = Math.round(partialTp2Progress * 1000) / 1000;     // 소수점 3자리

      if (partialTp2Progress < 0.33) {
        partialBand = 'tp1_only';
      } else if (partialTp2Progress < 0.66) {
        partialBand = 'mid_to_tp2';
      } else {
        partialBand = 'near_tp2';
      }
    }

    const bandLabel = partialBand ? ` (${partialBand})` : '';
    const progressLabel = partialTp2Progress !== null
      ? `TP2 진행률 ${partialTp2Progress.toFixed(3)}${bandLabel}`
      : 'TP2 진행률 산출 불가';

    return {
      result: 'partial_success',
      reason: `1차 목표가(${target1}) 도달, ${progressLabel}`,
      ...verificationMeta,
      partialTp2Progress,
      partialBand
    };
  }

  // 아직 도달 안 함
  return {
    result: 'pending',
    reason: `아직 목표가(${target1}/${target2}) 미도달 - 구간 고가 ${high.toFixed(2)}, 저가 ${low.toFixed(2)}`,
    ...verificationMeta,
    partialTp2Progress: null,
    partialBand: null
  };
}

/**
 * 실패 원인 판정
 */
function determineFailCause(signal, type) {
  const causes = [];

  const slDistance = Math.abs(signal.entryPrice - signal.stopLoss);
  const atr = slDistance / 1.5;
  const targetDistance = Math.abs(signal.target1 - signal.entryPrice);

  if (slDistance > atr * 2) {
    causes.push('손절 기준 지나치게 넓음');
  }

  if (targetDistance < slDistance * 2) {
    causes.push('리스크 대비 보상 부족');
  }

  if (causes.length === 0) {
    causes.push('가짜 돌파');
  }

  return causes[0];
}

/**
 * 신호 검증 실행
 * [mode_split_v2] 단일 signal_tracker.json + mode 필터 기반
 * @param {string} [mode] - 분석 모드 (省略時: VERIFIER_MODE 전역 기본값)
 */
async function verifyPendingSignals(mode) {
  const curMode = mode || VERIFIER_MODE;
  const files = getFILES(curMode);
  console.log(`[VERIFY][${curMode}] 신호 검증 시작`);
  // [mode_split_v2] 단일 tracker에서 전체 신호 로드 후 mode 필터
  const allSignals = loadJson(files.TRACKER, []);
  const tracker = allSignals.filter(s => normalizeSignalMode(s) === curMode);

  if (tracker.length === 0) {
    console.log(`[VERIFY][${curMode}] 검증할 미검증 신호 없음`);
    return;
  }

  console.log('═'.repeat(60));
  console.log(`[VERIFY][${curMode}] 과거 신호 검증 시작...`);
  console.log(`[VERIFY] 미검증 신호: ${tracker.length}개`);
  console.log('═'.repeat(60));

  // 현재가 조회 (체크포인트용)
  const klinesNow = await fetchKlines('BTCUSDT', '1h', 1);
  const currentPrice = klinesNow
    ? parseFloat(klinesNow[klinesNow.length - 1][4])
    : null;

  if (!currentPrice) {
    console.error('[VERIFY_ERROR] 현재가 조회 실패 - 검증 중단');
    return;
  }

  console.log(`[VERIFY] 현재 BTC 가격: ${currentPrice.toLocaleString()} USDT\n`);

  let verifiedCount = 0;
  // [mode_split_v2] allSignals 내 signal 참조로 updatedTracker 구성
  const updatedTracker = [];
  const processedIds = new Set();

  for (const signal of tracker) {
    // [mode_split_v2] allSignals 내 원본 참조 추적 — 중복 방지
    if (processedIds.has(signal.signalId)) continue;
    processedIds.add(signal.signalId);

    const elapsedHours = getElapsedHours(signal.timestamp);
    console.log(`[CHECK] ${signal.signalId} (${elapsedHours.toFixed(1)}시간 경과)`);
    console.log(`  진입: ${signal.entryPrice} | 손절: ${signal.stopLoss} | 1차: ${signal.target1} | 2차: ${signal.target2}`);

    // ⚠️ 수정 4: rr1, rr2가 없으면 tracker에서 계산하여 추가
    if ((signal.rr1 === undefined || signal.rr2 === undefined) && signal.stopLoss && signal.target1 && signal.target2) {
      const rr1 = Math.abs(signal.target1 - signal.entryPrice) / Math.abs(signal.entryPrice - signal.stopLoss);
      const rr2 = Math.abs(signal.target2 - signal.entryPrice) / Math.abs(signal.entryPrice - signal.stopLoss);
      signal.rr1 = Math.round(rr1 * 100) / 100;
      signal.rr2 = Math.round(rr2 * 100) / 100;
      console.log(`  [추가] rr1: ${signal.rr1}, rr2: ${signal.rr2} (계산됨)`);
    }

    // 이미 검증 완료된 신호는 통과
    if (signal.status !== 'pending') {
      console.log(`  → 상태: ${signal.status} (스킵)\n`);
      updatedTracker.push(signal);
      continue;
    }

    // ⚠️ 수정 4: 검증 구간 고가/저가 조회 (windowFrom/windowTo 포함)
    const hlData = await fetchHighLowSince(signal.timestamp);

    if (hlData.high === null || hlData.low === null) {
      console.error(`  [WARN] 고가/저가 조회 실패 - 스킵\n`);
      updatedTracker.push(signal);
      continue;
    }

    const { high, low, from: windowFrom, to: windowTo } = hlData;
    console.log(`  검증 구간: ${low.toFixed(2)} ~ ${high.toFixed(2)} (고가/저가 기반 판정)`);

    // 고가/저가로 평가 (windowFrom/windowTo 전달 — step3 partial_success 세분화)
    const evaluator = signal.direction === 'buy' ? evaluateBuySignal : evaluateSellSignal;
    const evalResult = evaluator(signal, high, low, currentPrice, windowFrom, windowTo);

    console.log(`  판정: ${evalResult.result} → 이유: ${evalResult.reason}\n`);

    // 상태 업데이트
    signal.checkpoints = signal.checkpoints || {};
    signal.checkpoints['1h'] = signal.checkpoints['1h'] || { checked: false, result: null, price: null, timestamp: null };
    signal.checkpoints['4h'] = signal.checkpoints['4h'] || { checked: false, result: null, price: null, timestamp: null };
    signal.checkpoints['24h'] = signal.checkpoints['24h'] || { checked: false, result: null, price: null, timestamp: null };

    // 1차 검증 (1시간 후)
    if (elapsedHours >= 1 && !signal.checkpoints['1h'].checked) {
      signal.checkpoints['1h'] = {
        checked: true,
        result: evalResult.result,
        price: currentPrice,
        high: high,
        low: low,
        timestamp: new Date().toISOString()
      };
    }

    // 2차 검증 (4시간 후)
    if (elapsedHours >= 4 && !signal.checkpoints['4h'].checked) {
      signal.checkpoints['4h'] = {
        checked: true,
        result: evalResult.result,
        price: currentPrice,
        high: high,
        low: low,
        timestamp: new Date().toISOString()
      };
    }

    // 최종 검증 (24시간 후)
    if (elapsedHours >= 24 && !signal.checkpoints['24h'].checked) {
      signal.checkpoints['24h'] = {
        checked: true,
        result: evalResult.result,
        price: currentPrice,
        high: high,
        low: low,
        timestamp: new Date().toISOString()
      };
    }

    // ⚠️ 수정 3: 상태값 통일 - partial_success로
    // 최종 상태 결정 — step3: 새 필드 추가
    if (evalResult.result !== 'pending') {
      signal.status = evalResult.result;  // success | partial_success | fail | invalid
      signal.resultReason = evalResult.reason;
      signal.failCause = evalResult.failCause || null;
      signal.verifiedAt = new Date().toISOString();
      // --- [Schema v2] ---
      signal.updatedAt = new Date().toISOString();
      signal.verifiedPrice = currentPrice;
      // --- [step3] 공통 검증 필드 ---
      signal.verificationHigh = evalResult.verificationHigh;
      signal.verificationLow = evalResult.verificationLow;
      signal.verificationWindowFrom = evalResult.verificationWindowFrom;
      signal.verificationWindowTo = evalResult.verificationWindowTo;
      // --- [step3] partial_success 세분화 필드 ---
      signal.partialTp2Progress = evalResult.partialTp2Progress;   // null when not partial_success
      signal.partialBand = evalResult.partialBand;                 // null when not partial_success
      verifiedCount++;

      // performance_summary 갱신
      // [fix-engine-tracker-typeerror] files 인자 추가 — 누락 시 files.TRACKER → TypeError
      updatePerformanceSummary(evalResult.result, evalResult.failCause, signal, files);

      // [accuracy_sync_root_fix] 정확도 축적 기록 — 안전 래퍼 사용
      // 기존: try/catch silent → 이제: 실패 시 accuracy_sync_errors.jsonl 기록
      await recordAccuracyVerificationSafe(signal, evalResult.result, evalResult.result, files);
    }

    updatedTracker.push(signal);
  }

  // [mode_split_v2] tracker 저장: 단일 signal_tracker.json에 전체 신호 보존
  // allSignals 내 signal 참조는 이미 위에서 직접 수정됨 → updatedTracker 순서와 무관하게 전체 저장
  saveJson(files.TRACKER, allSignals);

  console.log('═'.repeat(60));
  console.log(`[VERIFY][${curMode}] 검증 완료: ${verifiedCount}개 신호 판정됨`);
  console.log('═'.repeat(60));
}

/**
 * performance_summary 갱신 — 트래커 재스캔 방식
 * ==============================================================
 * 모든 카운터(successCount, partialSuccessCount 등)는 tracker.json의
 * 현재 status를 직접 읽어 매번 재계산한다.
 *
 * 설계 원칙:
 *   1. tracker가 신호 상태의 단일 진실 공급원(source of truth)
 *   2. _prevResults 같은 인메모리 상태 없음 → 재시작해도 정확
 *   3. partial_success → success 전환: tracker.status만 추적하면 정확
 *   4. updatePerformanceSummary() 호출 시마다 전체 재계산 (증분 없음)
 *
 * 한계: consecutiveSuccess/Fail은 tracker에서 복원 불가 (재시작 시 끊김)
 *       → 이것은 보조 지표로만 사용, 핵심 지표(totalSuccessRate, winRate)에 무영향
 * ==============================================================
 */
function updatePerformanceSummary(result, failCause, signal, files) {
  // ── [mode_split_v2] tracker 재스캔: 모든 카운터의 근원 ──────────────────────
  // 단일 signal_tracker.json에서 mode별 필터 적용
  const allSignals = loadJson(files.TRACKER, []);
  const tracker = allSignals.filter(s => normalizeSignalMode(s) === files.MODE);

  // verifiedSignals: pending 제외한 모든 신호
  const verifiedSignals = tracker.filter(s =>
    s.direction !== 'neutral' && s.direction !== undefined &&
    s.status && s.status !== 'pending'
  );

  // 각 상태별 카운트 (tracker status 기반 직접 COUNT)
  const successCount        = verifiedSignals.filter(s => s.status === 'success').length;
  const partialSuccessCount = verifiedSignals.filter(s => s.status === 'partial_success').length;
  const failCount           = verifiedSignals.filter(s => s.status === 'fail').length;
  const invalidCount        = verifiedSignals.filter(s => s.status === 'invalid').length;
  const cancelledCount      = verifiedSignals.filter(s => s.status === 'cancelled').length;

  // totalSignals: 모든 비중립 신호
  const totalSignals = tracker.filter(s =>
    s.direction !== 'neutral' && s.direction !== undefined
  ).length;

  // ── 비율 계산 ─────────────────────────────────────────────────────────────
  const vCount = verifiedSignals.length;
  const totalSuccessRate = vCount > 0
    ? Math.round((successCount + partialSuccessCount * 0.5) / vCount * 1000) / 10
    : 0;
  const winRate = vCount > 0
    ? Math.round(successCount / vCount * 1000) / 10
    : 0;

  // ── 최근 N회 성공률 ────────────────────────────────────────────────────────
  const sorted = [...verifiedSignals].sort(
    (a, b) => new Date(b.timestamp) - new Date(a.timestamp)
  );

  function recentRate(count) {
    if (sorted.length === 0) return 0;
    const recent = sorted.slice(0, count);
    const s = recent.filter(x => x.status === 'success').length;
    const p = recent.filter(x => x.status === 'partial_success').length;
    return Math.round((s + p * 0.5) / recent.length * 1000) / 10;
  }

  // ── 평균 RR ────────────────────────────────────────────────────────────────
  const rrSignals = tracker.filter(s => s.rr1 !== undefined && s.rr2 !== undefined);
  const avgRR1 = rrSignals.length > 0
    ? Math.round(rrSignals.reduce((sum, s) => sum + s.rr1, 0) / rrSignals.length * 100) / 100
    : 0;
  const avgRR2 = rrSignals.length > 0
    ? Math.round(rrSignals.reduce((sum, s) => sum + s.rr2, 0) / rrSignals.length * 100) / 100
    : 0;

  // ── 실패 원인 통계 ─────────────────────────────────────────────────────────
  const failCauseStats = {
    '거래량 부족': 0, '가짜 돌파': 0, '상위 추세 역행': 0,
    '지지선 이탈': 0, '저항선 돌파 실패': 0, '과열 구간 추격': 0,
    '횡보장 오판': 0, '시간 프레임 충돌': 0, '리스크 대비 보상 부족': 0
  };
  tracker
    .filter(s => s.failCause && failCauseStats[s.failCause] !== undefined)
    .forEach(s => failCauseStats[s.failCause]++);

  // ── [step3] partial_success 세분화 집계 ─────────────────────────────────
  // ⚠️ performance 계산식은 변경 안 함 (success=1, partial=0.5, fail=0 유지)
  // partialSuccessBreakdown은 보조 정보로만 추가
  //
  // [partial_breakdown_fix] partialBand fallback 계산
  // 문제: 기존 checkpoint 방식으로 partial_success 판정된 신호(구버전 verify 경로)는
  //   signal_tracker에 partialBand/partialTp2Progress 필드가 null로 저장됨.
  //   이 경우 단순 s.partialBand 비교로는 breakdown 집계가 불가능.
  //
  // 해결: partialBand가 null인 partial_success 신호에 대해
  //   verificationHigh / target1 / target2 (BUY) 또는
  //   verificationLow / target1 / target2 (SELL) 기반으로 band를 재계산.
  //   이 함수는 evaluateBuySignal/evaluateSellSignal의 band 계산과 완전 동일한 로직을 사용.
  //   재계산된 band는 tracker에 저장하지 않고 집계 전용으로만 사용.
  //   앞으로 새로운 partial_success는 autoVerifyPendingSignals에서 partialBand가
  //   정상 저장되므로 fallback 경로는 구버전 신호만 처리.

  function resolvePartialBand(signal) {
    // 이미 band가 있으면 그대로 반환
    if (signal.partialBand) return signal.partialBand;

    // band가 null이면 verificationHigh/Low와 target 기반으로 재계산
    const t1 = signal.target1;
    const t2 = signal.target2;
    if (!t1 || !t2) return null;

    const denominator = Math.abs(t2 - t1);
    if (denominator <= 0) return null;

    let progress;
    const dir = (signal.direction || '').toLowerCase();
    if (dir === 'buy') {
      const high = signal.verificationHigh;
      if (high == null || high < t1) return null;
      progress = (high - t1) / denominator;
    } else if (dir === 'sell') {
      const low = signal.verificationLow;
      if (low == null || low > t1) return null;
      progress = (t1 - low) / denominator;
    } else {
      return null;
    }

    progress = Math.max(0, Math.min(0.999, progress));
    if (progress < 0.33) return 'tp1_only';
    if (progress < 0.66) return 'mid_to_tp2';
    return 'near_tp2';
  }

  const partialSignals = verifiedSignals.filter(s => s.status === 'partial_success');
  const partialSuccessBreakdown = {
    tp1_only:    partialSignals.filter(s => resolvePartialBand(s) === 'tp1_only').length,
    mid_to_tp2:  partialSignals.filter(s => resolvePartialBand(s) === 'mid_to_tp2').length,
    near_tp2:    partialSignals.filter(s => resolvePartialBand(s) === 'near_tp2').length
  };

  // ── 최종 performance_summary 구성 ────────────────────────────────────────
  const perf = {
    totalSignals,
    verifiedSignals: vCount,
    successCount,
    partialSuccessCount,
    failCount,
    invalidCount,
    cancelledCount,
    totalSuccessRate,
    winRate,
    recent10SuccessRate: recentRate(10),
    recent20SuccessRate: recentRate(20),
    avgRR1,
    avgRR2,
    consecutiveSuccess: 0,  // 재시작 시 복원 불가 → 0으로 리셋
    consecutiveFail: 0,      // 재시작 시 복원 불가 → 0으로 리셋
    lastSignalResult: result,
    failCauseStats,
    // --- [step3] partial_success 세분화 보조 정보 ---
    partialSuccessBreakdown
  };

  saveJson(files.PERFORMANCE, perf);
  console.log(
    `[PERF] 갱신: total=${totalSignals} verified=${vCount} ` +
    `success=${successCount} partial=${partialSuccessCount} fail=${failCount} ` +
    `성공률=${totalSuccessRate}% winRate=${winRate}%`
  );
}

/**
 * 자동 재평가 엔진 (핵심 함수)
 * ==============================================================
 * 앱이 analyze 완료 후 / 새 신호 추가 후 / 주기적으로 호출하는 함수.
 * 수동 검증과 완전 동일한 판정 로직을 사용하되, 로그 수준만 간결.
 * 
 * [mode_split_v2] 단일 signal_tracker.json + mode 필터 기반
 * 
 * @param {Object} opts
 * @param {boolean} opts.quiet       - true: 체크포인트 로그 생략
 * @param {boolean} opts.autoVerified - true: auto_verified 플래그 추가
 * @param {string} [mode] - 분석 모드 (省略時: VERIFIER_MODE 전역 기본값)
 * @returns {Promise<{updated: number, summary: Object}>}
 * ==============================================================
 */
async function autoVerifyPendingSignals(opts = {}, mode) {
  const { quiet = false, autoVerified = false } = opts;
  const curMode = mode || VERIFIER_MODE;
  const files = getFILES(curMode);

  // [mode_split_v2] 단일 tracker에서 전체 신호 로드 후 mode 필터
  const allSignals = loadJson(files.TRACKER, []);
  const tracker = allSignals.filter(s => normalizeSignalMode(s) === curMode);

  if (tracker.length === 0) {
    if (!quiet) console.log(`[AUTO_VERIFY][${curMode}] 추적 중인 신호 없음`);
    return { updated: 0, summary: {} };
  }

  const klinesNow = await fetchKlines('BTCUSDT', '1h', 1);
  const currentPrice = klinesNow
    ? parseFloat(klinesNow[klinesNow.length - 1][4])
    : null;

  if (!currentPrice) {
    if (!quiet) console.error('[AUTO_VERIFY_ERROR] 현재가 조회 실패');
    return { updated: 0, summary: { error: 'price_fetch_failed' } };
  }

  let updatedCount = 0;
  // [mode_split_v2] allSignals 내 signal 참조로 updatedTracker 구성
  const updatedTracker = [];
  const processedIds = new Set();

  for (const signal of tracker) {
    // [mode_split_v2] allSignals 내 원본 참조 추적 — 중복 방지
    if (processedIds.has(signal.signalId)) continue;
    processedIds.add(signal.signalId);

    const elapsedHours = getElapsedHours(signal.timestamp);

    // 이미 종료된 신호는 통과
    if (signal.status !== 'pending') {
      updatedTracker.push(signal);
      continue;
    }

    const hlData = await fetchHighLowSince(signal.timestamp);

    if (hlData.high === null || hlData.low === null) {
      updatedTracker.push(signal);
      continue;
    }

    const { high, low, from: windowFrom, to: windowTo } = hlData;

    // 판정 (windowFrom/windowTo 전달 — step3 partial_success 세분화)
    const evaluator = signal.direction === 'buy' ? evaluateBuySignal : evaluateSellSignal;
    const evalResult = evaluator(signal, high, low, currentPrice, windowFrom, windowTo);

    if (evalResult.result !== 'pending') {
      signal.status = evalResult.result;
      signal.resultReason = evalResult.reason;
      signal.failCause = evalResult.failCause || null;
      signal.verifiedAt = new Date().toISOString();
      // --- [Schema v2] ---
      signal.updatedAt = new Date().toISOString();
      signal.verifiedPrice = currentPrice;
      // --- [step3] 공통 검증 필드 ---
      signal.verificationHigh = evalResult.verificationHigh;
      signal.verificationLow = evalResult.verificationLow;
      signal.verificationWindowFrom = evalResult.verificationWindowFrom;
      signal.verificationWindowTo = evalResult.verificationWindowTo;
      // --- [step3] partial_success 세분화 필드 ---
      signal.partialTp2Progress = evalResult.partialTp2Progress;   // null when not partial_success
      signal.partialBand = evalResult.partialBand;                 // null when not partial_success
      signal.autoVerified = autoVerified;
      signal.lastAutoVerifyAt = new Date().toISOString();
      updatedCount++;

      // [accuracy_sync_root_fix] accuracy 기록 — 안전 래퍼 사용 (silent catch 제거)
      // 실패 시 accuracy_sync_errors.jsonl에 signalId/stage/error 기록
      await recordAccuracyVerificationSafe(signal, evalResult.result, evalResult.result, files);
    }

    updatedTracker.push(signal);
  }

  // ── [mode_split_v2] 1단계: tracker 먼저 저장 (performance 재계산 기준이 됨)
  // 단일 signal_tracker.json에 전체(allSignals) 보존 — 다른 mode 신호 삭제 안 됨
  saveJson(files.TRACKER, allSignals);

  // ── 2단계: tracker 저장 후 performance_summary 1회 재계산 ──
  // ⚠️ tracker 파일 저장 직후이므로 최신 상태 기준임
  // ⚠️ updatePerformanceSummary 내부에서 files.MODE 기반 필터 재적용
  updatePerformanceSummary(null, null, null, files);

  if (!quiet) {
    console.log(`[AUTO_VERIFY][${curMode}] 완료: ${updatedCount}/${tracker.length}개 갱신 | 현재가 ${currentPrice.toLocaleString()} USDT`);
  }

  return {
    updated: updatedCount,
    summary: {
      currentPrice,
      total: tracker.length,
      updated: updatedCount
    }
  };
}

// ============ CLI 핸들러 ============

const cliArgs = process.argv.slice(2);

// [mode_split] --mode 인자 추출 (verify / auto-verify 모두 공통 사용)
const modeIdx = cliArgs.indexOf('--mode');
const cliMode = (modeIdx >= 0 && VERIFIER_MODES.includes(cliArgs[modeIdx + 1]))
  ? cliArgs[modeIdx + 1]
  : VERIFIER_DEFAULT;

// 첫 번째 positional 인자가 action
const action = cliArgs.find(a => a !== '--mode' && !a.startsWith('--')) || 'verify';

if (action === 'verify') {
  console.log(`[VERIFY][${cliMode}] 신호 검증 시작`);
  verifyPendingSignals(cliMode).catch(console.error);
} else if (action === 'auto-verify') {
  console.log(`[AUTO_VERIFY][${cliMode}] 자동 재평가 시작`);
  autoVerifyPendingSignals({ quiet: false, autoVerified: true }, cliMode).catch(e => {
    console.error('[AUTO_VERIFY_ERROR]', e.message);
    process.exit(1);
  });
} else {
  console.log('사용법:');
  console.log('  node signal_verifier.mjs verify       [--mode classic|scalp]');
  console.log('  node signal_verifier.mjs auto-verify [--mode classic|scalp]');
  console.log('기본값: --mode classic');
}

export { verifyPendingSignals, evaluateBuySignal, evaluateSellSignal, autoVerifyPendingSignals };
