package services

import (
	"log"
	"sort"
	"stocksearch/config"
	"stocksearch/models"
	"sync"
	"sync/atomic"
	"time"
)

// SignalStock 체결강도 상승 감지 시그널 종목
type SignalStock struct {
	models.StockPrice
	PrevCntrStr     float64   `json:"prevCntrStr"`     // 직전 체결강도
	RisingCount     int       `json:"risingCount"`     // 연속 상승 횟수 (10초 단위)
	DetectedAt      time.Time `json:"detectedAt"`      // 감지 시각
	Sentiment       string    `json:"sentiment"`       // 호재/악재/중립/정보없음
	SentimentReason string    `json:"sentimentReason"` // 한 줄 이유
	TargetPrice     int64     `json:"targetPrice"`     // AI 추론 목표가
	TargetReason    string    `json:"targetReason"`    // 목표가 추론 근거 (한 줄)
	RiseScore       int       `json:"riseScore"`       // 상승 확률 점수 (0~100)
	RiseLabel       string    `json:"riseLabel"`       // "매우 높음" / "높음" / ""
	NextDayTrend    string    `json:"nextDayTrend"`    // 익일 추세: "상승" | "하락" | "횡보"
	NextDayConf     string    `json:"nextDayConf"`     // 신뢰도: "높음" | "보통" | "낮음"
	NextDayReason   string    `json:"nextDayReason"`   // 익일 추세 근거 (한 줄)
	// 복합 분석 지표 (체결강도 + 매도잔량 + 거래량 + 가격위치)
	TotalAskVol int64   `json:"totalAskVol"` // 총매도잔량
	TotalBidVol int64   `json:"totalBidVol"` // 총매수잔량
	AskBidRatio float64 `json:"askBidRatio"` // 매도/매수 잔량비 (1 이상=매도우세)
	VolDelta    int64   `json:"volDelta"`    // 당 구간 거래량 증가분 (10초)
	VolRatio    float64 `json:"volRatio"`    // 거래량 증가율 (직전 평균 대비 배수)
	UpperWick   float64 `json:"upperWick"`   // 윗꼬리 비율 (0=없음, 1=전부 윗꼬리)
	PricePos    float64 `json:"pricePos"`    // 장중 가격 위치 % (0=저가, 100=고가)
	SignalType  string  `json:"signalType"`  // "강한매수" | "매수우세" | "물량소화" | "추격위험" | "약한상승"
}

// cntrHistory 종목별 체결강도 이력 (최근 N회)
type cntrHistory struct {
	values []float64 // 오래된 것부터, 최신이 마지막
}

func (h *cntrHistory) push(v float64) {
	h.values = append(h.values, v)
	if len(h.values) > 6 { // 최대 6회(1분) 유지
		h.values = h.values[1:]
	}
}

// risingCount 직전 N회 연속 상승 횟수 반환 (최소 1회 비교 필요)
func (h *cntrHistory) risingCount() int {
	vals := h.values
	if len(vals) < 2 {
		return 0
	}
	count := 0
	for i := len(vals) - 1; i >= 1; i-- {
		if vals[i] > vals[i-1] {
			count++
		} else {
			break
		}
	}
	return count
}

// volumeHist 종목별 거래량 이력 (10초 구간 증가분 추적)
type volumeHist struct {
	last   int64   // 직전 스캔 누적 거래량
	deltas []int64 // 최근 6회 구간 증가분
}

// push 현재 누적 거래량을 기록하고 (구간 증가분, 평균 대비 배수) 반환
func (h *volumeHist) push(current int64) (delta int64, ratio float64) {
	if h.last > 0 && current > h.last {
		delta = current - h.last
		// 비율 계산: 기존 이력 기준 (현재 delta 추가 전)
		if len(h.deltas) > 0 {
			sum := int64(0)
			for _, d := range h.deltas {
				sum += d
			}
			avg := float64(sum) / float64(len(h.deltas))
			if avg > 0 {
				ratio = float64(delta) / avg
			}
		}
		h.deltas = append(h.deltas, delta)
		if len(h.deltas) > 6 {
			h.deltas = h.deltas[1:]
		}
	}
	h.last = current
	return delta, ratio
}

