package services

import (
	"crypto/rand"
	"fmt"
	"log"
	"strconv"
	"strings"
	"sync"
	"sync/atomic"
	"time"

	"stocksearch/models"
)

// newUUID UUID v4 생성
func newUUID() string {
	b := make([]byte, 16)
	_, _ = rand.Read(b)
	b[6] = (b[6] & 0x0f) | 0x40
	b[8] = (b[8] & 0x3f) | 0x80
	return fmt.Sprintf("%08x-%04x-%04x-%04x-%012x", b[0:4], b[4:6], b[6:8], b[8:10], b[10:])
}

const (
	maxLogEntries   = 300 // 최대 로그 보관 건수 (debug 로그 빈도 대비)
	cooldownMinutes = 5   // 동일 종목 재진입 쿨다운(분)
	exitLoopSec     = 5   // 청산 루프 주기(초)
	entryLoopSec    = 10  // 진입 루프 주기(초)
	pendingCheckSec = 5   // pending 확인 주기(초) — 시장가 주문 즉시 체결 대응
)

// AutoTradeService 자동매매 엔진 서비스
type AutoTradeService struct {
	scanner    *ScannerService
	orderSvc   *OrderService
	accountSvc *AccountService
	stockSvc   *StockService
	themeSvc   *ThemeService

	mu             sync.RWMutex
	running        int32 // atomic: 1=실행, 0=중지
	rules          []models.AutoTradeRule
	positions      map[string]*models.AutoTradePosition // code → 포지션
	logs           []models.AutoTradeLog                // 최근 maxLogEntries건
	cooldown       map[string]time.Time                 // code → 마지막 진입 시각
	watchSource    models.AutoTradeWatchSource          // 감시 소스 설정
	logBroadcaster func(models.AutoTradeLog)            // WS 브로드캐스트 콜백
}

var autoTradeService *AutoTradeService

// GetAutoTradeService 자동매매 서비스 싱글턴 반환
func GetAutoTradeService() *AutoTradeService {
	if autoTradeService == nil {
		autoTradeService = &AutoTradeService{
			scanner:    GetScannerService(),
			orderSvc:   GetOrderService(),
			accountSvc: GetAccountService(),
			stockSvc:   GetStockService(),
			themeSvc:   GetThemeService(),
			positions:  make(map[string]*models.AutoTradePosition),
			cooldown:   make(map[string]time.Time),
			watchSource: models.AutoTradeWatchSource{
				UseScanner:     true,
				SelectedThemes: []models.ThemeRef{},
			},
		}
		// DB에서 데이터 복원
		if rules := dbLoadRules(); len(rules) > 0 {
			autoTradeService.rules = rules
			log.Printf("DB에서 규칙 %d개 로드", len(rules))
		}
		if positions := dbLoadActivePositions(); len(positions) > 0 {
			autoTradeService.positions = positions
			log.Printf("DB에서 활성 포지션 %d개 로드", len(positions))
		}
		if ws := dbLoadWatchSource(); ws != nil {
			autoTradeService.watchSource = *ws
			log.Printf("DB에서 감시소스 로드")
		}
		if logs := dbLoadRecentLogs(maxLogEntries); len(logs) > 0 {
			autoTradeService.logs = logs
			log.Printf("DB에서 로그 %d건 로드", len(logs))
		}
	}
	return autoTradeService
}

// SetLogBroadcaster WS 브로드캐스트 콜백 등록 (main.go에서 Hub 주입 시 호출)
func (s *AutoTradeService) SetLogBroadcaster(fn func(models.AutoTradeLog)) {
	s.mu.Lock()
	s.logBroadcaster = fn
	s.mu.Unlock()
}

// GetWatchSource 현재 감시 소스 설정 반환
func (s *AutoTradeService) GetWatchSource() models.AutoTradeWatchSource {
	s.mu.RLock()
	defer s.mu.RUnlock()
	ws := s.watchSource
	themes := make([]models.ThemeRef, len(ws.SelectedThemes))
	copy(themes, ws.SelectedThemes)
	ws.SelectedThemes = themes
	return ws
}

