package services

import (
	"bytes"
	"encoding/json"
	"fmt"
	"io"
	"log"
	"net/http"
	"stocksearch/config"
	"stocksearch/models"
	"strconv"
	"strings"
	"sync"
	"time"
)

// cntrStrCacheEntry getCntrStr 캐시 항목
type cntrStrCacheEntry struct {
	value     float64
	expiresAt time.Time
}

// apiJob 큐에 넣을 API 요청 작업
type apiJob struct {
	req    *http.Request
	result chan apiResult
}

// apiResult 워커의 응답
type apiResult struct {
	resp *http.Response
	err  error
}

// KiwoomClient 키움증권 REST API HTTP 클라이언트
// 모든 API 호출은 단일 워커 큐를 통해 순차 처리 (429 방지)
// 1초 윈도우 내 처리 시간 합산 → 남은 시간 대기 후 다음 배치
type KiwoomClient struct {
	httpClient   *http.Client
	tokenService *TokenService
	queue        chan apiJob // API 요청 큐
	cntrStrCache sync.Map    // stockCode → cntrStrCacheEntry (5초 TTL)
}

const apiQueueSize = 256 // 큐 버퍼 크기

var kiwoomClient *KiwoomClient

// GetKiwoomClient 키움 클라이언트 싱글턴 반환
func GetKiwoomClient() *KiwoomClient {
	if kiwoomClient == nil {
		kiwoomClient = &KiwoomClient{
			httpClient:   &http.Client{Timeout: 10 * time.Second},
			tokenService: GetTokenService(),
			queue:        make(chan apiJob, apiQueueSize),
		}
		go kiwoomClient.worker()
	}
	return kiwoomClient
}

// worker 단일 고루틴이 큐에서 작업을 꺼내 순차 실행
// 1건 실행 후 (1초 - 처리시간)만큼 대기 → 다음 1건
func (k *KiwoomClient) worker() {
	for job := range k.queue {
		start := time.Now()
		resp, err := k.httpClient.Do(job.req)
		elapsed := time.Since(start)

		job.result <- apiResult{resp: resp, err: err}

		// 1초 - 처리시간 = 대기시간 (처리가 1초 이상이면 대기 없이 즉시 다음)
		if wait := time.Second - elapsed; wait > 0 {
			time.Sleep(wait)
		}
	}
}

// enqueue HTTP 요청을 큐에 넣고 응답 대기
func (k *KiwoomClient) enqueue(req *http.Request) (*http.Response, error) {
	ch := make(chan apiResult, 1)
	k.queue <- apiJob{req: req, result: ch}
	res := <-ch
	return res.resp, res.err
}

// post 공통 POST 요청 (api-id 헤더, JSON body, 큐 기반 순차 처리, 429 재시도)
func (k *KiwoomClient) post(apiID string, path string, body map[string]string) ([]byte, error) {
	const maxRetries = 3
	backoff := 1 * time.Second

	data, _ := json.Marshal(body)

	for attempt := 0; attempt < maxRetries; attempt++ {
		req, err := http.NewRequest("POST", config.App.BaseURL+path, bytes.NewReader(data))
		if err != nil {
			return nil, fmt.Errorf("요청 생성 실패: %w", err)
		}

		req.Header.Set("Content-Type", "application/json;charset=UTF-8")
		req.Header.Set("authorization", "Bearer "+k.tokenService.GetToken())
		req.Header.Set("api-id", apiID)
		req.Header.Set("cont-yn", "N")
		req.Header.Set("next-key", "")

		resp, err := k.enqueue(req)
		if err != nil {
			return nil, fmt.Errorf("API 요청 실패: %w", err)
		}

		respBody, err := io.ReadAll(resp.Body)
		resp.Body.Close()
		if err != nil {
			return nil, fmt.Errorf("응답 읽기 실패: %w", err)
		}

		// 429: 잠시 대기 후 재시도
		if resp.StatusCode == http.StatusTooManyRequests {
			if attempt < maxRetries-1 {
				log.Printf("[키움API] 429 Too Many Requests (api-id=%s), %v 후 재시도 (%d/%d)", apiID, backoff, attempt+1, maxRetries)
				time.Sleep(backoff)
				backoff *= 2
				continue
			}
			return nil, fmt.Errorf("API 요청 한도 초과 (api-id=%s): %s", apiID, string(respBody))
		}

		if resp.StatusCode != http.StatusOK {
			return nil, fmt.Errorf("API 응답 오류: HTTP %d, body: %s", resp.StatusCode, string(respBody))
		}

		// HTML 응답 감지 (서버 점검/리다이렉트 시 HTML 반환)
		if len(respBody) > 0 && respBody[0] == '<' {
			return nil, fmt.Errorf("API 서버 점검 중 (HTML 응답 수신)")
		}

		return respBody, nil
	}

	return nil, fmt.Errorf("API 요청 최대 재시도 초과 (api-id=%s)", apiID)
}