// ScannerService 체결강도 상승 감지 스캐너 서비스
type ScannerService struct {
	kiwoom        *KiwoomClient
	stockSvc      *StockService
	analysis      *AnalysisService
	mu            sync.RWMutex
	enabled       int32 // atomic: 1=켜짐(기본), 0=꺼짐
	signals       []SignalStock
	history       map[string]*cntrHistory // 종목별 체결강도 이력
	volumeHistory map[string]*volumeHist  // 종목별 거래량 이력
	signalCache   map[string]SignalStock  // 종목별 마지막 시그널 (LLM 결과 포함)
	signalExpiry  map[string]time.Time    // 종목별 시그널 만료 시각 (1분)
	// 관심종목 전용 이력/캐시
	watchlistHistory      map[string]*cntrHistory
	watchlistVolHistory   map[string]*volumeHist
	watchlistSignalCache  map[string]SignalStock
	watchlistSignalExpiry map[string]time.Time
	// WS 구독 콜백 (Hub.SubscribeInternal 연결)
	subscribeCallback func([]string)
}

var scannerSvc *ScannerService

// GetScannerService 스캐너 서비스 싱글턴 반환
func GetScannerService() *ScannerService {
	if scannerSvc == nil {
		scannerSvc = &ScannerService{
			kiwoom:                GetKiwoomClient(),
			stockSvc:              GetStockService(),
			analysis:              GetAnalysisService(config.App.GroqAPIKey, config.App.GroqModel),
			history:               make(map[string]*cntrHistory),
			volumeHistory:         make(map[string]*volumeHist),
			signalCache:           make(map[string]SignalStock),
			signalExpiry:          make(map[string]time.Time),
			watchlistHistory:      make(map[string]*cntrHistory),
			watchlistVolHistory:   make(map[string]*volumeHist),
			watchlistSignalCache:  make(map[string]SignalStock),
			watchlistSignalExpiry: make(map[string]time.Time),
		}
		atomic.StoreInt32(&scannerSvc.enabled, 1) // 기본값: 켜짐
	}
	return scannerSvc
}

// Start 스캐너 백그라운드 고루틴 시작
func (s *ScannerService) Start() {
	go s.run()
}

// SetEnabled 스캐너 활성화 여부 설정
func (s *ScannerService) SetEnabled(on bool) {
	if on {
		atomic.StoreInt32(&s.enabled, 1)
	} else {
		atomic.StoreInt32(&s.enabled, 0)
	}
}

// IsEnabled 스캐너 활성화 여부 반환
func (s *ScannerService) IsEnabled() bool {
	return atomic.LoadInt32(&s.enabled) == 1
}

// SetSubscribeCallback 종목 WS 구독 요청 콜백 설정 (Hub.SubscribeInternal 연결)
func (s *ScannerService) SetSubscribeCallback(fn func([]string)) {
	s.mu.Lock()
	s.subscribeCallback = fn
	s.mu.Unlock()
}

// GetSignals 현재 감지된 시그널 종목 목록 반환
func (s *ScannerService) GetSignals() []SignalStock {
	s.mu.RLock()
	defer s.mu.RUnlock()
	result := make([]SignalStock, len(s.signals))
	copy(result, s.signals)
	return result
}

// run 08:00 KST 이후 10초 주기로 스캔 반복 (enabled=0이면 스캔 건너뜀)
func (s *ScannerService) run() {
	kst, _ := time.LoadLocation("Asia/Seoul")
	for {
		now := time.Now().In(kst)
		if now.Hour() < 8 {
			next := time.Date(now.Year(), now.Month(), now.Day(), 8, 0, 0, 0, kst)
			log.Printf("스캐너: 08:00 KST 대기 중 (%v)", next.Format("2006-01-02 15:04:05"))
			time.Sleep(time.Until(next))
		}
		if s.IsEnabled() {
			s.scan()
		}
		time.Sleep(10 * time.Second)
	}
}