// SetWatchSource 감시 소스 설정 업데이트
func (s *AutoTradeService) SetWatchSource(ws models.AutoTradeWatchSource) {
	s.mu.Lock()
	s.watchSource = ws
	s.mu.Unlock()
	dbUpsertWatchSource(ws)

	sources := "없음"
	if ws.UseScanner && len(ws.SelectedThemes) > 0 {
		names := make([]string, len(ws.SelectedThemes))
		for i, t := range ws.SelectedThemes {
			names[i] = t.Name
		}
		sources = fmt.Sprintf("자동감지+테마(%s)", joinStrings(names, ","))
	} else if ws.UseScanner {
		sources = "체결강도 자동감지"
	} else if len(ws.SelectedThemes) > 0 {
		names := make([]string, len(ws.SelectedThemes))
		for i, t := range ws.SelectedThemes {
			names[i] = t.Name
		}
		sources = fmt.Sprintf("테마(%s)", joinStrings(names, ","))
	}
	s.addLog("info", "", fmt.Sprintf("감시 소스 변경: %s", sources))
}

// joinStrings 문자열 슬라이스를 구분자로 결합
func joinStrings(ss []string, sep string) string {
	result := ""
	for i, s := range ss {
		if i > 0 {
			result += sep
		}
		result += s
	}
	return result
}

// getWatchSignals 설정된 감시 소스에서 신호 목록 수집
func (s *AutoTradeService) getWatchSignals() []SignalStock {
	s.mu.RLock()
	ws := s.watchSource
	themes := make([]models.ThemeRef, len(ws.SelectedThemes))
	copy(themes, ws.SelectedThemes)
	s.mu.RUnlock()

	var result []SignalStock
	seen := make(map[string]bool)

	// 체결강도 자동감지 신호 수집
	if ws.UseScanner {
		scannerSigs := s.scanner.GetSignals()
		s.addLog("debug", "", fmt.Sprintf("스캐너 신호 수신: %d개", len(scannerSigs)))
		for _, sig := range scannerSigs {
			s.addLog("debug", sig.Code, fmt.Sprintf("스캐너 [%s] 현재가=%s원 체결강도=%.1f RiseScore=%d 유형=%s 등락=%.2f%%",
				sig.Name, formatComma(sig.CurrentPrice), sig.CntrStr, sig.RiseScore, sig.SignalType, sig.ChangeRate))
			if !seen[sig.Code] {
				result = append(result, sig)
				seen[sig.Code] = true
			}
		}
	}

	// 선택된 테마 종목 분석 (테마당 최대 15종목으로 제한해 API 호출량 억제)
	const maxThemeStocks = 15
	for _, theme := range themes {
		detail, err := s.themeSvc.GetThemeStocks(theme.Code, "D")
		if err != nil {
			s.addLog("warn", "", fmt.Sprintf("테마 종목 조회 실패 [%s]: %v", theme.Name, err))
			continue
		}
		codes := make([]string, 0, maxThemeStocks)
		for _, st := range detail.Stocks {
			if !seen[st.Code] {
				codes = append(codes, st.Code)
				if len(codes) >= maxThemeStocks {
					break
				}
			}
		}
		if len(codes) == 0 {
			s.addLog("debug", "", fmt.Sprintf("테마[%s] 분석 대상 종목 없음 (중복 제외 후)", theme.Name))
			continue
		}
		s.addLog("debug", "", fmt.Sprintf("테마[%s] %d종목 분석 시작", theme.Name, len(codes)))
		sigs := s.scanner.AnalyzeWatchlist(codes)
		s.addLog("debug", "", fmt.Sprintf("테마[%s] 분석 완료: %d개 신호", theme.Name, len(sigs)))
		for _, sig := range sigs {
			s.addLog("debug", sig.Code, fmt.Sprintf("테마검증 [%s] 현재가=%s원 체결강도=%.1f RiseScore=%d 유형=%s 등락=%.2f%%",
				sig.Name, formatComma(sig.CurrentPrice), sig.CntrStr, sig.RiseScore, sig.SignalType, sig.ChangeRate))
			if !seen[sig.Code] {
				result = append(result, sig)
				seen[sig.Code] = true
			}
		}
	}

	return result
}

// Start 자동매매 엔진 시작
func (s *AutoTradeService) Start() {
	if !atomic.CompareAndSwapInt32(&s.running, 0, 1) {
		return // 이미 실행 중
	}
	s.addLog("info", "", "자동매매 엔진 시작")
	go s.entryLoop()
	go s.exitLoop()
}