// postPaged 연속조회 지원 POST 요청 - 응답 헤더(cont-yn, next-key) 함께 반환
func (k *KiwoomClient) postPaged(apiID, path string, body map[string]string, contYn, nextKey string) ([]byte, string, string, error) {
	data, _ := json.Marshal(body)
	req, err := http.NewRequest("POST", config.App.BaseURL+path, bytes.NewReader(data))
	if err != nil {
		return nil, "", "", fmt.Errorf("요청 생성 실패: %w", err)
	}

	req.Header.Set("Content-Type", "application/json;charset=UTF-8")
	req.Header.Set("authorization", "Bearer "+k.tokenService.GetToken())
	req.Header.Set("api-id", apiID)
	req.Header.Set("cont-yn", contYn)
	req.Header.Set("next-key", nextKey)

	resp, err := k.enqueue(req)
	if err != nil {
		return nil, "", "", fmt.Errorf("API 요청 실패: %w", err)
	}

	respBody, err := io.ReadAll(resp.Body)
	resp.Body.Close()
	if err != nil {
		return nil, "", "", fmt.Errorf("응답 읽기 실패: %w", err)
	}
	if resp.StatusCode != http.StatusOK {
		return nil, "", "", fmt.Errorf("API 오류: HTTP %d", resp.StatusCode)
	}

	return respBody, resp.Header.Get("cont-yn"), resp.Header.Get("next-key"), nil
}

// GetCurrentPrice 주식 기본정보 + 체결강도 조회 (ka10001 + ka10003)
// NXT 거래소 데이터를 우선 조회하고, NXT에 없으면 KRX로 폴백
func (k *KiwoomClient) GetCurrentPrice(stockCode string) (*models.StockPrice, error) {
	// 이미 거래소 접미사(_NX, _AL)가 붙어있으면 그대로 조회
	if strings.Contains(stockCode, "_") {
		return k.fetchPrice(stockCode)
	}

	// NXT 우선 시도 → 실패하거나 종목명이 비어있으면 KRX 폴백
	if price, err := k.fetchPrice(stockCode + "_NX"); err == nil && price.Name != "" {
		log.Printf("NXT 가격 사용: %s → %d원", stockCode, price.CurrentPrice)
		return price, nil
	}
	return k.fetchPrice(stockCode)
}