// calcRiseScore 4가지 복합 요소 기반 상승 확률 점수 계산 (0~100점)
// A.체결강도(30) + B.연속상승(25) + C.가격위치/캔들(20) + D.거래량건전성(15) + E.매도잔량소화(10)
func calcRiseScore(cntrStr float64, risingCount int, changeRate float64,
	high, low, currentPrice int64, volRatio float64,
	totalAskVol, totalBidVol int64) int {

	score := 0

	// A. 체결강도 레벨 (0~30점)
	switch {
	case cntrStr >= 150:
		score += 30
	case cntrStr >= 130:
		score += 22
	case cntrStr >= 110:
		score += 14
	case cntrStr >= 100:
		score += 7
	}

	// B. 연속 상승 횟수 (0~25점)
	switch {
	case risingCount >= 5:
		score += 25
	case risingCount >= 4:
		score += 20
	case risingCount >= 3:
		score += 14
	case risingCount >= 2:
		score += 8
	case risingCount == 1:
		score += 3
	}

	// C. 가격 위치 및 캔들 형태 (0~20점)
	// C1. 등락률
	switch {
	case changeRate >= 3.0:
		score += 6
	case changeRate >= 1.0:
		score += 4
	case changeRate >= 0.0:
		score += 1
	}
	// C2. 윗꼬리 비율 (0=없음 → 강한 양봉, 클수록 매도 압력)
	if high > low {
		upperWick := float64(high-currentPrice) / float64(high-low)
		switch {
		case upperWick <= 0.10:
			score += 10 // 윗꼬리 거의 없음 = 강한 양봉
		case upperWick <= 0.25:
			score += 6
		case upperWick <= 0.40:
			score += 2
		case upperWick > 0.60:
			score -= 8 // 긴 윗꼬리 = 강한 매도 압력
		}
		// C3. 가격이 고가 80% 이상 위치 → 매수 우세
		pricePos := float64(currentPrice-low) / float64(high-low) * 100
		if pricePos >= 80 {
			score += 4
		}
	}

	// D. 거래량 건전성 (0~15점)
	// 2~5배 증가가 최적, 10배+ 과열은 고점 물량털기 가능성으로 감점
	switch {
	case volRatio >= 2.0 && volRatio < 5.0:
		score += 15 // 건강한 거래량 증가
	case volRatio >= 1.5 && volRatio < 2.0:
		score += 10
	case volRatio >= 1.0 && volRatio < 1.5:
		score += 6
	case volRatio >= 5.0 && volRatio < 10.0:
		score += 4 // 과열 초입
	case volRatio >= 10.0:
		score -= 5 // 폭발적 과열: 고점 물량털기 경계
	case volRatio > 0:
		score += 2
	}

	// E. 매도잔량 소화 여부 (0~10점)
	if totalAskVol > 0 && totalBidVol > 0 {
		bidAskRatio := float64(totalBidVol) / float64(totalAskVol)
		switch {
		case bidAskRatio >= 1.5:
			score += 10 // 매수잔량 압도적: 위 물량 소화 중
		case bidAskRatio >= 1.0:
			score += 6 // 매수 ≥ 매도
		case bidAskRatio >= 0.7:
			score += 2
		default:
			score -= 3 // 매도잔량 크게 우세: 상단 물량 부담
		}
	}

	if score < 0 {
		return 0
	}
	return score
}