// Stop 자동매매 엔진 중지
func (s *AutoTradeService) Stop() {
	if atomic.CompareAndSwapInt32(&s.running, 1, 0) {
		s.addLog("info", "", "자동매매 엔진 중지")
	}
}

// IsRunning 엔진 실행 여부 확인
func (s *AutoTradeService) IsRunning() bool {
	return atomic.LoadInt32(&s.running) == 1
}

// EmergencyStop 긴급 청산: 엔진 중지 후 모든 포지션 시장가 매도
func (s *AutoTradeService) EmergencyStop() {
	s.Stop()

	s.mu.Lock()
	codes := make([]string, 0, len(s.positions))
	for code, p := range s.positions {
		if p.Status == "open" || p.Status == "pending" {
			codes = append(codes, code)
		}
	}
	s.mu.Unlock()

	for _, code := range codes {
		s.mu.RLock()
		pos, ok := s.positions[code]
		s.mu.RUnlock()
		if !ok {
			continue
		}
		if err := s.executeSell(pos, "긴급"); err != nil {
			s.addLog("error", code, fmt.Sprintf("긴급청산 실패: %v", err))
		}
	}
	s.addLog("warn", "", "긴급 전량청산 완료")
}

// --- CRUD ---

// AddRule 규칙 추가
func (s *AutoTradeService) AddRule(rule models.AutoTradeRule) models.AutoTradeRule {
	rule.ID = newUUID()
	rule.CreatedAt = time.Now()
	s.mu.Lock()
	s.rules = append(s.rules, rule)
	s.mu.Unlock()
	dbInsertRule(rule)
	s.addLog("info", "", fmt.Sprintf("규칙 추가: %s", rule.Name))
	return rule
}

// UpdateRule 규칙 수정
func (s *AutoTradeService) UpdateRule(id string, updated models.AutoTradeRule) bool {
	s.mu.Lock()
	defer s.mu.Unlock()
	for i, r := range s.rules {
		if r.ID == id {
			updated.ID = id
			updated.CreatedAt = r.CreatedAt
			s.rules[i] = updated
			dbUpdateRule(updated)
			return true
		}
	}
	return false
}

// DeleteRule 규칙 삭제
func (s *AutoTradeService) DeleteRule(id string) bool {
	s.mu.Lock()
	defer s.mu.Unlock()
	for i, r := range s.rules {
		if r.ID == id {
			s.rules = append(s.rules[:i], s.rules[i+1:]...)
			dbDeleteRule(id)
			return true
		}
	}
	return false
}

// ToggleRule 규칙 활성화/비활성화 토글
func (s *AutoTradeService) ToggleRule(id string) (bool, bool) {
	s.mu.Lock()
	defer s.mu.Unlock()
	for i, r := range s.rules {
		if r.ID == id {
			s.rules[i].Enabled = !r.Enabled
			dbUpdateRule(s.rules[i])
			return true, s.rules[i].Enabled
		}
	}
	return false, false
}

// GetRules 규칙 목록 반환
func (s *AutoTradeService) GetRules() []models.AutoTradeRule {
	s.mu.RLock()
	defer s.mu.RUnlock()
	result := make([]models.AutoTradeRule, len(s.rules))
	copy(result, s.rules)
	return result
}

// GetPositions 포지션 목록 반환
func (s *AutoTradeService) GetPositions() []*models.AutoTradePosition {
	s.mu.RLock()
	defer s.mu.RUnlock()
	result := make([]*models.AutoTradePosition, 0, len(s.positions))
	for _, p := range s.positions {
		cp := *p
		result = append(result, &cp)
	}
	return result
}

// GetTrades 종료된 거래 내역 반환 (DB에서 조회, 없으면 메모리에서 필터)
func (s *AutoTradeService) GetTrades(limit int) []*models.AutoTradePosition {
	if trades := dbLoadClosedPositions(limit); trades != nil {
		return trades
	}
	// DB 없으면 메모리에서 closed 포지션 반환
	s.mu.RLock()
	defer s.mu.RUnlock()
	var result []*models.AutoTradePosition
	for _, p := range s.positions {
		if p.Status == "closed" {
			cp := *p
			result = append(result, &cp)
		}
	}
	return result
}