// fetchPrice ka10001로 특정 거래소 종목코드의 현재가 조회
func (k *KiwoomClient) fetchPrice(stkCd string) (*models.StockPrice, error) {
	// 브라우저 표시용 종목코드는 거래소 접미사 제거 (005930_NX → 005930)
	displayCode := strings.SplitN(stkCd, "_", 2)[0]

	body, err := k.post("ka10001", "/api/dostk/stkinfo", map[string]string{
		"stk_cd": stkCd,
	})
	if err != nil {
		return nil, err
	}

	var result struct {
		StkNm      string `json:"stk_nm"`
		CurPrc     string `json:"cur_prc"`
		PredPre    string `json:"pred_pre"`
		FluRt      string `json:"flu_rt"`
		TrdeQty    string `json:"trde_qty"`
		OpenPric   string `json:"open_pric"`
		HighPric   string `json:"high_pric"`
		LowPric    string `json:"low_pric"`
		ReturnCode int    `json:"return_code"`
		ReturnMsg  string `json:"return_msg"`
	}

	if err := json.Unmarshal(body, &result); err != nil {
		return nil, fmt.Errorf("현재가 응답 파싱 실패: %w", err)
	}
	if result.ReturnCode != 0 {
		return nil, fmt.Errorf("현재가 조회 실패: %s", result.ReturnMsg)
	}

	price := &models.StockPrice{
		Code:         displayCode,
		Name:         result.StkNm,
		CurrentPrice: absParseIntSafe(result.CurPrc),
		ChangePrice:  parseIntSafe(result.PredPre),
		ChangeRate:   parseFloatSafe(result.FluRt),
		Volume:       absParseIntSafe(result.TrdeQty),
		High:         absParseIntSafe(result.HighPric),
		Low:          absParseIntSafe(result.LowPric),
		Open:         absParseIntSafe(result.OpenPric),
		UpdatedAt:    time.Now(),
	}

	// ka10003으로 체결강도 조회 (실패해도 나머지 데이터 반환)
	if cntrStr, err := k.getCntrStr(stkCd); err == nil {
		price.CntrStr = cntrStr
	}

	return price, nil
}

// getCntrStr 체결정보에서 최신 체결강도 조회 (ka10003, 5초 캐시)
func (k *KiwoomClient) getCntrStr(stockCode string) (float64, error) {
	// 캐시 확인
	if v, ok := k.cntrStrCache.Load(stockCode); ok {
		entry := v.(cntrStrCacheEntry)
		if time.Now().Before(entry.expiresAt) {
			return entry.value, nil
		}
	}

	body, err := k.post("ka10003", "/api/dostk/stkinfo", map[string]string{
		"stk_cd": stockCode,
	})
	if err != nil {
		return 0, err
	}

	var result struct {
		CntrInfr []struct {
			CntrStr string `json:"cntr_str"`
		} `json:"cntr_infr"`
		ReturnCode int `json:"return_code"`
	}

	if err := json.Unmarshal(body, &result); err != nil {
		return 0, err
	}
	if result.ReturnCode != 0 || len(result.CntrInfr) == 0 {
		return 0, fmt.Errorf("체결강도 없음")
	}

	val := parseFloatSafe(result.CntrInfr[0].CntrStr)
	// 결과 캐시 저장 (30초 — 스캔 3주기(30s) 동안 재호출 없음)
	k.cntrStrCache.Store(stockCode, cntrStrCacheEntry{value: val, expiresAt: time.Now().Add(30 * time.Second)})
	return val, nil
}

// GetDailyChart 일봉 차트 데이터 조회 (ka10005)
func (k *KiwoomClient) GetDailyChart(stockCode string) ([]models.CandleData, error) {
	body, err := k.post("ka10005", "/api/dostk/mrkcond", map[string]string{
		"stk_cd": stockCode,
	})
	if err != nil {
		return nil, err
	}

	var result struct {
		StkDdwkmm []struct {
			Date      string `json:"date"`
			OpenPric  string `json:"open_pric"`
			HighPric  string `json:"high_pric"`
			LowPric   string `json:"low_pric"`
			ClosePric string `json:"close_pric"`
			TrdeQty   string `json:"trde_qty"`
		} `json:"stk_ddwkmm"`
		ReturnCode int    `json:"return_code"`
		ReturnMsg  string `json:"return_msg"`
	}

	if err := json.Unmarshal(body, &result); err != nil {
		return nil, fmt.Errorf("일봉 응답 파싱 실패: %w", err)
	}
	if result.ReturnCode != 0 {
		return nil, fmt.Errorf("일봉 조회 실패: %s", result.ReturnMsg)
	}

	// 날짜 오름차순 정렬 (API는 내림차순 반환)
	candles := make([]models.CandleData, 0, len(result.StkDdwkmm))
	for i := len(result.StkDdwkmm) - 1; i >= 0; i-- {
		row := result.StkDdwkmm[i]
		candles = append(candles, models.CandleData{
			Time:   parseDateToUnix(row.Date),
			Open:   absParseIntSafe(row.OpenPric),
			High:   absParseIntSafe(row.HighPric),
			Low:    absParseIntSafe(row.LowPric),
			Close:  absParseIntSafe(row.ClosePric),
			Volume: absParseIntSafe(row.TrdeQty),
		})
	}
	return candles, nil
}