// classifySignalType 4가지 요소 조합으로 신호 유형 분류
func classifySignalType(sig *SignalStock) string {
	upperWick := sig.UpperWick
	askBidRatio := sig.AskBidRatio // 1 이상=매도우세, 1 미만=매수우세
	if askBidRatio == 0 {
		askBidRatio = 1.0 // 데이터 없으면 중립으로 취급
	}

	// 추격위험: 체결강도 과열 + 거래량 폭발 + 긴 윗꼬리
	// → 단타 추격매수 몰림 후 고점 물량털기 패턴
	if sig.CntrStr >= 170 && sig.VolRatio >= 7.0 && upperWick >= 0.4 {
		return "추격위험"
	}

	// 강한매수: 체결강도 강함 + 연속상승 + 가격 우상향 + 윗꼬리 없음 + 매도잔량 소화
	// → "사는 쪽이 실제로 강하고, 던지는 물량도 받아내는" 패턴
	if sig.CntrStr >= 130 && sig.RisingCount >= 3 &&
		sig.ChangeRate >= 1.0 && upperWick <= 0.25 && askBidRatio <= 1.0 {
		return "강한매수"
	}

	// 물량소화: 체결강도 높은데 가격이 제자리 + 긴 윗꼬리
	// → 위에서 던지는 물량을 받아내기만 하는 중
	if sig.CntrStr >= 120 && sig.ChangeRate <= 0.5 && upperWick >= 0.35 {
		return "물량소화"
	}

	// 약한상승: 거래량 적고 체결강도도 약함
	// → 얇은 호가에서 뜬 상승, 쉽게 꺾일 수 있음
	if sig.VolRatio < 1.0 && sig.CntrStr < 120 {
		return "약한상승"
	}

	return "매수우세"
}