// GetLogs 최근 로그 반환
func (s *AutoTradeService) GetLogs() []models.AutoTradeLog {
	s.mu.RLock()
	defer s.mu.RUnlock()
	result := make([]models.AutoTradeLog, len(s.logs))
	copy(result, s.logs)
	return result
}

// GetStats 오늘 통계 반환 (매매 횟수, 손익)
func (s *AutoTradeService) GetStats() (tradeCount int, totalPL int64) {
	// DB가 있으면 DB에서 조회 (종료된 포지션 포함)
	if db != nil {
		return dbGetTodayStats()
	}
	// DB 없으면 메모리에서 계산
	today := time.Now().Truncate(24 * time.Hour)
	s.mu.RLock()
	defer s.mu.RUnlock()
	for _, p := range s.positions {
		if p.Status == "closed" && !p.ExitTime.Before(today) {
			tradeCount++
			totalPL += (p.ExitPrice - p.BuyPrice) * p.Qty
		}
	}
	return
}

// --- 내부 고루틴 ---

// entryLoop 10초 주기 진입 루프
func (s *AutoTradeService) entryLoop() {
	ticker := time.NewTicker(entryLoopSec * time.Second)
	defer ticker.Stop()
	for {
		if atomic.LoadInt32(&s.running) == 0 {
			return
		}
		<-ticker.C
		if atomic.LoadInt32(&s.running) == 0 {
			return
		}
		s.checkEntries()
	}
}

// exitLoop 5초 주기 청산 루프
func (s *AutoTradeService) exitLoop() {
	ticker := time.NewTicker(exitLoopSec * time.Second)
	defer ticker.Stop()
	pendingTicker := time.NewTicker(pendingCheckSec * time.Second)
	defer pendingTicker.Stop()

	for {
		if atomic.LoadInt32(&s.running) == 0 {
			return
		}
		select {
		case <-ticker.C:
			if atomic.LoadInt32(&s.running) == 0 {
				return
			}
			s.checkExits()
		case <-pendingTicker.C:
			if atomic.LoadInt32(&s.running) == 0 {
				return
			}
			s.checkPending()
		}
	}
}