// GetMinuteChart 분봉 차트 데이터 조회 (ka10080)
// minutes: 1, 5, 10, 15, 30, 60
func (k *KiwoomClient) GetMinuteChart(stockCode string, minutes int) ([]models.CandleData, error) {
	body, err := k.post("ka10080", "/api/dostk/chart", map[string]string{
		"stk_cd":       stockCode,
		"tic_scope":    fmt.Sprintf("%d", minutes),
		"upd_stkpc_tp": "1",
	})
	if err != nil {
		return nil, err
	}

	var result struct {
		StkMinPoleChartQry []struct {
			CurPrc   string `json:"cur_prc"`
			TrdeQty  string `json:"trde_qty"`
			CntrTm   string `json:"cntr_tm"`
			OpenPric string `json:"open_pric"`
			HighPric string `json:"high_pric"`
			LowPric  string `json:"low_pric"`
		} `json:"stk_min_pole_chart_qry"`
		ReturnCode int    `json:"return_code"`
		ReturnMsg  string `json:"return_msg"`
	}

	if err := json.Unmarshal(body, &result); err != nil {
		return nil, fmt.Errorf("분봉 응답 파싱 실패: %w", err)
	}
	if result.ReturnCode != 0 {
		return nil, fmt.Errorf("분봉 조회 실패: %s", result.ReturnMsg)
	}

	// 시간 오름차순 정렬 (API는 내림차순 반환)
	rows := result.StkMinPoleChartQry
	candles := make([]models.CandleData, 0, len(rows))
	for i := len(rows) - 1; i >= 0; i-- {
		row := rows[i]
		candles = append(candles, models.CandleData{
			Time:   parseMinuteCandleTime(row.CntrTm),
			Open:   absParseIntSafe(row.OpenPric),
			High:   absParseIntSafe(row.HighPric),
			Low:    absParseIntSafe(row.LowPric),
			Close:  absParseIntSafe(row.CurPrc),
			Volume: absParseIntSafe(row.TrdeQty),
		})
	}
	return candles, nil
}

// parseMinuteCandleTime 분봉 체결시간(YYYYMMDDHHmmss) → Unix 초 변환
func parseMinuteCandleTime(s string) int64 {
	s = strings.TrimSpace(s)
	// "YYYYMMDDHHmmss" (14자리) 또는 "HHmmss" (6자리) 처리
	var t time.Time
	var err error
	switch len(s) {
	case 14:
		t, err = time.ParseInLocation("20060102150405", s, time.Local)
	case 12:
		t, err = time.ParseInLocation("060102150405", s, time.Local)
	default:
		return 0
	}
	if err != nil {
		return 0
	}
	return t.Unix()
}

