import requests
import json
import time
import os
from datetime import datetime, timedelta

try:
    import aiohttp
except ImportError:
    aiohttp = None

from modules.config import Config

class KISClient:
    """
    한국투자증권 (Korea Investment & Securities) REST API Client
    """
    def __init__(self, is_virtual=None):
        # Config에서 설정 로드
        self.app_key = Config.KIS_APP_KEY
        self.app_secret = Config.KIS_APP_SECRET
        self.cano = Config.KIS_ACCOUNT[:8]
        self.acnt_prdt_cd = Config.KIS_ACCOUNT[-2:] # "01" 등
        
        # 가상/실전 모드 설정
        if is_virtual is None:
            self.is_virtual = Config.KIS_IS_VIRTUAL
        else:
            self.is_virtual = is_virtual
            
        self.base_url = Config.KIS_BASE_URL
            
        self.access_token = None
        self.token_expired = None
        self.last_req_time = 0
        
        # 토큰 파일 경로 (영구 저장용)
        self.token_file = os.path.join(Config.DATA_DIR, "kis_token.json")
        self.load_token() # 초기화 시 토큰 로드 시도

    def _safe_int(self, val):
        """안전한 int 변환"""
        try:
            if not val:
                return 0
            return int(str(val).strip())
        except:
            return 0

    def _throttle(self):
        """API 요청 속도 제한 (초당 2회 이하로 제한)"""
        # 모의투자는 Rate Limit이 매우 엄격함 (초당 2~3회 권장)
        min_interval = 0.5  # 0.5초 대기 (초당 2회)
        now = time.time()
        elapsed = now - self.last_req_time
        
        if elapsed < min_interval:
            time.sleep(min_interval - elapsed)
            
        self.last_req_time = time.time()

    def load_token(self):
        """파일에서 토큰 로드"""
        if os.path.exists(self.token_file):
            try:
                with open(self.token_file, "r", encoding="utf-8") as f:
                    data = json.load(f)
                    # 만료 시간 체크
                    expire_str = data.get("expired_at")
                    if expire_str:
                        expire_dt = datetime.strptime(expire_str, "%Y-%m-%d %H:%M:%S")
                        if datetime.now() < expire_dt:
                            self.access_token = data.get("access_token")
                            self.token_expired = expire_dt
                            print(f"📂 [KIS] Saved Token Loaded (Expires: {expire_str})")
            except Exception as e:
                print(f"⚠️ Failed to load token file: {e}")

    def save_token(self):
        """토큰 파일 저장"""
        if not self.access_token or not self.token_expired:
            return
        
        try:
            data = {
                "access_token": self.access_token,
                "expired_at": self.token_expired.strftime("%Y-%m-%d %H:%M:%S")
            }
            with open(self.token_file, "w", encoding="utf-8") as f:
                json.dump(data, f, indent=2)
        except Exception as e:
            print(f"⚠️ Failed to save token file: {e}")

    def _get_headers(self, tr_id=None):
        """공통 헤더 생성"""
        headers = {
            "Content-Type": "application/json; charset=utf-8",
            "authorization": f"Bearer {self.access_token}",
            "appkey": self.app_key,
            "appsecret": self.app_secret,
        }
        if tr_id:
            headers["tr_id"] = tr_id
            
        return headers

    def ensure_token(self, force=False):
        """접근 토큰 발급 (OAuth 2.0) 및 유효성 관리"""
        # 토큰이 있고, 만료 시간이 아직 안 지났으면 재사용
        if not force and self.access_token and self.token_expired:
            if datetime.now() < self.token_expired:
                return

        # 앱키 확인
        if not self.app_key or not self.app_secret:
            print("❌ [KIS] App Key or Secret is missing!")
            return

        url = f"{self.base_url}/oauth2/tokenP"
        payload = {
            "grant_type": "client_credentials",
            "appkey": self.app_key,
            "appsecret": self.app_secret
        }
        
        try:
            print(f"🔑 [KIS] 토큰 발급 요청: {url}")
            res = requests.post(url, json=payload, timeout=Config.HTTP_TIMEOUT)
            res.raise_for_status()
            data = res.json()
            
            self.access_token = data.get('access_token')
            
            # 만료 시간 설정
            expires_in = int(data.get('expires_in', 86400))
            self.token_expired = datetime.now() + timedelta(seconds=expires_in - 60)
            
            # 파일 저장
            self.save_token()
            
            print(f"✅ [KIS] 토큰 발급 성공 (만료: {self.token_expired.strftime('%Y-%m-%d %H:%M:%S')})")
            
        except Exception as e:
            # 1분 제한 에러 핸들링 (EGW00133)
            retry = False
            if isinstance(e, requests.exceptions.RequestException) and e.response is not None:
                err_text = e.response.text
                print(f"📄 [KIS Error]: {err_text}")
                if "EGW00133" in err_text:
                    print("⏳ [KIS] Rate Limit Hit (1 min). Waiting 65s...")
                    time.sleep(65) # 1분 대기
                    retry = True
            
            if retry:
                # 재귀 호출 (한 번만)
                self.ensure_token()
                return

            print(f"❌ [KIS] 토큰 발급 실패: {e}")
            self.access_token = None
            raise e

    def get_hash_key(self, datas):
        """주문 시 필요한 Hash Key 생성 (Koreainvestment header 특화)"""
        url = f"{self.base_url}/uapi/hashkey"
        headers = {
            "content-type": "application/json; charset=utf-8",
            "appkey": self.app_key,
            "appsecret": self.app_secret
        }
        try:
            res = requests.post(url, headers=headers, json=datas, timeout=Config.HTTP_TIMEOUT)
            return res.json()["HASH"]
        except Exception as e:
            print(f"❌ Hash Key 생성 실패: {e}")
            return None

    def _request_api(self, method, endpoint, tr_id, params=None, data=None, use_hash=False):
        """API 요청 공통 핸들러 (토큰 만료 시 자동 갱신)"""
        self._throttle()
        self.ensure_token()
        
        url = f"{self.base_url}/{endpoint}"
        headers = self._get_headers(tr_id)
        
        if use_hash and data:
            hash_key = self.get_hash_key(data)
            if hash_key:
                headers["hashkey"] = hash_key

        try:
            if method == "GET":
                res = requests.get(url, headers=headers, params=params,
                                   timeout=Config.HTTP_TIMEOUT)
            else:
                res = requests.post(url, headers=headers, json=data,
                                    timeout=Config.HTTP_TIMEOUT)

            # 토큰 만료 체크 (500 에러 or msg_cd 확인)
            is_token_error = False
            try:
                # KIS는 토큰 만료 시 500을 주거나 200/403 등과 함께 msg_cd로 알려줌
                if res.status_code == 500 or res.status_code == 401 or res.status_code == 403:
                    err_data = res.json()
                    # EGW00121: 유효하지 않은 토큰, EGW00123: 만료된 토큰
                    if err_data.get('msg_cd') in ['EGW00121', 'EGW00123']:
                        is_token_error = True
            except:
                pass

            if is_token_error:
                print("🔄 [KIS] Token expired (caught). Refreshing...")
                self.ensure_token(force=True)
                headers = self._get_headers(tr_id)
                if use_hash and data and "hashkey" in headers:
                     pass # Hash 재활용

                if method == "GET":
                    res = requests.get(url, headers=headers, params=params,
                                       timeout=Config.HTTP_TIMEOUT)
                else:
                    res = requests.post(url, headers=headers, json=data,
                                        timeout=Config.HTTP_TIMEOUT)
            
            res.raise_for_status()
            return res.json()
            
        except Exception as e:
            print(f"❌ [KIS] API Request Failed: {url} | {e}")
            if isinstance(e, requests.exceptions.RequestException) and e.response is not None:
                print(f"📄 [KIS Error Body]: {e.response.text}")
            raise e

    def get_balance(self):
        """주식 잔고 조회"""
        tr_id = "VTTC8434R" if self.is_virtual else "TTTC8434R"
        endpoint = "uapi/domestic-stock/v1/trading/inquire-balance"
        
        # 쿼리 파라미터
        params = {
            "CANO": self.cano,
            "ACNT_PRDT_CD": self.acnt_prdt_cd,
            "AFHR_FLPR_YN": "N",
            "OFL_YN": "",
            "INQR_DVSN": "02",
            "UNPR_DVSN": "01",
            "FUND_STTL_ICLD_YN": "N",
            "FNCG_AMT_AUTO_RDPT_YN": "N",
            "PRCS_DVSN": "00",
            "CTX_AREA_FK100": "",
            "CTX_AREA_NK100": ""
        }
        
        try:
            data = self._request_api("GET", endpoint, tr_id, params=params)
            
            # 응답 정리
            if data['rt_cd'] != '0':
                return {"error": data['msg1']}
                
            holdings = []
            for item in data['output1']:
                if int(item['hldg_qty']) > 0:
                    holdings.append({
                        "code": item['pdno'],
                        "name": item['prdt_name'],
                        "qty": int(item['hldg_qty']),
                        "yield": float(item['evlu_pfls_rt']),
                        "purchase_price": float(item['pchs_avg_pric']), # 매입평균가
                        "current_price": float(item['prpr']),           # 현재가
                        "profit_loss": int(item['evlu_pfls_amt'])       # 평가손익
                    })
            
            summary = data['output2'][0]
            return {
                "holdings": holdings,
                "total_eval": int(summary['tot_evlu_amt']),
                "deposit": int(summary['dnca_tot_amt']),
                "today_buy_amt": int(summary.get('thdt_buy_amt', 0)),  # 금일매수금액 (T+2 차감 전 당일 집행액)
            }
        except Exception as e:
            return {"error": str(e)}

    def order(self, ticker, qty, buy_sell, price=0, order_type="market"):
        """주문
        buy_sell: 'BUY' or 'SELL'
        order_type: 'market'(시장가), 'limit'(지정가), 'conditional'(조건부지정가)
        price: 지정가일 때 주문 가격 (market이면 무시)
        """
        self._throttle()
        self.ensure_token()

        # 모의투자/실전 TR ID 구분
        if buy_sell == 'BUY':
            tr_id = "VTTC0802U" if self.is_virtual else "TTTC0802U"
        else:
            tr_id = "VTTC0801U" if self.is_virtual else "TTTC0801U"

        # 주문 구분 코드
        # 00: 지정가, 01: 시장가, 03: 최유리지정가, 05: 장전시간외, 06: 장후시간외
        if order_type == "limit" and price > 0:
            ord_dvsn = "00"
            ord_unpr = str(int(price))
            order_type_str = f"지정가({price:,.0f})"
        elif order_type == "conditional" and price > 0:
            ord_dvsn = "03"  # 최유리지정가
            ord_unpr = str(int(price))
            order_type_str = f"조건부({price:,.0f})"
        else:
            ord_dvsn = "01"  # 시장가
            ord_unpr = "0"
            order_type_str = "시장가"

        url = f"{self.base_url}/uapi/domestic-stock/v1/trading/order-cash"

        datas = {
            "CANO": self.cano,
            "ACNT_PRDT_CD": self.acnt_prdt_cd,
            "PDNO": ticker,
            "ORD_DVSN": ord_dvsn,
            "ORD_QTY": str(qty),
            "ORD_UNPR": ord_unpr
        }

        headers = self._get_headers(tr_id=tr_id)
        hash_key = self.get_hash_key(datas)
        if hash_key:
            headers["hashkey"] = hash_key
        else:
            print("⚠️ [KIS] Hash Key 생성 실패 (주문 전송 시도)")

        try:
            print(f"📤 [KIS] 주문 전송: {buy_sell} {ticker} {qty}ea ({order_type_str})")
            res = requests.post(url, headers=headers, json=datas, timeout=Config.HTTP_TIMEOUT)
            res.raise_for_status()
            data = res.json()

            print(f"📥 [KIS] 주문 응답 코드(rt_cd): {data['rt_cd']}")
            print(f"📥 [KIS] 주문 응답 메시지(msg1): {data['msg1']}")

            if data['rt_cd'] != '0':
                return {"status": False, "msg": data['msg1'], "rt_cd": data['rt_cd']}

            return {"status": True, "msg": "주문 전송 완료", "order_no": data['output']['ODNO'], "rt_cd": data['rt_cd']}
        except Exception as e:
            return {"status": False, "msg": str(e), "rt_cd": "EXCEPTION"}

    def get_current_price(self, ticker):
        """현재가 조회"""
        self._throttle()
        self.ensure_token()
        url = f"{self.base_url}/uapi/domestic-stock/v1/quotations/inquire-price"
        headers = self._get_headers(tr_id="FHKST01010100")
        
        params = {
            "FID_COND_MRKT_DIV_CODE": "J",
            "FID_INPUT_ISCD": ticker
        }
        
        try:
            res = requests.get(url, headers=headers, params=params,
                               timeout=Config.HTTP_TIMEOUT)
            res.raise_for_status()
            data = res.json()
            if data['rt_cd'] != '0':
                return None
            return int(data['output']['stck_prpr']) # 현재가
        except Exception as e:
            print(f"❌ 현재가 조회 실패: {e}")
            return None

    def _get_daily_ohlcv_by_range(self, ticker, period="D", count=100):
        """기간별시세 API (FHKST03010100) - OHLCV 전체 반환
        output2에서 stck_oprc, stck_hgpr, stck_lwpr, stck_clpr, acml_vol 파싱
        """
        self._throttle()
        self.ensure_token()

        end_date = datetime.now().strftime("%Y%m%d")
        start_date = (datetime.now() - timedelta(days=int(count * 1.6))).strftime("%Y%m%d")

        url = f"{self.base_url}/uapi/domestic-stock/v1/quotations/inquire-daily-itemchartprice"
        headers = self._get_headers(tr_id="FHKST03010100")

        params = {
            "FID_COND_MRKT_DIV_CODE": "J",
            "FID_INPUT_ISCD": ticker,
            "FID_INPUT_DATE_1": start_date,
            "FID_INPUT_DATE_2": end_date,
            "FID_PERIOD_DIV_CODE": period,
            "FID_ORG_ADJ_PRC": "1"
        }

        try:
            res = requests.get(url, headers=headers, params=params,
                               timeout=Config.HTTP_TIMEOUT)
            res.raise_for_status()
            data = res.json()

            if data.get('rt_cd') != '0':
                return None

            output = data.get('output2', [])
            if not output:
                return None

            opens, highs, lows, closes, volumes = [], [], [], [], []
            for item in output:
                try:
                    c = int(item.get('stck_clpr', 0) or 0)
                    o = int(item.get('stck_oprc', 0) or 0)
                    h = int(item.get('stck_hgpr', 0) or 0)
                    l = int(item.get('stck_lwpr', 0) or 0)
                    v = int(item.get('acml_vol', 0) or 0)
                    if c > 0:
                        opens.append(o if o > 0 else c)
                        highs.append(h if h > 0 else c)
                        lows.append(l if l > 0 else c)
                        closes.append(c)
                        volumes.append(v)
                except (ValueError, TypeError):
                    pass

            if not closes:
                return None

            # API는 최신순 → 과거→현재 순으로 변환
            opens.reverse(); highs.reverse(); lows.reverse()
            closes.reverse(); volumes.reverse()

            result = {
                'open': opens[-count:],
                'high': highs[-count:],
                'low': lows[-count:],
                'close': closes[-count:],
                'volume': volumes[-count:]
            }
            print(f"[KIS] {ticker} OHLCV: {len(result['close'])}개 ({start_date}~{end_date})")
            return result

        except Exception as e:
            print(f"⚠️ [KIS] OHLCV 조회 실패 ({ticker}): {e}")
            return None

    def get_daily_ohlcv(self, ticker, period="D", count=100):
        """일별 OHLCV 시세 조회 (기술적 분석 + LSTM 7차원 입력용)
        1차: 기간별시세 API OHLCV 파싱 (100일)
        2차: 기존 close-only fallback
        """
        ohlcv = self._get_daily_ohlcv_by_range(ticker, period, count)
        if ohlcv and len(ohlcv['close']) >= 30:
            return ohlcv

        # fallback: close만 반환 (가짜 OHLCV)
        print(f"[KIS] {ticker} OHLCV 실패 → close-only fallback")
        prices = self._get_daily_price_by_range(ticker, period, count)
        if not prices:
            return None
        return {
            'open': prices, 'high': prices, 'low': prices,
            'close': prices, 'volume': []
        }

    def _get_daily_price_by_range(self, ticker, period="D", count=100):
        """기간별시세 API (FHKST03010100) - 날짜 범위로 최대 100일 데이터 반환
        inquire-daily-price(FHKST01010400)가 30일만 반환하는 한계 극복"""
        self._throttle()
        self.ensure_token()

        end_date = datetime.now().strftime("%Y%m%d")
        # 영업일 count개 확보를 위해 역일 1.6배 요청 (주말/공휴일 여유)
        start_date = (datetime.now() - timedelta(days=int(count * 1.6))).strftime("%Y%m%d")

        url = f"{self.base_url}/uapi/domestic-stock/v1/quotations/inquire-daily-itemchartprice"
        headers = self._get_headers(tr_id="FHKST03010100")

        params = {
            "FID_COND_MRKT_DIV_CODE": "J",
            "FID_INPUT_ISCD": ticker,
            "FID_INPUT_DATE_1": start_date,
            "FID_INPUT_DATE_2": end_date,
            "FID_PERIOD_DIV_CODE": period,
            "FID_ORG_ADJ_PRC": "1"
        }

        try:
            res = requests.get(url, headers=headers, params=params,
                               timeout=Config.HTTP_TIMEOUT)
            res.raise_for_status()
            data = res.json()

            if data.get('rt_cd') != '0':
                return []

            # 기간별시세는 output2에 배열로 반환
            output = data.get('output2', [])
            if not output:
                return []

            prices = []
            for item in output:
                clpr = item.get('stck_clpr', '')
                if clpr and clpr != '0':
                    try:
                        prices.append(int(clpr))
                    except ValueError:
                        pass

            prices.reverse()  # API는 최신순 → 과거→현재 순으로 변환
            result = prices[-count:]
            print(f"[KIS] {ticker} 기간별시세: {len(result)}개 "
                  f"({start_date}~{end_date})")
            return result

        except Exception as e:
            print(f"⚠️ [KIS] 기간별시세 조회 실패 ({ticker}): {e}")
            return []

    def get_daily_price(self, ticker, period="D", count=100):
        """일별 시세 조회 (기술적 분석 + LSTM용)
        1차: 기간별시세 API (100일, LSTM 학습 가능)
        2차: 구형 API fallback (30일)
        """
        # 1차: 기간별시세 API (FHKST03010100) - 100일
        prices = self._get_daily_price_by_range(ticker, period, count)
        if prices and len(prices) >= 30:
            return prices

        # 2차: 구형 API fallback (FHKST01010400) - 30일
        print(f"[KIS] {ticker} 기간별시세 실패 → 구형 API(30일) fallback")
        self._throttle()
        self.ensure_token()
        url = f"{self.base_url}/uapi/domestic-stock/v1/quotations/inquire-daily-price"
        headers = self._get_headers(tr_id="FHKST01010400")
        params = {
            "FID_COND_MRKT_DIV_CODE": "J",
            "FID_INPUT_ISCD": ticker,
            "FID_PERIOD_DIV_CODE": period,
            "FID_ORG_ADJ_PRC": "1"
        }
        try:
            res = requests.get(url, headers=headers, params=params,
                               timeout=Config.HTTP_TIMEOUT)
            res.raise_for_status()
            data = res.json()
            if data.get('rt_cd') != '0':
                return []
            prices = [int(item['stck_clpr']) for item in data['output']
                      if item.get('stck_clpr')]
            prices.reverse()
            return prices
        except Exception as e:
            print(f"❌ 일별 시세 조회 실패 ({ticker}): {e}")
            return []

    def get_volume_rank(self, limit=5):
        """거래량 상위 종목 조회"""
        self._throttle()
        self.ensure_token()
        url = f"{self.base_url}/uapi/domestic-stock/v1/quotations/volume-rank"
        headers = self._get_headers(tr_id="FHPST01710000")
        
        params = {
            "FID_COND_MRKT_DIV_CODE": "J", # 주식, ETF, ETN 전체
            "FID_COND_SCR_RSLT_GD_CD": "20171", # 전체
            "FID_INPUT_ISCD": "0000",   # 전체
            "FID_DIV_CLS_CODE": "0",  # 0: 전체
            "FID_BLNG_CLS_CODE": "0", # 0: 전체
            "FID_TRGT_CLS_CODE": "111111111", # 필터링 조건 (이대로 두면 됨)
            "FID_TRGT_EXCLS_CLS_CODE": "0000000000", # 제외 조건
            "FID_INPUT_PRICE_1": "",
            "FID_INPUT_PRICE_2": "",
            "FID_VOL_CNT": "",
            "FID_INPUT_DATE_1": "" 
        }
        
        try:
            res = requests.get(url, headers=headers, params=params,
                               timeout=Config.HTTP_TIMEOUT)
            res.raise_for_status()
            data = res.json()
            if data['rt_cd'] != '0':
                return []

            results = []
            for item in data['output'][:limit]:
                # 코드는 shtn_iscd, 이름은 hts_kor_isnm
                results.append({
                    "code": item['mksc_shrn_iscd'],
                    "name": item['hts_kor_isnm'],
                    "volume": int(item['acml_vol']),
                    "price": int(item['stck_prpr'])
                })
            return results
        except Exception as e:
            print(f"❌ 거래량 순위 조회 실패: {e}")
            return []

    def buy_stock(self, ticker, qty):
        return self.order(ticker, qty, 'BUY')

    def get_current_index(self, ticker):
        """지수 현재가 조회 (업종/지수)
        ticker: 0001 (KOSPI), 1001 (KOSDAQ), etc.
        """
        endpoint = "uapi/domestic-stock/v1/quotations/inquire-index-price"
        params = {
            "FID_COND_MRKT_DIV_CODE": "U", # U: 업종/지수
            "FID_INPUT_ISCD": ticker
        }
        
        try:
            data = self._request_api("GET", endpoint, "FHKUP03500100", params=params)
            if data['rt_cd'] != '0':
                return None
            o = data['output']
            def _f(val): return float(val) if val else 0.0
            def _i(val): return int(float(val)) if val else 0
            return {
                "price": _f(o.get('bstp_nmix_prpr')),       # 현재지수
                "change": _f(o.get('bstp_nmix_prdy_ctrt')), # 등락률(%)
                "change_val": _f(o.get('bstp_nmix_prdy_vrss')), # 전일 대비 포인트
                "high": _f(o.get('bstp_nmix_hgpr')),        # 장중 고가
                "low": _f(o.get('bstp_nmix_lwpr')),         # 장중 저가
                "prev_close": _f(o.get('prdy_nmix')),       # 전일 종가
                "volume": _i(o.get('acml_vol')),             # 누적 거래량(천주)
                "trade_value": _i(o.get('acml_tr_pbmn')),   # 누적 거래대금(백만원)
            }
        except Exception as e:
            print(f"❌ 지수 조회 실패({ticker}): {e}")
            return None

    def sell_stock(self, ticker, qty):
        return self.order(ticker, qty, 'SELL')

    def get_daily_index_price(self, ticker, period="D"):
        """지수 일별 시세 조회 (Market Stress Index용)"""
        endpoint = "uapi/domestic-stock/v1/quotations/inquire-daily-indexchartprice"
        
        # 날짜 계산 (최근 100일)
        end_dt = datetime.now().strftime("%Y%m%d")
        start_dt = (datetime.now() - timedelta(days=100)).strftime("%Y%m%d")
        
        params = {
            "FID_COND_MRKT_DIV_CODE": "U", # U: 업종/지수
            "FID_INPUT_ISCD": ticker,
            "FID_INPUT_DATE_1": start_dt,  # 시작일
            "FID_INPUT_DATE_2": end_dt,    # 종료일
            "FID_PERIOD_DIV_CODE": period,
            "FID_ORG_ADJ_PRC": "0"         # 수정주가 반영 여부
        }

        try:
            data = self._request_api("GET", endpoint, "FHKUP03500200", params=params)
            if data['rt_cd'] != '0':
                return []
            
            # output 리스트: [ {bstp_nmix_prpr: 지수, ...}, ... ]
            prices = [float(item['bstp_nmix_prpr']) for item in data['output']]
            prices.reverse() # 과거 -> 현재
            return prices
        except Exception as e:
            print(f"❌ 지수 일별 시세 조회 실패({ticker}): {e}")
            return []

    def get_investor_trend(self, ticker):
        """종목별 투자자(외인/기관) 매매동향 조회"""
        self._throttle()
        self.ensure_token()
        url = f"{self.base_url}/uapi/domestic-stock/v1/quotations/inquire-investor"
        headers = self._get_headers(tr_id="FHKST01010900")

        params = {
            "FID_COND_MRKT_DIV_CODE": "J",
            "FID_INPUT_ISCD": ticker
        }

        try:
            res = requests.get(url, headers=headers, params=params,
                               timeout=Config.HTTP_TIMEOUT)
            res.raise_for_status()
            data = res.json()
            if data['rt_cd'] != '0':
                return None

            trends = []
            for item in data['output'][:5]:
                trends.append({
                    "date": item['stck_bsop_date'],
                    "foreigner": self._safe_int(item.get('frgn_ntby_qty')),
                    "institutional": self._safe_int(item.get('orgn_ntby_qty')),
                    "price_change": float(item['prdy_vrss'])
                })

            return trends
        except Exception as e:
            print(f"[KIS] 투자자 동향 조회 실패({ticker}): {e}")
            return None