// checkEntries 진입 조건 체크 및 매수 주문
func (s *AutoTradeService) checkEntries() {
	// 규칙 및 현재 포지션 수 선조회
	s.mu.RLock()
	rules := make([]models.AutoTradeRule, len(s.rules))
	copy(rules, s.rules)
	posCount := s.countActivePositions()
	s.mu.RUnlock()

	// 활성 규칙 수 계산
	activeRules := 0
	for _, r := range rules {
		if r.Enabled {
			activeRules++
		}
	}

	// 모든 활성 규칙이 최대 포지션에 도달했으면 신호 조회 생략 → 보유 종목 감시만 진행
	hasRoom := false
	for _, r := range rules {
		if r.Enabled && posCount < r.MaxPositions {
			hasRoom = true
			break
		}
	}
	if !hasRoom && activeRules > 0 {
		s.addLog("debug", "", fmt.Sprintf("진입 스캔 생략: 최대 포지션 도달 (%d개 보유 중, 청산 감시만 진행)", posCount))
		return
	}

	signals := s.getWatchSignals()

	s.addLog("debug", "", fmt.Sprintf("진입 스캔: 신호 %d개, 활성규칙 %d개", len(signals), activeRules))

	if len(signals) == 0 {
		return
	}

	for _, rule := range rules {
		if !rule.Enabled {
			continue
		}

		for _, sig := range signals {
			code := sig.Code
			if code == "" {
				continue
			}

			s.mu.RLock()
			_, alreadyHeld := s.positions[code]
			lastEntry, hasCooldown := s.cooldown[code]
			posCount := s.countActivePositions()
			s.mu.RUnlock()

			// 신호 검토 로그
			sentimentStr := sig.Sentiment
			if sentimentStr == "" {
				sentimentStr = "중립"
			}
			s.addLog("debug", code, fmt.Sprintf("검토 [%s] RiseScore=%d 체결강도=%.1f 감정=%s 유형=%s",
				sig.Name, sig.RiseScore, sig.CntrStr, sentimentStr, sig.SignalType))

			// 이미 보유 중인 종목 스킵
			if alreadyHeld {
				s.addLog("debug", code, "스킵: 이미 보유 중")
				continue
			}

			// 쿨다운 체크 (5분)
			if hasCooldown && time.Since(lastEntry) < cooldownMinutes*time.Minute {
				remaining := cooldownMinutes*time.Minute - time.Since(lastEntry)
				s.addLog("debug", code, fmt.Sprintf("스킵: 쿨다운 %.1f분 남음", remaining.Minutes()))
				continue
			}

			// 최대 보유 종목 수 초과 스킵
			if posCount >= rule.MaxPositions {
				s.addLog("debug", code, fmt.Sprintf("스킵: 최대 포지션 초과 (%d/%d)", posCount, rule.MaxPositions))
				continue
			}

			// 진입 조건 체크
			if sig.RiseScore < rule.MinRiseScore {
				s.addLog("debug", code, fmt.Sprintf("스킵: RiseScore 미달 (%d < %d)", sig.RiseScore, rule.MinRiseScore))
				continue
			}
			if sig.CntrStr < rule.MinCntrStr {
				s.addLog("debug", code, fmt.Sprintf("스킵: 체결강도 미달 (%.1f < %.1f)", sig.CntrStr, rule.MinCntrStr))
				continue
			}
			if rule.RequireBullish && sig.Sentiment != "호재" {
				s.addLog("debug", code, fmt.Sprintf("스킵: AI 호재 없음 (%s)", sentimentStr))
				continue
			}

			// 주문가능금액 확인
			curPriceStr := strconv.FormatInt(sig.CurrentPrice, 10)
			orderable, err := s.accountSvc.GetOrderable(code, curPriceStr, "2")
			if err != nil {
				s.addLog("warn", code, fmt.Sprintf("주문가능금액 조회 실패: %v", err))
				continue
			}

			avail, _ := strconv.ParseInt(orderable.OrdAlowa, 10, 64)
			if avail < rule.OrderAmount {
				s.addLog("warn", code, fmt.Sprintf("주문가능금액 부족: 필요 %d원, 가용 %d원", rule.OrderAmount, avail))
				continue
			}

			// 주문 수량 계산
			if sig.CurrentPrice <= 0 {
				continue
			}
			qty := rule.OrderAmount / sig.CurrentPrice
			if qty <= 0 {
				s.addLog("warn", code, fmt.Sprintf("주문수량 계산 오류: 주문금액 %d원, 현재가 %d원", rule.OrderAmount, sig.CurrentPrice))
				continue
			}

			// 매수 주문 실행 (시장가)
			result, err := s.orderSvc.Buy(OrderRequest{
				Exchange: "KRX",
				Code:     code,
				Qty:      strconv.FormatInt(qty, 10),
				Price:    "",
				TradeTP:  "3", // 시장가
			})
			if err != nil {
				s.addLog("error", code, fmt.Sprintf("매수주문 실패: %v", err))
				continue
			}

			// 포지션 등록 (현재가 기준 예상 손절/익절가 미리 계산 → UI에 0원 방지)
			estStop1, estStop, estProfit := calcStopTargets(sig.CurrentPrice, &rule)
			pos := &models.AutoTradePosition{
				Code:       code,
				Name:       sig.Name,
				Qty:        qty,
				BuyPrice:   sig.CurrentPrice,
				StopLoss1:  estStop1,
				StopLoss:   estStop,
				TakeProfit: estProfit,
				OrderNo:    result.OrderNo,
				EntryTime:  time.Now(),
				RuleID:     rule.ID,
				Status:     "pending",
			}

			s.mu.Lock()
			s.positions[code] = pos
			s.cooldown[code] = time.Now()
			s.mu.Unlock()

			dbInsertPosition(pos)
			s.addLog("info", code, fmt.Sprintf("매수 주문 접수: %s %d주 (주문번호: %s, RiseScore: %d)", sig.Name, qty, result.OrderNo, sig.RiseScore))
		}
	}
}

