package services

import (
	"encoding/json"
	"fmt"
	"log"
	"stocksearch/config"
	"stocksearch/models"
	"strconv"
	"strings"
	"sync"
	"time"

	"github.com/gorilla/websocket"
)

const (
	writeTimeout = 10 * time.Second // 쓰기 타임아웃
)

// kiwoomWSURL 키움 WS 서버 URL (모의투자 여부에 따라 분기)
func kiwoomWSURL() string {
	base := config.App.BaseURL
	if strings.Contains(base, "mockapi") {
		return "wss://mockapi.kiwoom.com:10000/api/dostk/websocket"
	}
	return "wss://api.kiwoom.com:10000/api/dostk/websocket"
}

// KiwoomWSClient 키움증권 실시간 WebSocket 클라이언트
type KiwoomWSClient struct {
	tokenService *TokenService
	conn         *websocket.Conn
	mu           sync.Mutex // conn 보호 + 직렬화된 쓰기 보장

	// 현재 구독 중인 종목 코드 집합 (재연결 시 복구용)
	subscribed map[string]bool

	// REG 배치 전송용: 짧은 시간 내 요청을 모아 단일 REG로 발송
	pendingReg []string
	regTimer   *time.Timer

	// 실시간 데이터 수신 콜백
	onPrice        func(price *models.StockPrice)
	onOrderBook    func(ob *models.OrderBook)
	onProgram      func(pg *models.ProgramTrading)
	onMarketStatus func(ms *models.MarketStatus)
	onMeta         func(meta *models.StockMeta)
}

var kiwoomWSOnce sync.Once
var kiwoomWSSvc *KiwoomWSClient

// GetKiwoomWSClient KiwoomWS 클라이언트 싱글턴 반환
func GetKiwoomWSClient(onPrice func(*models.StockPrice)) *KiwoomWSClient {
	kiwoomWSOnce.Do(func() {
		kiwoomWSSvc = &KiwoomWSClient{
			tokenService: GetTokenService(),
			subscribed:   make(map[string]bool),
			onPrice:      onPrice,
		}
	})
	return kiwoomWSSvc
}

// SetCallbacks 추가 실시간 데이터 콜백 등록
func (k *KiwoomWSClient) SetCallbacks(
	onOrderBook func(*models.OrderBook),
	onProgram func(*models.ProgramTrading),
	onMarketStatus func(*models.MarketStatus),
	onMeta func(*models.StockMeta),
) {
	k.mu.Lock()
	defer k.mu.Unlock()
	k.onOrderBook = onOrderBook
	k.onProgram = onProgram
	k.onMarketStatus = onMarketStatus
	k.onMeta = onMeta
}

// Connect 키움 WS 서버에 연결 후 읽기 루프 시작
func (k *KiwoomWSClient) Connect() error {
	conn, err := k.dial()
	if err != nil {
		return err
	}

	k.mu.Lock()
	k.conn = conn
	k.mu.Unlock()

	stopCh := make(chan struct{})
	go k.readLoop(conn, stopCh)

	log.Println("키움 WS 연결 완료")
	return nil
}

// dial WSS 연결 수립 후 로그인 패킷 전송
func (k *KiwoomWSClient) dial() (*websocket.Conn, error) {
	// HTTP 헤더 없이 연결 (키움 WS는 헤더 인증 불필요)
	conn, _, err := websocket.DefaultDialer.Dial(kiwoomWSURL(), nil)
	if err != nil {
		return nil, err
	}

	// 연결 직후 로그인 패킷 전송 (Bearer 없이 token만)
	loginMsg := map[string]string{
		"trnm":  "LOGIN",
		"token": k.tokenService.GetToken(),
	}
	data, _ := json.Marshal(loginMsg)
	conn.SetWriteDeadline(time.Now().Add(writeTimeout))
	if err := conn.WriteMessage(websocket.TextMessage, data); err != nil {
		conn.Close()
		return nil, err
	}

	// 로그인 응답 대기
	conn.SetReadDeadline(time.Now().Add(10 * time.Second))
	_, resp, err := conn.ReadMessage()
	if err != nil {
		conn.Close()
		return nil, err
	}

	var loginResp struct {
		Trnm       string `json:"trnm"`
		ReturnCode int    `json:"return_code"`
		ReturnMsg  string `json:"return_msg"`
	}
	if err := json.Unmarshal(resp, &loginResp); err != nil {
		conn.Close()
		return nil, fmt.Errorf("로그인 응답 파싱 실패: %w", err)
	}
	if loginResp.Trnm != "LOGIN" || loginResp.ReturnCode != 0 {
		conn.Close()
		return nil, fmt.Errorf("키움 WS 로그인 실패 [%d]: %s", loginResp.ReturnCode, loginResp.ReturnMsg)
	}

	// 읽기 데드라인 초기화 (readLoop에서 관리)
	conn.SetReadDeadline(time.Time{})

	// 장운영 상태(0s) 글로벌 구독 (item 빈 문자열)
	k.sendMarketStatusReg(conn)

	log.Println("키움 WS 로그인 성공")
	return conn, nil
}