class KISAsyncClient:
    """
    비동기 KIS API 클라이언트
    - aiohttp 기반 HTTP 호출
    - 동기 KISClient의 토큰/설정을 공유
    - 다중 종목 병렬 수집용
    """
    def __init__(self, sync_client):
        self.sync = sync_client
        self.min_interval = 0.5  # 초당 2회 제한

    async def _async_get(self, session, url, headers, params):
        """비동기 GET 요청"""
        try:
            timeout = aiohttp.ClientTimeout(total=Config.HTTP_TIMEOUT) if aiohttp else None
            async with session.get(url, headers=headers, params=params,
                                   timeout=timeout) as resp:
                return await resp.json()
        except Exception as e:
            print(f"[KIS Async] Request failed: {e}")
            return None

    async def get_daily_price_async(self, ticker):
        """비동기 일별 시세 조회 (close only, 하위 호환)"""
        import aiohttp

        self.sync.ensure_token()
        url = f"{self.sync.base_url}/uapi/domestic-stock/v1/quotations/inquire-daily-price"
        headers = self.sync._get_headers(tr_id="FHKST01010400")
        params = {
            "FID_COND_MRKT_DIV_CODE": "J",
            "FID_INPUT_ISCD": ticker,
            "FID_PERIOD_DIV_CODE": "D",
            "FID_ORG_ADJ_PRC": "1"
        }

        async with aiohttp.ClientSession() as session:
            data = await self._async_get(session, url, headers, params)
            if data and data.get('rt_cd') == '0':
                prices = [int(item['stck_clpr']) for item in data['output']]
                prices.reverse()
                return prices
        return []

    async def get_daily_ohlcv_async(self, ticker, count=100):
        """비동기 OHLCV 조회 (기간별시세 API 사용)"""
        import aiohttp
        from datetime import datetime, timedelta

        self.sync.ensure_token()
        end_date = datetime.now().strftime("%Y%m%d")
        start_date = (datetime.now() - timedelta(days=int(count * 1.6))).strftime("%Y%m%d")

        url = f"{self.sync.base_url}/uapi/domestic-stock/v1/quotations/inquire-daily-itemchartprice"
        headers = self.sync._get_headers(tr_id="FHKST03010100")
        params = {
            "FID_COND_MRKT_DIV_CODE": "J",
            "FID_INPUT_ISCD": ticker,
            "FID_INPUT_DATE_1": start_date,
            "FID_INPUT_DATE_2": end_date,
            "FID_PERIOD_DIV_CODE": "D",
            "FID_ORG_ADJ_PRC": "1"
        }

        async with aiohttp.ClientSession() as session:
            data = await self._async_get(session, url, headers, params)
            if data and data.get('rt_cd') == '0':
                output = data.get('output2', [])
                opens, highs, lows, closes, volumes = [], [], [], [], []
                for item in output:
                    try:
                        c = int(item.get('stck_clpr', 0) or 0)
                        if c > 0:
                            opens.append(int(item.get('stck_oprc', 0) or c))
                            highs.append(int(item.get('stck_hgpr', 0) or c))
                            lows.append(int(item.get('stck_lwpr', 0) or c))
                            closes.append(c)
                            volumes.append(int(item.get('acml_vol', 0) or 0))
                    except (ValueError, TypeError):
                        pass
                if closes:
                    opens.reverse(); highs.reverse(); lows.reverse()
                    closes.reverse(); volumes.reverse()
                    return {
                        'open': opens[-count:], 'high': highs[-count:],
                        'low': lows[-count:], 'close': closes[-count:],
                        'volume': volumes[-count:]
                    }
        return None

    async def get_investor_trend_async(self, ticker):
        """비동기 투자자 동향 조회"""
        import aiohttp

        self.sync.ensure_token()
        url = f"{self.sync.base_url}/uapi/domestic-stock/v1/quotations/inquire-investor"
        headers = self.sync._get_headers(tr_id="FHKST01010900")
        params = {
            "FID_COND_MRKT_DIV_CODE": "J",
            "FID_INPUT_ISCD": ticker
        }

        async with aiohttp.ClientSession() as session:
            data = await self._async_get(session, url, headers, params)
            if data and data.get('rt_cd') == '0':
                trends = []
                for item in data['output'][:5]:
                    trends.append({
                        "date": item['stck_bsop_date'],
                        "foreigner": self.sync._safe_int(item.get('frgn_ntby_qty')),
                        "institutional": self.sync._safe_int(item.get('orgn_ntby_qty')),
                        "price_change": float(item['prdy_vrss'])
                    })
                return trends
        return None

    async def get_daily_prices_batch(self, tickers):
        """여러 종목의 일별 시세(close only)를 병렬로 조회 (하위 호환)"""
        import aiohttp
        import asyncio

        self.sync.ensure_token()
        results = {}

        async with aiohttp.ClientSession() as session:
            tasks = []
            for i, ticker in enumerate(tickers):
                if i > 0:
                    await asyncio.sleep(self.min_interval)

                url = f"{self.sync.base_url}/uapi/domestic-stock/v1/quotations/inquire-daily-price"
                headers = self.sync._get_headers(tr_id="FHKST01010400")
                params = {
                    "FID_COND_MRKT_DIV_CODE": "J",
                    "FID_INPUT_ISCD": ticker,
                    "FID_PERIOD_DIV_CODE": "D",
                    "FID_ORG_ADJ_PRC": "1"
                }
                tasks.append((ticker, self._async_get(session, url, headers, params)))

            for ticker, task in tasks:
                data = await task
                if data and data.get('rt_cd') == '0':
                    prices = [int(item['stck_clpr']) for item in data['output']]
                    prices.reverse()
                    results[ticker] = prices
                else:
                    results[ticker] = []

        return results

    async def get_daily_ohlcv_batch(self, tickers, count=100):
        """여러 종목의 OHLCV를 병렬로 조회 (기간별시세 API)"""
        import aiohttp
        import asyncio
        from datetime import datetime, timedelta

        self.sync.ensure_token()
        results = {}

        end_date = datetime.now().strftime("%Y%m%d")
        start_date = (datetime.now() - timedelta(days=int(count * 1.6))).strftime("%Y%m%d")

        async with aiohttp.ClientSession() as session:
            tasks = []
            for i, ticker in enumerate(tickers):
                if i > 0:
                    await asyncio.sleep(self.min_interval)

                url = f"{self.sync.base_url}/uapi/domestic-stock/v1/quotations/inquire-daily-itemchartprice"
                headers = self.sync._get_headers(tr_id="FHKST03010100")
                params = {
                    "FID_COND_MRKT_DIV_CODE": "J",
                    "FID_INPUT_ISCD": ticker,
                    "FID_INPUT_DATE_1": start_date,
                    "FID_INPUT_DATE_2": end_date,
                    "FID_PERIOD_DIV_CODE": "D",
                    "FID_ORG_ADJ_PRC": "1"
                }
                tasks.append((ticker, self._async_get(session, url, headers, params)))

            for ticker, task in tasks:
                data = await task
                if data and data.get('rt_cd') == '0':
                    output = data.get('output2', [])
                    opens, highs, lows, closes, volumes = [], [], [], [], []
                    for item in output:
                        try:
                            c = int(item.get('stck_clpr', 0) or 0)
                            if c > 0:
                                opens.append(int(item.get('stck_oprc', 0) or c))
                                highs.append(int(item.get('stck_hgpr', 0) or c))
                                lows.append(int(item.get('stck_lwpr', 0) or c))
                                closes.append(c)
                                volumes.append(int(item.get('acml_vol', 0) or 0))
                        except (ValueError, TypeError):
                            pass
                    if closes:
                        opens.reverse(); highs.reverse(); lows.reverse()
                        closes.reverse(); volumes.reverse()
                        results[ticker] = {
                            'open': opens[-count:], 'high': highs[-count:],
                            'low': lows[-count:], 'close': closes[-count:],
                            'volume': volumes[-count:]
                        }
                    else:
                        results[ticker] = None
                else:
                    results[ticker] = None

        return results

    async def get_investor_trends_batch(self, tickers):
        """여러 종목의 투자자 동향을 병렬로 조회"""
        import aiohttp
        import asyncio

        self.sync.ensure_token()
        results = {}

        async with aiohttp.ClientSession() as session:
            tasks = []
            for i, ticker in enumerate(tickers):
                if i > 0:
                    await asyncio.sleep(self.min_interval)

                url = f"{self.sync.base_url}/uapi/domestic-stock/v1/quotations/inquire-investor"
                headers = self.sync._get_headers(tr_id="FHKST01010900")
                params = {
                    "FID_COND_MRKT_DIV_CODE": "J",
                    "FID_INPUT_ISCD": ticker
                }
                tasks.append((ticker, self._async_get(session, url, headers, params)))

            for ticker, task in tasks:
                data = await task
                if data and data.get('rt_cd') == '0':
                    trends = []
                    for item in data['output'][:5]:
                        trends.append({
                            "date": item['stck_bsop_date'],
                            "foreigner": self.sync._safe_int(item.get('frgn_ntby_qty')),
                            "institutional": self.sync._safe_int(item.get('orgn_ntby_qty')),
                            "price_change": float(item['prdy_vrss'])
                        })
                    results[ticker] = trends
                else:
                    results[ticker] = None

        return results