// checkExits 청산 조건 체크
func (s *AutoTradeService) checkExits() {
	s.mu.RLock()
	codes := make([]string, 0, len(s.positions))
	for code, p := range s.positions {
		if p.Status == "open" {
			codes = append(codes, code)
		}
	}
	s.mu.RUnlock()

	now := time.Now()
	// 장 마감 전 청산 기준: KST 15:20
	loc, _ := time.LoadLocation("Asia/Seoul")
	kstNow := now.In(loc)
	closeTime := time.Date(kstNow.Year(), kstNow.Month(), kstNow.Day(), 15, 20, 0, 0, loc)

	for _, code := range codes {
		s.mu.RLock()
		pos, ok := s.positions[code]
		if !ok || pos.Status != "open" {
			s.mu.RUnlock()
			continue
		}
		// 포지션 복사
		posCopy := *pos
		s.mu.RUnlock()

		// 현재가 조회
		price, err := s.stockSvc.GetCurrentPrice(code)
		if err != nil {
			log.Printf("[자동매매] 현재가 조회 실패 [%s]: %v", code, err)
			continue
		}
		curPrice := price.CurrentPrice

		// 포지션 모니터링 debug 로그
		if posCopy.BuyPrice > 0 {
			pl := (float64(curPrice) - float64(posCopy.BuyPrice)) / float64(posCopy.BuyPrice) * 100
			s.addLog("debug", code, fmt.Sprintf("모니터링 [%s] 현재가=%s원 손익=%+.2f%% (1차손절=%s[%d/%d회] 2차손절=%s 익절=%s)",
				posCopy.Name, formatComma(curPrice), pl,
				formatComma(posCopy.StopLoss1), posCopy.StopLoss1Touches, s.stopLoss1Limit(posCopy.RuleID),
				formatComma(posCopy.StopLoss), formatComma(posCopy.TakeProfit)))
		}

		reason := s.evalExitReason(code, &posCopy, curPrice, kstNow, closeTime, now)

		if reason != "" {
			if err := s.executeSell(&posCopy, reason); err != nil {
				s.addLog("error", code, fmt.Sprintf("%s 매도 실패: %v", reason, err))
			}
		}
	}
}

// checkPending pending 포지션 체결 확인
// 미체결 주문 조회(ka10075)에서 OrderNo가 사라지면 체결된 것으로 간주
func (s *AutoTradeService) checkPending() {
	s.mu.RLock()
	pending := make([]*models.AutoTradePosition, 0)
	for _, p := range s.positions {
		if p.Status == "pending" {
			cp := *p
			pending = append(pending, &cp)
		}
	}
	s.mu.RUnlock()

	if len(pending) == 0 {
		return
	}

	// 미체결 주문 목록 조회 → 주문번호 집합 구성
	unfilled, err := s.accountSvc.GetPendingOrders()
	if err != nil {
		log.Printf("[자동매매] 미체결 조회 실패: %v", err)
		return
	}
	unfilledMap := make(map[string]struct{}, len(unfilled))
	for _, o := range unfilled {
		unfilledMap[strings.TrimSpace(o.OrdNo)] = struct{}{}
	}

	for _, pos := range pending {
		orderNo := strings.TrimSpace(pos.OrderNo)
		if _, stillPending := unfilledMap[orderNo]; stillPending {
			// 아직 미체결 목록에 있음 → 대기 유지
			s.addLog("debug", pos.Code, fmt.Sprintf("체결 대기 중 (주문번호: %s)", orderNo))
			continue
		}

		// 미체결 목록에서 사라짐 → 체결(또는 취소) 완료
		// 잔고에서 실제 매입단가 조회 시도
		buyPrice := s.getBuyPriceFromBalance(pos.Code)
		if buyPrice == 0 {
			buyPrice = pos.BuyPrice // checkEntries에서 설정한 현재가 예상값
		}

		rule := s.findRule(pos.RuleID)
		var stopLoss1, stopLoss, takeProfit int64
		if rule != nil && buyPrice > 0 {
			stopLoss1, stopLoss, takeProfit = calcStopTargets(buyPrice, rule)
		}

		s.mu.Lock()
		if p, ok := s.positions[pos.Code]; ok && p.Status == "pending" {
			p.BuyPrice = buyPrice
			p.StopLoss1 = stopLoss1
			p.StopLoss = stopLoss
			p.TakeProfit = takeProfit
			p.Status = "open"
			dbUpdatePosition(p)
		}
		s.mu.Unlock()

		sl1Count := 0
		if rule != nil {
			sl1Count = rule.StopLoss1Count
		}
		s.addLog("info", pos.Code, fmt.Sprintf("매수 체결 확인: %s %d주 @ %d원 (1차손절: %d[%d회], 2차손절: %d, 익절: %d)",
			pos.Name, pos.Qty, buyPrice, stopLoss1, sl1Count, stopLoss, takeProfit))
	}
}