// sendMarketStatusReg 장운영 상태(0s) 구독 전송 (item="", 전역)
func (k *KiwoomWSClient) sendMarketStatusReg(conn *websocket.Conn) {
	msg := map[string]interface{}{
		"trnm":    "REG",
		"grp_no":  "1",
		"refresh": "1",
		"data": []map[string]interface{}{
			{"item": []string{""}, "type": []string{"0s"}},
		},
	}
	data, _ := json.Marshal(msg)
	conn.SetWriteDeadline(time.Now().Add(writeTimeout))
	if err := conn.WriteMessage(websocket.TextMessage, data); err != nil {
		log.Printf("키움 WS 장운영상태 구독 실패: %v", err)
	}
}

// SubscribePair KRX + NXT 종목을 debounce 배치 REG로 구독
// 200ms 내 여러 호출이 들어오면 하나의 REG 메시지로 묶어 전송
func (k *KiwoomWSClient) SubscribePair(code string) {
	k.mu.Lock()
	defer k.mu.Unlock()

	nxt := code + "_NX"
	if !k.subscribed[code] {
		k.subscribed[code] = true
		k.pendingReg = append(k.pendingReg, code)
	}
	if !k.subscribed[nxt] {
		k.subscribed[nxt] = true
		k.pendingReg = append(k.pendingReg, nxt)
	}
	k.scheduleFlush()
}

// scheduleFlush REG 배치 전송 타이머 설정 (mu 보유 상태에서 호출)
// 연속 호출 시 타이머를 초기화해 마지막 호출로부터 200ms 후 한 번만 전송
func (k *KiwoomWSClient) scheduleFlush() {
	if k.regTimer != nil {
		k.regTimer.Stop()
	}
	k.regTimer = time.AfterFunc(200*time.Millisecond, k.flushPendingReg)
}

// flushPendingReg 누적된 구독 코드를 단일 REG 메시지로 전송
func (k *KiwoomWSClient) flushPendingReg() {
	k.mu.Lock()
	defer k.mu.Unlock()

	if len(k.pendingReg) == 0 {
		return
	}
	codes := k.pendingReg
	k.pendingReg = nil
	k.sendRegBatch(codes, "1")
	log.Printf("키움 WS 배치 구독 전송 (%d개): %v", len(codes), codes)
}

// UnsubscribePair KRX + NXT 종목을 단일 REMOVE 메시지로 동시 해제
func (k *KiwoomWSClient) UnsubscribePair(code string) {
	k.mu.Lock()
	defer k.mu.Unlock()

	nxt := code + "_NX"
	var toRemove []string
	if k.subscribed[code] {
		delete(k.subscribed, code)
		toRemove = append(toRemove, code)
	}
	if k.subscribed[nxt] {
		delete(k.subscribed, nxt)
		toRemove = append(toRemove, nxt)
	}
	if len(toRemove) > 0 {
		k.sendRemoveBatch(toRemove)
		log.Printf("키움 WS 구독 해제: %v", toRemove)
	}
}