// GetTopVolumeStocks 거래량 상위 종목 조회 (ka10030)
// market: "J"(KOSPI) → "001", "Q"(KOSDAQ) → "101"
func (k *KiwoomClient) GetTopVolumeStocks(market string, count int) ([]models.StockPrice, error) {
	// market 코드 변환
	mrktTp := "000"
	mktName := "전체"
	switch market {
	case "J":
		mrktTp = "001"
		mktName = "KOSPI"
	case "Q":
		mrktTp = "101"
		mktName = "KOSDAQ"
	}

	body, err := k.post("ka10030", "/api/dostk/rkinfo", map[string]string{
		"mrkt_tp":        mrktTp,
		"sort_tp":        "1", // 거래량 기준 정렬
		"mang_stk_incls": "1", // 관리종목 미포함
		"crd_tp":         "0",
		"trde_qty_tp":    "0",
		"pric_tp":        "0",
		"trde_prica_tp":  "0",
		"mrkt_open_tp":   "0",
		"stex_tp":        "3", // 통합
	})
	if err != nil {
		return nil, err
	}

	var result struct {
		TdyTrdeQtyUpper []struct {
			StkCd   string `json:"stk_cd"`
			StkNm   string `json:"stk_nm"`
			CurPrc  string `json:"cur_prc"`
			PredPre string `json:"pred_pre"`
			FluRt   string `json:"flu_rt"`
			TrdeQty string `json:"trde_qty"`
		} `json:"tdy_trde_qty_upper"`
		ReturnCode int    `json:"return_code"`
		ReturnMsg  string `json:"return_msg"`
	}

	if err := json.Unmarshal(body, &result); err != nil {
		return nil, fmt.Errorf("거래량순위 응답 파싱 실패: %w", err)
	}
	if result.ReturnCode != 0 {
		return nil, fmt.Errorf("거래량순위 조회 실패: %s", result.ReturnMsg)
	}

	stocks := make([]models.StockPrice, 0, count)
	for i, row := range result.TdyTrdeQtyUpper {
		if i >= count {
			break
		}
		stocks = append(stocks, models.StockPrice{
			Code:         row.StkCd,
			Name:         row.StkNm,
			CurrentPrice: absParseIntSafe(row.CurPrc),
			ChangePrice:  parseIntSafe(row.PredPre),
			ChangeRate:   parseFloatSafe(row.FluRt),
			Volume:       absParseIntSafe(row.TrdeQty),
			Market:       mktName,
			UpdatedAt:    time.Now(),
		})
	}
	return stocks, nil
}

// getOrderBook 호가잔량 조회 (ka10004) - 총매도잔량, 총매수잔량, 총매도잔량직전대비 반환
// WS 구독 중인 종목은 CacheService 캐시 우선 조회 → REST API 호출 최소화
func (k *KiwoomClient) getOrderBook(stockCode string) (totalAsk, totalBid, askChange int64, err error) {
	if cached, ok := GetCacheService().Get("orderbook:" + stockCode); ok {
		if ob, ok2 := cached.(*models.OrderBook); ok2 {
			return ob.TotalAskVol, ob.TotalBidVol, 0, nil
		}
	}

	body, err := k.post("ka10004", "/api/dostk/mrkcond", map[string]string{
		"stk_cd": stockCode,
	})
	if err != nil {
		return 0, 0, 0, err
	}
	var result struct {
		TotSelReq    string `json:"tot_sel_req"`
		TotBuyReq    string `json:"tot_buy_req"`
		TotSelReqJub string `json:"tot_sel_req_jub_pre"`
		ReturnCode   int    `json:"return_code"`
	}
	if err := json.Unmarshal(body, &result); err != nil {
		return 0, 0, 0, err
	}
	if result.ReturnCode != 0 {
		return 0, 0, 0, fmt.Errorf("호가 조회 실패 (return_code=%d)", result.ReturnCode)
	}
	return absParseIntSafe(result.TotSelReq),
		absParseIntSafe(result.TotBuyReq),
		parseIntSafe(result.TotSelReqJub),
		nil
}