// getBuyPriceFromBalance 잔고 조회로 종목 매입단가 반환 (없으면 0)
func (s *AutoTradeService) getBuyPriceFromBalance(code string) int64 {
	balance, err := s.accountSvc.GetBalance()
	if err != nil {
		return 0
	}
	for _, stock := range balance.Stocks {
		stkCd := strings.TrimSpace(stock.StkCd)
		// 키움 API 코드 형식 A-prefix 처리 (예: "A005930" → "005930")
		if strings.HasPrefix(stkCd, "A") {
			stkCd = stkCd[1:]
		}
		if stkCd == code {
			price, _ := strconv.ParseInt(strings.TrimSpace(stock.PurPric), 10, 64)
			return price
		}
	}
	return 0
}

// executeSell 매도 주문 실행 및 포지션 업데이트
func (s *AutoTradeService) executeSell(pos *models.AutoTradePosition, reason string) error {
	if pos.Qty <= 0 {
		return fmt.Errorf("매도 수량 없음")
	}

	result, err := s.orderSvc.Sell(OrderRequest{
		Exchange: "KRX",
		Code:     pos.Code,
		Qty:      strconv.FormatInt(pos.Qty, 10),
		Price:    "",
		TradeTP:  "3", // 시장가
	})
	if err != nil {
		return err
	}

	// 현재가 조회 (청산가 근사치)
	exitPrice := pos.BuyPrice
	if p, err := s.stockSvc.GetCurrentPrice(pos.Code); err == nil {
		exitPrice = p.CurrentPrice
	}

	s.mu.Lock()
	if p, ok := s.positions[pos.Code]; ok {
		p.Status = "closed"
		p.ExitTime = time.Now()
		p.ExitPrice = exitPrice
		p.ExitReason = reason
		dbUpdatePosition(p)
	}
	s.mu.Unlock()

	pl := (exitPrice - pos.BuyPrice) * pos.Qty
	s.addLog("info", pos.Code, fmt.Sprintf("%s 매도 완료: %s @ %d원 (손익: %+d원, 주문번호: %s)", reason, pos.Name, exitPrice, pl, result.OrderNo))
	return nil
}

// ClosePosition 개별 포지션 수동 청산
func (s *AutoTradeService) ClosePosition(code string) error {
	s.mu.RLock()
	pos, ok := s.positions[code]
	s.mu.RUnlock()
	if !ok {
		return fmt.Errorf("포지션을 찾을 수 없습니다: %s", code)
	}
	if pos.Status != "open" && pos.Status != "pending" {
		return fmt.Errorf("이미 청산된 포지션입니다: %s", code)
	}
	return s.executeSell(pos, "수동청산")
}

// countActivePositions pending+open 포지션 수
func (s *AutoTradeService) countActivePositions() int {
	count := 0
	for _, p := range s.positions {
		if p.Status == "pending" || p.Status == "open" {
			count++
		}
	}
	return count
}

// findRule ID로 규칙 조회
func (s *AutoTradeService) findRule(id string) *models.AutoTradeRule {
	for _, r := range s.rules {
		if r.ID == id {
			cp := r
			return &cp
		}
	}
	return nil
}

// addLog 로그 추가 (최대 maxLogEntries 유지) + WS 브로드캐스트
func (s *AutoTradeService) addLog(level, code, message string) {
	entry := models.AutoTradeLog{
		At:      time.Now(),
		Level:   level,
		Code:    code,
		Message: message,
	}
	log.Printf("[자동매매][%s] %s %s", level, code, message)

	s.mu.Lock()
	s.logs = append([]models.AutoTradeLog{entry}, s.logs...)
	if len(s.logs) > maxLogEntries {
		s.logs = s.logs[:maxLogEntries]
	}
	broadcaster := s.logBroadcaster
	s.mu.Unlock()

	// debug 로그는 DB에 저장하지 않음 (빈도 높음)
	if level != "debug" {
		dbInsertLog(entry)
	}

	// WS 브로드캐스트 (락 밖에서 호출해 데드락 방지)
	if broadcaster != nil {
		broadcaster(entry)
	}
}