// scan 거래량 상위 20종목을 조회해 복합 분석으로 시그널 종목 필터링
func (s *ScannerService) scan() {
	stocks, err := s.kiwoom.GetTopVolumeStocks("J", 20)
	if err != nil {
		log.Printf("스캐너 거래량순위 조회 실패: %v", err)
		return
	}

	// 거래량 상위 종목 WS 구독 요청 (캐시 활용을 위해 미리 등록)
	s.mu.RLock()
	cb := s.subscribeCallback
	s.mu.RUnlock()
	if cb != nil {
		codes := make([]string, len(stocks))
		for i, st := range stocks {
			codes[i] = st.Code
		}
		cb(codes)
	}

	var signals []SignalStock

	s.mu.Lock()
	for _, stock := range stocks {
		// ka10003으로 최신 체결강도 조회; 실패 시 순위 응답값 사용
		cntrStr, err := s.kiwoom.getCntrStr(stock.Code)
		if err != nil || cntrStr == 0 {
			cntrStr = stock.CntrStr
		}

		// 체결강도 이력 업데이트
		h, ok := s.history[stock.Code]
		if !ok {
			h = &cntrHistory{}
			s.history[stock.Code] = h
		}
		h.push(cntrStr)

		rising := h.risingCount()
		if rising == 0 {
			continue
		}

		prev := float64(0)
		if len(h.values) >= 2 {
			prev = h.values[len(h.values)-2]
		}

		// 거래량 이력 업데이트 → 구간 증가분 및 증가율 계산
		vh, ok := s.volumeHistory[stock.Code]
		if !ok {
			vh = &volumeHist{}
			s.volumeHistory[stock.Code] = vh
		}
		volDelta, volRatio := vh.push(stock.Volume)

		// 윗꼬리 비율 및 가격 위치 계산
		upperWick, pricePos := 0.5, 50.0 // 데이터 없으면 중간값
		if stock.High > stock.Low {
			upperWick = float64(stock.High-stock.CurrentPrice) / float64(stock.High-stock.Low)
			pricePos = float64(stock.CurrentPrice-stock.Low) / float64(stock.High-stock.Low) * 100
		}

		stock.CntrStr = cntrStr
		signals = append(signals, SignalStock{
			StockPrice:  stock,
			PrevCntrStr: prev,
			RisingCount: rising,
			DetectedAt:  time.Now(),
			VolDelta:    volDelta,
			VolRatio:    volRatio,
			UpperWick:   upperWick,
			PricePos:    pricePos,
		})
	}
	s.mu.Unlock()

	// ── 호가잔량 순차 조회 (체결강도 상승 종목에 한해) ────────────────
	for i := range signals {
		ask, bid, _, err := s.kiwoom.getOrderBook(signals[i].Code)
		if err != nil {
			continue
		}
		signals[i].TotalAskVol = ask
		signals[i].TotalBidVol = bid
		if bid > 0 {
			signals[i].AskBidRatio = float64(ask) / float64(bid)
		}
	}

	// ── 최종 스코어 및 신호 유형 계산 (호가잔량 포함) ────────────────
	for i := range signals {
		sig := &signals[i]
		sig.RiseScore = calcRiseScore(
			sig.CntrStr, sig.RisingCount, sig.ChangeRate,
			sig.High, sig.Low, sig.CurrentPrice,
			sig.VolRatio, sig.TotalAskVol, sig.TotalBidVol,
		)
		sig.SignalType = classifySignalType(sig)
		switch {
		case sig.RiseScore >= 70:
			sig.RiseLabel = "매우 높음"
		case sig.RiseScore >= 50:
			sig.RiseLabel = "높음"
		default:
			sig.RiseLabel = ""
		}
	}

	// ── 1분 유지 캐시 병합 ────────────────────────────────────────────
	const signalTTL = time.Minute
	now := time.Now()

	s.mu.Lock()
	activeCodes := make(map[string]bool, len(signals))
	for i := range signals {
		code := signals[i].Code
		activeCodes[code] = true
		s.signalExpiry[code] = now.Add(signalTTL)
		// 기존 LLM 결과 재사용 (Groq 재호출 방지)
		if cached, ok := s.signalCache[code]; ok && cached.Sentiment != "" {
			signals[i].Sentiment = cached.Sentiment
			signals[i].SentimentReason = cached.SentimentReason
			signals[i].TargetPrice = cached.TargetPrice
			signals[i].TargetReason = cached.TargetReason
			signals[i].NextDayTrend = cached.NextDayTrend
			signals[i].NextDayConf = cached.NextDayConf
			signals[i].NextDayReason = cached.NextDayReason
		}
	}
	// 만료 안 된 이전 시그널 병합
	for code, expiry := range s.signalExpiry {
		if expiry.Before(now) {
			delete(s.signalExpiry, code)
			delete(s.signalCache, code)
			continue
		}
		if !activeCodes[code] {
			signals = append(signals, s.signalCache[code])
		}
	}
	s.mu.Unlock()

	// ── RiseScore 내림차순 정렬 (동점 시 체결강도 기준) ──────────────
	sort.Slice(signals, func(i, j int) bool {
		if signals[i].RiseScore != signals[j].RiseScore {
			return signals[i].RiseScore > signals[j].RiseScore
		}
		return signals[i].CntrStr > signals[j].CntrStr
	})

	// ── LLM 병렬 분석 (shouldAnalyze 통과 종목만, 5초 타임아웃) ──────
	if len(signals) > 0 {
		var wg sync.WaitGroup
		done := make(chan struct{})
		for i := range signals {
			if !shouldAnalyze(&signals[i]) {
				continue
			}
			wg.Add(1)
			go func(idx int) {
				defer wg.Done()
				sig := &signals[idx]
				sentiment, reason := s.analysis.Analyze(sig.Code, sig.Name)
				sig.Sentiment = sentiment
				sig.SentimentReason = reason

				targetPrice, targetReason := s.analysis.PredictTargetPriceFromSignal(
					sig.Code, sig.Name,
					sig.CurrentPrice, sig.High, sig.Low, sig.Open,
					sig.ChangeRate, sig.CntrStr, sig.PrevCntrStr, sig.RisingCount,
					sentiment, reason,
				)
				sig.TargetPrice = targetPrice
				sig.TargetReason = targetReason

				trend, conf, trendReason := s.analysis.PredictNextDayTrend(
					sig.Code, sig.Name,
					sig.CurrentPrice, sig.High, sig.Low, sig.Open,
					sig.ChangeRate, sig.CntrStr, sig.RisingCount,
					sentiment, reason,
				)
				sig.NextDayTrend = trend
				sig.NextDayConf = conf
				sig.NextDayReason = trendReason
			}(i)
		}
		go func() {
			wg.Wait()
			close(done)
		}()
		select {
		case <-done:
		case <-time.After(5 * time.Second):
			log.Printf("스캐너: 감성 분석 5초 타임아웃 (일부 미완료 가능)")
		}
	}

	s.mu.Lock()
	for _, sig := range signals {
		s.signalCache[sig.Code] = sig
	}
	s.signals = signals
	s.mu.Unlock()

	log.Printf("스캐너: 거래량상위20 체결강도 체크 완료 → 시그널 %d개", len(signals))
}