// GetTopFluctuation 전일대비 등락률 상위 종목 조회 (ka10027)
// ascending=false: 상승률, ascending=true: 하락률
func (k *KiwoomClient) GetTopFluctuation(market string, ascending bool, count int) ([]models.StockPrice, error) {
	sortTp := "1" // 상승률
	if ascending {
		sortTp = "3" // 하락률
	}

	// market: "J"(KOSPI) → "001", "Q"(KOSDAQ) → "101", 그 외 "000"(전체)
	mrktTp := "000"
	mktName := "전체"
	switch market {
	case "J":
		mrktTp = "001"
		mktName = "KOSPI"
	case "Q":
		mrktTp = "101"
		mktName = "KOSDAQ"
	}

	body, err := k.post("ka10027", "/api/dostk/rkinfo", map[string]string{
		"mrkt_tp":        mrktTp,
		"sort_tp":        sortTp,
		"trde_qty_cnd":   "0000", // 거래량 전체
		"stk_cnd":        "0",    // 종목조건 전체
		"crd_cnd":        "0",    // 신용조건 전체
		"updown_incls":   "1",    // 상하한포함
		"pric_cnd":       "0",    // 가격조건 전체
		"trde_prica_cnd": "0",    // 거래대금조건 전체
		"stex_tp":        "1",    // KRX
	})
	if err != nil {
		return nil, err
	}

	var result struct {
		PredPreFluRtUpper []struct {
			StkCd      string `json:"stk_cd"`
			StkNm      string `json:"stk_nm"`
			CurPrc     string `json:"cur_prc"`
			PredPre    string `json:"pred_pre"`
			FluRt      string `json:"flu_rt"`
			NowTrdeQty string `json:"now_trde_qty"`
			CntrStr    string `json:"cntr_str"`
		} `json:"pred_pre_flu_rt_upper"`
		ReturnCode int    `json:"return_code"`
		ReturnMsg  string `json:"return_msg"`
	}

	if err := json.Unmarshal(body, &result); err != nil {
		return nil, fmt.Errorf("등락률 응답 파싱 실패: %w", err)
	}
	if result.ReturnCode != 0 {
		return nil, fmt.Errorf("등락률 조회 실패: %s", result.ReturnMsg)
	}

	stocks := make([]models.StockPrice, 0, count)
	for i, row := range result.PredPreFluRtUpper {
		if i >= count {
			break
		}
		stocks = append(stocks, models.StockPrice{
			Code:         row.StkCd,
			Name:         row.StkNm,
			CurrentPrice: absParseIntSafe(row.CurPrc),
			ChangePrice:  parseIntSafe(row.PredPre),
			ChangeRate:   parseFloatSafe(row.FluRt),
			Volume:       absParseIntSafe(row.NowTrdeQty),
			CntrStr:      parseFloatSafe(row.CntrStr),
			Market:       mktName,
			UpdatedAt:    time.Now(),
		})
	}
	return stocks, nil
}

// --- 유틸 함수 ---

func parseIntSafe(s string) int64 {
	s = strings.ReplaceAll(s, ",", "")
	s = strings.TrimPrefix(s, "+")
	n, _ := strconv.ParseInt(strings.TrimSpace(s), 10, 64)
	return n
}

// absParse 키움 API 가격 필드 파싱 (+/- 부호는 방향 표시용이므로 절댓값 반환)
func absParseIntSafe(s string) int64 {
	n := parseIntSafe(s)
	if n < 0 {
		return -n
	}
	return n
}

func parseFloatSafe(s string) float64 {
	s = strings.ReplaceAll(s, ",", "")
	s = strings.TrimPrefix(s, "+")
	f, _ := strconv.ParseFloat(strings.TrimSpace(s), 64)
	return f
}

// parseDateToUnix YYYYMMDD 형식을 Unix 타임스탬프(초)로 변환
func parseDateToUnix(dateStr string) int64 {
	t, err := time.ParseInLocation("20060102", dateStr, time.Local)
	if err != nil {
		return 0
	}
	return t.Unix()
}