// sendRegBatch 여러 종목을 단일 REG 메시지로 배치 전송 (mu 보유 상태에서 호출)
// 0B: KRX + NXT 전체, 0D/0H/0w/0g: KRX 코드만
func (k *KiwoomWSClient) sendRegBatch(codes []string, refresh string) {
	// KRX 전용 코드 추출 (_NX 등 접미사 없는 코드)
	krxCodes := filterKRXCodes(codes)

	dataItems := []map[string]interface{}{
		{"item": codes, "type": []string{"0B"}},
	}
	if len(krxCodes) > 0 {
		dataItems = append(dataItems, map[string]interface{}{
			"item": krxCodes,
			"type": []string{"0D", "0H", "0w", "0g"},
		})
	}

	msg := map[string]interface{}{
		"trnm":    "REG",
		"grp_no":  "1",
		"refresh": refresh,
		"data":    dataItems,
	}
	if err := k.write(msg); err != nil {
		log.Printf("키움 WS 구독 전송 실패: %v", err)
	}
}

// sendRemoveBatch 여러 종목을 단일 REMOVE 메시지로 배치 전송 (mu 보유 상태에서 호출)
func (k *KiwoomWSClient) sendRemoveBatch(codes []string) {
	krxCodes := filterKRXCodes(codes)

	dataItems := []map[string]interface{}{
		{"item": codes, "type": []string{"0B"}},
	}
	if len(krxCodes) > 0 {
		dataItems = append(dataItems, map[string]interface{}{
			"item": krxCodes,
			"type": []string{"0D", "0H", "0w", "0g"},
		})
	}

	msg := map[string]interface{}{
		"trnm":   "REMOVE",
		"grp_no": "1",
		"data":   dataItems,
	}
	if err := k.write(msg); err != nil {
		log.Printf("키움 WS 구독해제 전송 실패: %v", err)
	}
}

// filterKRXCodes 접미사 없는 KRX 코드만 반환
func filterKRXCodes(codes []string) []string {
	var krx []string
	for _, c := range codes {
		if !strings.Contains(c, "_") {
			krx = append(krx, c)
		}
	}
	return krx
}

// write JSON 메시지 전송 (mu 보유 상태에서 호출)
func (k *KiwoomWSClient) write(v interface{}) error {
	if k.conn == nil {
		return nil
	}
	data, _ := json.Marshal(v)
	k.conn.SetWriteDeadline(time.Now().Add(writeTimeout))
	return k.conn.WriteMessage(websocket.TextMessage, data)
}

// readLoop 키움 WS 메시지 수신 루프
func (k *KiwoomWSClient) readLoop(conn *websocket.Conn, stopCh chan struct{}) {
	defer func() {
		close(stopCh)

		k.mu.Lock()
		if k.conn == conn {
			k.conn = nil
		}
		k.mu.Unlock()

		conn.Close()
		log.Println("키움 WS 연결 끊김, 재연결 시도...")
		go k.reconnect()
	}()

	for {
		_, data, err := conn.ReadMessage()
		if err != nil {
			if !websocket.IsCloseError(err, websocket.CloseNormalClosure, websocket.CloseGoingAway) {
				log.Printf("키움 WS 읽기 오류: %v", err)
			}
			return
		}
		k.handleMessage(data)
	}
}

// handleMessage 수신 메시지 파싱 및 콜백 호출
func (k *KiwoomWSClient) handleMessage(data []byte) {
	var msg struct {
		Trnm string `json:"trnm"`
		Data []struct {
			Type   string            `json:"type"`
			Item   string            `json:"item"`
			Values map[string]string `json:"values"`
		} `json:"data"`
		ReturnCode int    `json:"return_code"`
		ReturnMsg  string `json:"return_msg"`
	}

	if err := json.Unmarshal(data, &msg); err != nil {
		return
	}

	switch msg.Trnm {
	case "PING":
		// 키움 서버 PING 수신 → PONG 응답 전송
		k.mu.Lock()
		_ = k.write(map[string]string{"trnm": "PONG"})
		k.mu.Unlock()
	case "REG", "REMOVE":
		if msg.ReturnCode != 0 {
			log.Printf("키움 WS %s 오류: %s", msg.Trnm, msg.ReturnMsg)
		}
	case "REAL":
		for _, d := range msg.Data {
			switch d.Type {
			case "0B":
				if k.onPrice != nil {
					k.onPrice(parseRealPrice(d.Item, d.Values))
				}
			case "0D":
				if k.onOrderBook != nil {
					k.onOrderBook(parseOrderBook(d.Item, d.Values))
				}
			case "0H":
				// 예상체결 데이터 → StockPrice 형식으로 통합
				if k.onPrice != nil {
					k.onPrice(parseExpectedPrice(d.Item, d.Values))
				}
			case "0w":
				if k.onProgram != nil {
					k.onProgram(parseProgramTrading(d.Item, d.Values))
				}
			case "0s":
				if k.onMarketStatus != nil {
					k.onMarketStatus(parseMarketStatus(d.Values))
				}
			case "0g":
				if k.onMeta != nil {
					k.onMeta(parseStockMeta(d.Item, d.Values))
				}
			}
		}
	}
}