// shouldAnalyze LLM 분석 호출 여부 판단 (Groq API 호출량 절감)
// 상승 확률이 높은 종목만 통과: RiseScore 50+, 2회 이상 연속 상승, 체결강도 100+, 등락률 0% 이상
func shouldAnalyze(sig *SignalStock) bool {
	return sig.RiseScore >= 50 &&
		sig.RisingCount >= 2 &&
		sig.CntrStr >= 100 &&
		sig.ChangeRate >= 0
}

// AnalyzeWatchlist 관심종목 코드 목록에 대해 복합 분석 수행 후 SignalStock 반환
func (s *ScannerService) AnalyzeWatchlist(codes []string) []SignalStock {
	// 분석 전 WS 구독 요청 (다음 사이클부터 캐시 활용 가능)
	s.mu.RLock()
	cb := s.subscribeCallback
	s.mu.RUnlock()
	if cb != nil && len(codes) > 0 {
		cb(codes)
	}

	type interim struct {
		code     string
		cntrStr  float64
		prev     float64
		rising   int
		volDelta int64
		volRatio float64
		price    *models.StockPrice
	}

	// Phase 1: 현재가/체결강도/거래량 이력 순차 수집
	// GetCurrentPrice 내부에서 getCntrStr(ka10003)을 이미 호출하므로 중복 호출 없음
	items := make([]interim, 0, len(codes))
	for _, code := range codes {
		// 현재가 조회 (캐시 활용, 내부적으로 체결강도도 포함)
		sp, err := s.stockSvc.GetCurrentPrice(code)
		if err != nil {
			log.Printf("관심종목 현재가 조회 실패 [%s]: %v", code, err)
			continue
		}

		cntrStr := sp.CntrStr

		// 체결강도 이력 업데이트
		s.mu.Lock()
		h, ok := s.watchlistHistory[code]
		if !ok {
			h = &cntrHistory{}
			s.watchlistHistory[code] = h
		}
		h.push(cntrStr)
		rising := h.risingCount()
		prev := float64(0)
		if len(h.values) >= 2 {
			prev = h.values[len(h.values)-2]
		}

		// 거래량 이력 업데이트
		vh, ok := s.watchlistVolHistory[code]
		if !ok {
			vh = &volumeHist{}
			s.watchlistVolHistory[code] = vh
		}
		volDelta, volRatio := vh.push(sp.Volume)
		s.mu.Unlock()

		items = append(items, interim{
			code:     code,
			cntrStr:  cntrStr,
			prev:     prev,
			rising:   rising,
			volDelta: volDelta,
			volRatio: volRatio,
			price:    sp,
		})
	}

	// Phase 2: SignalStock 슬라이스 구성
	signals := make([]SignalStock, 0, len(items))
	for _, it := range items {
		sp := it.price
		upperWick, pricePos := 0.5, 50.0
		if sp.High > sp.Low {
			upperWick = float64(sp.High-sp.CurrentPrice) / float64(sp.High-sp.Low)
			pricePos = float64(sp.CurrentPrice-sp.Low) / float64(sp.High-sp.Low) * 100
		}
		signals = append(signals, SignalStock{
			StockPrice:  *sp,
			PrevCntrStr: it.prev,
			RisingCount: it.rising,
			DetectedAt:  time.Now(),
			VolDelta:    it.volDelta,
			VolRatio:    it.volRatio,
			UpperWick:   upperWick,
			PricePos:    pricePos,
		})
	}

	// Phase 3: 호가잔량 순차 조회
	for i := range signals {
		ask, bid, _, err := s.kiwoom.getOrderBook(signals[i].Code)
		if err != nil {
			continue
		}
		signals[i].TotalAskVol = ask
		signals[i].TotalBidVol = bid
		if bid > 0 {
			signals[i].AskBidRatio = float64(ask) / float64(bid)
		}
	}

	// Phase 4: 스코어 및 신호 유형 계산
	for i := range signals {
		sig := &signals[i]
		sig.RiseScore = calcRiseScore(
			sig.CntrStr, sig.RisingCount, sig.ChangeRate,
			sig.High, sig.Low, sig.CurrentPrice,
			sig.VolRatio, sig.TotalAskVol, sig.TotalBidVol,
		)
		sig.SignalType = classifySignalType(sig)
		switch {
		case sig.RiseScore >= 70:
			sig.RiseLabel = "매우 높음"
		case sig.RiseScore >= 50:
			sig.RiseLabel = "높음"
		default:
			sig.RiseLabel = ""
		}
	}

	// Phase 5: 1분 TTL 캐시 병합 (LLM 결과 재사용)
	const signalTTL = time.Minute
	now := time.Now()

	s.mu.Lock()
	for i := range signals {
		code := signals[i].Code
		s.watchlistSignalExpiry[code] = now.Add(signalTTL)
		if cached, ok := s.watchlistSignalCache[code]; ok && cached.Sentiment != "" {
			signals[i].Sentiment = cached.Sentiment
			signals[i].SentimentReason = cached.SentimentReason
			signals[i].TargetPrice = cached.TargetPrice
			signals[i].TargetReason = cached.TargetReason
			signals[i].NextDayTrend = cached.NextDayTrend
			signals[i].NextDayConf = cached.NextDayConf
			signals[i].NextDayReason = cached.NextDayReason
		}
	}
	// 만료된 캐시 항목 정리
	for code, expiry := range s.watchlistSignalExpiry {
		if expiry.Before(now) {
			delete(s.watchlistSignalExpiry, code)
			delete(s.watchlistSignalCache, code)
		}
	}
	s.mu.Unlock()

	// Phase 6: LLM 병렬 분석 (shouldAnalyze 통과 종목만, 5초 타임아웃)
	if len(signals) > 0 {
		var wg sync.WaitGroup
		done := make(chan struct{})
		for i := range signals {
			if !shouldAnalyze(&signals[i]) {
				continue
			}
			wg.Add(1)
			go func(idx int) {
				defer wg.Done()
				sig := &signals[idx]
				sentiment, reason := s.analysis.Analyze(sig.Code, sig.Name)
				sig.Sentiment = sentiment
				sig.SentimentReason = reason

				targetPrice, targetReason := s.analysis.PredictTargetPriceFromSignal(
					sig.Code, sig.Name,
					sig.CurrentPrice, sig.High, sig.Low, sig.Open,
					sig.ChangeRate, sig.CntrStr, sig.PrevCntrStr, sig.RisingCount,
					sentiment, reason,
				)
				sig.TargetPrice = targetPrice
				sig.TargetReason = targetReason

				trend, conf, trendReason := s.analysis.PredictNextDayTrend(
					sig.Code, sig.Name,
					sig.CurrentPrice, sig.High, sig.Low, sig.Open,
					sig.ChangeRate, sig.CntrStr, sig.RisingCount,
					sentiment, reason,
				)
				sig.NextDayTrend = trend
				sig.NextDayConf = conf
				sig.NextDayReason = trendReason
			}(i)
		}
		go func() {
			wg.Wait()
			close(done)
		}()
		select {
		case <-done:
		case <-time.After(5 * time.Second):
			log.Printf("관심종목 분석: 감성 분석 5초 타임아웃")
		}
	}

	// Phase 7: 결과 캐시 저장
	s.mu.Lock()
	for _, sig := range signals {
		s.watchlistSignalCache[sig.Code] = sig
	}
	s.mu.Unlock()

	return signals
}