// formatComma 천 단위 콤마 포맷 (서비스 내부용)
func formatComma(n int64) string {
	if n == 0 {
		return "0"
	}
	negative := n < 0
	if negative {
		n = -n
	}
	s := strconv.FormatInt(n, 10)
	result := make([]byte, 0, len(s)+len(s)/3)
	for i, c := range s {
		if i > 0 && (len(s)-i)%3 == 0 {
			result = append(result, ',')
		}
		result = append(result, byte(c))
	}
	if negative {
		return "-" + string(result)
	}
	return string(result)
}

// --- 포지션 헬퍼 함수 (규칙 정보 조회용) ---

// exitBeforeCloseRule 포지션이 속한 규칙의 ExitBeforeClose 반환
func exitBeforeCloseRule(rules []models.AutoTradeRule, p *models.AutoTradePosition) bool {
	for _, r := range rules {
		if r.ID == p.RuleID {
			return r.ExitBeforeClose
		}
	}
	return false
}

// maxHoldExpired 최대 보유 시간 초과 여부 반환
func maxHoldExpired(rules []models.AutoTradeRule, p *models.AutoTradePosition, now time.Time) bool {
	for _, r := range rules {
		if r.ID == p.RuleID {
			if r.MaxHoldMinutes <= 0 {
				return false
			}
			return now.Sub(p.EntryTime) >= time.Duration(r.MaxHoldMinutes)*time.Minute
		}
	}
	return false
}

// calcStopTargets 매수가 기준으로 1차 손절가 / 2차 손절가 / 익절가 계산
func calcStopTargets(buyPrice int64, rule *models.AutoTradeRule) (stopLoss1, stopLoss, takeProfit int64) {
	if rule == nil || buyPrice <= 0 {
		return 0, 0, 0
	}
	if rule.StopLoss1Count > 0 && rule.StopLoss1Pct != 0 {
		stopLoss1 = int64(float64(buyPrice) * (1 + rule.StopLoss1Pct/100))
	}
	stopLoss = int64(float64(buyPrice) * (1 + rule.StopLossPct/100))
	takeProfit = int64(float64(buyPrice) * (1 + rule.TakeProfitPct/100))
	return
}

// stopLoss1Limit 규칙의 1차 손절 터치 임계 반환 (규칙 없으면 0)
func (s *AutoTradeService) stopLoss1Limit(ruleID string) int {
	s.mu.RLock()
	defer s.mu.RUnlock()
	for _, r := range s.rules {
		if r.ID == ruleID {
			return r.StopLoss1Count
		}
	}
	return 0
}

// evalExitReason 청산 조건 평가 — 2중 손절 포함
// 2차 손절(즉시) → 1차 손절(터치 카운트) → 익절 → 장마감 → 시간초과 순으로 체크
func (s *AutoTradeService) evalExitReason(code string, pos *models.AutoTradePosition, curPrice int64,
	kstNow time.Time, closeTime time.Time, now time.Time) string {

	// 2차 손절: 즉시 매도
	if curPrice <= pos.StopLoss {
		return "2차손절"
	}

	// 1차 손절: X회 터치 시 매도
	if pos.StopLoss1 > 0 && curPrice <= pos.StopLoss1 {
		rule := s.findRule(pos.RuleID)
		limit := 0
		if rule != nil {
			limit = rule.StopLoss1Count
		}

		s.mu.Lock()
		var touches int
		if p, ok := s.positions[code]; ok && p.Status == "open" {
			p.StopLoss1Touches++
			touches = p.StopLoss1Touches
			dbUpdatePosition(p)
		}
		s.mu.Unlock()

		if limit > 0 && touches >= limit {
			return "1차손절"
		}
		s.addLog("warn", code, fmt.Sprintf("1차손절 터치 %d/%d회 (현재가: %s원, 1차손절가: %s원)",
			touches, limit, formatComma(curPrice), formatComma(pos.StopLoss1)))
		return ""
	}

	// 익절
	if curPrice >= pos.TakeProfit {
		return "익절"
	}

	// 장마감 전 청산
	s.mu.RLock()
	rules := s.rules
	s.mu.RUnlock()
	if exitBeforeCloseRule(rules, pos) && kstNow.After(closeTime) {
		return "장마감"
	}
	if maxHoldExpired(rules, pos, now) {
		return "시간초과"
	}
	return ""
}