// reconnect 재연결 및 기존 구독 복구 (지수 백오프)
func (k *KiwoomWSClient) reconnect() {
	k.mu.Lock()
	codes := make([]string, 0, len(k.subscribed))
	for code := range k.subscribed {
		codes = append(codes, code)
	}
	k.mu.Unlock()

	delay := 5 * time.Second
	for {
		time.Sleep(delay)
		log.Printf("키움 WS 재연결 시도... (%v 후 다음 시도)", delay*2)

		conn, err := k.dial()
		if err != nil {
			log.Printf("키움 WS 재연결 실패: %v", err)
			if delay < 60*time.Second {
				delay *= 2
			}
			continue
		}

		k.mu.Lock()
		k.conn = conn
		// 기존 구독 복구: 모든 코드를 단일 REG 메시지로 배치 전송
		if len(codes) > 0 {
			k.sendRegBatch(codes, "1")
		}
		k.mu.Unlock()

		stopCh := make(chan struct{})
		go k.readLoop(conn, stopCh)

		log.Printf("키움 WS 재연결 성공, %d개 종목 구독 복구", len(codes))
		return
	}
}

// parseRealPrice 0B 실시간 주식체결 값 → StockPrice 변환
// 20=체결시간, 10=현재가, 11=전일대비, 12=등락률, 13=누적거래량, 14=누적거래대금
// 15=거래량(체결량), 16=시가, 17=고가, 18=저가, 27=최우선매도호가, 28=최우선매수호가
// 228=체결강도, 290=장구분
// NXT 종목코드(005930_NX)는 _NX 접미사 제거 후 KRX 코드(005930)로 통일
func parseRealPrice(code string, v map[string]string) *models.StockPrice {
	normalized := strings.TrimPrefix(code, "A")
	normalized = strings.SplitN(normalized, "_", 2)[0] // _NX, _AL 등 접미사 제거
	return &models.StockPrice{
		Code:         normalized,
		CurrentPrice: absInt(parseWSInt(v["10"])),
		ChangePrice:  parseWSInt(v["11"]),
		ChangeRate:   parseWSFloat(v["12"]),
		Volume:       absInt(parseWSInt(v["13"])),
		TradeMoney:   absInt(parseWSInt(v["14"])),
		TradeVolume:  absInt(parseWSInt(v["15"])),
		Open:         absInt(parseWSInt(v["16"])),
		High:         absInt(parseWSInt(v["17"])),
		Low:          absInt(parseWSInt(v["18"])),
		TradeTime:    v["20"],
		AskPrice1:    absInt(parseWSInt(v["27"])),
		BidPrice1:    absInt(parseWSInt(v["28"])),
		CntrStr:      parseWSFloat(v["228"]),
		MarketStatus: v["290"],
		UpdatedAt:    time.Now(),
	}
}

// parseExpectedPrice 0H 주식예상체결 → StockPrice 변환 (장 전/후 예상체결 시 사용)
func parseExpectedPrice(code string, v map[string]string) *models.StockPrice {
	normalized := strings.TrimPrefix(code, "A")
	normalized = strings.SplitN(normalized, "_", 2)[0]
	return &models.StockPrice{
		Code:         normalized,
		CurrentPrice: absInt(parseWSInt(v["10"])),
		ChangePrice:  parseWSInt(v["11"]),
		ChangeRate:   parseWSFloat(v["12"]),
		TradeVolume:  absInt(parseWSInt(v["15"])),
		Volume:       absInt(parseWSInt(v["13"])),
		TradeTime:    v["20"],
		UpdatedAt:    time.Now(),
	}
}

// parseOrderBook 0D 주식호가잔량 → OrderBook 변환
// 41~50=매도호가1~10, 61~70=매도호가수량1~10
// 51~60=매수호가1~10, 71~80=매수호가수량1~10
// 121=매도총잔량, 125=매수총잔량, 23=예상체결가, 24=예상체결수량
func parseOrderBook(code string, v map[string]string) *models.OrderBook {
	normalized := strings.TrimPrefix(code, "A")
	normalized = strings.SplitN(normalized, "_", 2)[0]

	ob := &models.OrderBook{
		Code:    normalized,
		AskTime: v["21"],
	}

	for i := 1; i <= 10; i++ {
		askKey := strconv.Itoa(40 + i)    // 41..50
		askVolKey := strconv.Itoa(60 + i) // 61..70
		bidKey := strconv.Itoa(50 + i)    // 51..60
		bidVolKey := strconv.Itoa(70 + i) // 71..80

		ob.Asks = append(ob.Asks, models.OrderBookEntry{
			Price:  absInt(parseWSInt(v[askKey])),
			Volume: absInt(parseWSInt(v[askVolKey])),
		})
		ob.Bids = append(ob.Bids, models.OrderBookEntry{
			Price:  absInt(parseWSInt(v[bidKey])),
			Volume: absInt(parseWSInt(v[bidVolKey])),
		})
	}

	ob.TotalAskVol = absInt(parseWSInt(v["121"]))
	ob.TotalBidVol = absInt(parseWSInt(v["125"]))
	ob.ExpectedPrc = absInt(parseWSInt(v["23"]))
	ob.ExpectedVol = absInt(parseWSInt(v["24"]))

	return ob
}

// parseProgramTrading 0w 종목프로그램매매 → ProgramTrading 변환
func parseProgramTrading(code string, v map[string]string) *models.ProgramTrading {
	normalized := strings.TrimPrefix(code, "A")
	normalized = strings.SplitN(normalized, "_", 2)[0]
	return &models.ProgramTrading{
		Code:         normalized,
		SellVolume:   absInt(parseWSInt(v["202"])),
		SellAmount:   absInt(parseWSInt(v["204"])),
		BuyVolume:    absInt(parseWSInt(v["206"])),
		BuyAmount:    absInt(parseWSInt(v["208"])),
		NetBuyVolume: parseWSInt(v["210"]),
		NetBuyAmount: parseWSInt(v["212"]),
	}
}

// parseMarketStatus 0s 장시작시간 → MarketStatus 변환
func parseMarketStatus(v map[string]string) *models.MarketStatus {
	code := v["215"]
	return &models.MarketStatus{
		StatusCode: code,
		StatusName: marketStatusName(code),
		Time:       v["20"],
	}
}

// marketStatusName 장운영구분 코드 → 한글 이름
func marketStatusName(code string) string {
	switch code {
	case "0":
		return "장시작전"
	case "2":
		return "장마감알림"
	case "3":
		return "장 중"
	case "4":
		return "장마감"
	case "8":
		return "정규장마감"
	case "9":
		return "전체장마감"
	case "a":
		return "시간외종가시작"
	case "b":
		return "시간외종가종료"
	case "c":
		return "시간외단일가시작"
	case "d":
		return "시간외단일가종료"
	default:
		return "장외"
	}
}

// parseStockMeta 0g 주식종목정보 → StockMeta 변환
func parseStockMeta(code string, v map[string]string) *models.StockMeta {
	normalized := strings.TrimPrefix(code, "A")
	normalized = strings.SplitN(normalized, "_", 2)[0]
	return &models.StockMeta{
		Code:       normalized,
		UpperLimit: absInt(parseWSInt(v["305"])),
		LowerLimit: absInt(parseWSInt(v["306"])),
		BasePrice:  absInt(parseWSInt(v["307"])),
	}
}

func parseWSInt(s string) int64 {
	s = strings.TrimSpace(s)
	if s == "" {
		return 0
	}
	neg := strings.HasPrefix(s, "-")
	s = strings.TrimLeft(s, "+-")
	s = strings.ReplaceAll(s, ",", "")
	n, _ := strconv.ParseInt(s, 10, 64)
	if neg {
		return -n
	}
	return n
}

func parseWSFloat(s string) float64 {
	s = strings.TrimSpace(s)
	if s == "" {
		return 0
	}
	neg := strings.HasPrefix(s, "-")
	s = strings.TrimLeft(s, "+-")
	f, _ := strconv.ParseFloat(s, 64)
	if neg {
		return -f
	}
	return f
}

func absInt(n int64) int64 {
	if n < 0 {
		return -n
	}
	return n
}
