"""KIS REST API client — 분봉 + 호가. V1 토큰 read-only 공유."""
from __future__ import annotations
import asyncio
import json
import logging
import time
from datetime import datetime, timedelta
from pathlib import Path
from zoneinfo import ZoneInfo

import httpx

logger = logging.getLogger(__name__)
KST = ZoneInfo("Asia/Seoul")

_MAX_ATTEMPTS = 3
_THROTTLE_INTERVAL = 0.5  # 초당 2회 제한


class KISClient:
    """KIS REST (분봉 + 호가). V1 토큰 파일 read-only."""

    def __init__(
        self,
        app_key: str, app_secret: str, account: str, is_virtual: bool,
        v1_token_path: Path,
        timeout: float = 10.0,
    ):
        self._app_key = app_key
        self._app_secret = app_secret
        self._account = account
        self._is_virtual = is_virtual
        self._v1_token_path = Path(v1_token_path)
        self._base_url = (
            "https://openapivts.koreainvestment.com:29443" if is_virtual
            else "https://openapi.koreainvestment.com:9443"
        )
        self._client = httpx.AsyncClient(timeout=timeout)
        self._token_cache: tuple[str, float] | None = None  # (token, file_mtime)
        self._last_throttle_at = 0.0
        self._throttle_lock = asyncio.Lock()

    async def close(self) -> None:
        await self._client.aclose()

    def _read_v1_token(self) -> str:
        if not self._v1_token_path.exists():
            raise RuntimeError(f"V1 token file missing: {self._v1_token_path}")
        mtime = self._v1_token_path.stat().st_mtime
        if self._token_cache and self._token_cache[1] == mtime:
            return self._token_cache[0]
        data = json.loads(self._v1_token_path.read_text(encoding="utf-8"))
        token = data.get("access_token", "")
        if not token:
            raise RuntimeError("V1 token file has no access_token")
        self._token_cache = (token, mtime)
        return token

    async def _throttle(self) -> None:
        # F2: Lock으로 직렬화. 없으면 asyncio.gather 동시 호출 시 race로
        # 같은 elapsed 계산 후 동시에 깨어나 KIS 초당 2회(EGW00201) 위반.
        async with self._throttle_lock:
            elapsed = time.monotonic() - self._last_throttle_at
            if elapsed < _THROTTLE_INTERVAL:
                await asyncio.sleep(_THROTTLE_INTERVAL - elapsed)
            self._last_throttle_at = time.monotonic()

    def _common_headers(self, tr_id: str) -> dict[str, str]:
        token = self._read_v1_token()
        return {
            "authorization": f"Bearer {token}",
            "appkey": self._app_key,
            "appsecret": self._app_secret,
            "tr_id": tr_id,
            "custtype": "P",
        }

    async def _request_with_retry(
        self, method: str, path: str, tr_id: str, **kwargs,
    ) -> dict:
        url = f"{self._base_url}{path}"
        headers = self._common_headers(tr_id)
        for attempt in range(_MAX_ATTEMPTS):
            await self._throttle()
            try:
                response = await self._client.request(
                    method, url, headers=headers, **kwargs
                )
                if response.status_code == 429:
                    if attempt < _MAX_ATTEMPTS - 1:
                        await asyncio.sleep(2**attempt)
                        continue
                    response.raise_for_status()
                response.raise_for_status()
                return response.json()
            except httpx.TimeoutException:
                if attempt < _MAX_ATTEMPTS - 1:
                    await asyncio.sleep(2**attempt)
                    continue
                raise
        raise RuntimeError("retry exhausted")

    async def get_minute_ohlcv(self, ticker: str) -> list[dict]:
        """현재 시점 직전 30개 1분봉 OHLCV (TR_ID FHKST03010200)."""
        path = "/uapi/domestic-stock/v1/quotations/inquire-time-itemchartprice"
        params = {
            "FID_ETC_CLS_CODE": "",
            "FID_COND_MRKT_DIV_CODE": "J",
            "FID_INPUT_ISCD": ticker,
            "FID_INPUT_HOUR_1": datetime.now(KST).strftime("%H%M%S"),
            "FID_PW_DATA_INCU_YN": "N",
        }
        raw = await self._request_with_retry(
            "GET", path, tr_id="FHKST03010200", params=params,
        )
        output2 = raw.get("output2", [])
        bars = []
        for row in output2:
            try:
                date = row["stck_bsop_date"]
                hhmmss = row["stck_cntg_hour"]
                dt = datetime.strptime(f"{date} {hhmmss}", "%Y%m%d %H%M%S").replace(tzinfo=KST)
                bars.append({
                    "datetime": dt.isoformat(),
                    "open": int(row["stck_oprc"]),
                    "high": int(row["stck_hgpr"]),
                    "low": int(row["stck_lwpr"]),
                    "close": int(row["stck_prpr"]),
                    "volume": int(row["cntg_vol"]),
                })
            except (KeyError, ValueError) as e:
                logger.warning("skip malformed bar for %s: %r", ticker, e)
        # KIS returns descending; reverse to ascending (most recent last)
        bars.reverse()
        return bars

    async def get_asking_price(self, ticker: str) -> dict:
        """현재 호가 + 매수/매도 잔량 (TR_ID FHKST01010200)."""
        path = "/uapi/domestic-stock/v1/quotations/inquire-asking-price-exp-ccn"
        params = {
            "FID_COND_MRKT_DIV_CODE": "J",
            "FID_INPUT_ISCD": ticker,
        }
        raw = await self._request_with_retry(
            "GET", path, tr_id="FHKST01010200", params=params,
        )
        output1 = raw.get("output1", {})
        bid_total = int(output1.get("total_bidp_rsqn", 0))
        ask_total = int(output1.get("total_askp_rsqn", 0))
        total = bid_total + ask_total
        bid_ratio = bid_total / total if total > 0 else 0.0
        current_price = int(output1.get("stck_prpr", 0))
        return {
            "bid_total": bid_total,
            "ask_total": ask_total,
            "bid_ratio": bid_ratio,
            "current_price": current_price,
            "as_of": datetime.now(KST).isoformat(),
        }

    async def get_daily_ohlcv(self, ticker: str, days: int = 60) -> list[dict]:
        """KRX 일봉 OHLCV (TR_ID FHKST03010100).

        Returns: [{"datetime", "open", "high", "low", "close", "volume"}, ...]
                 시간 오름차순.
        """
        path = "/uapi/domestic-stock/v1/quotations/inquire-daily-itemchartprice"
        today = datetime.now(KST).strftime("%Y%m%d")
        start_date = (datetime.now(KST) - timedelta(days=days * 2)).strftime("%Y%m%d")
        params = {
            "FID_COND_MRKT_DIV_CODE": "J",
            "FID_INPUT_ISCD": ticker,
            "FID_INPUT_DATE_1": start_date,
            "FID_INPUT_DATE_2": today,
            "FID_PERIOD_DIV_CODE": "D",
            "FID_ORG_ADJ_PRC": "1",
        }
        raw = await self._request_with_retry(
            "GET", path, tr_id="FHKST03010100", params=params,
        )
        output2 = raw.get("output2", [])
        bars = []
        for row in output2:
            try:
                date = row["stck_bsop_date"]
                bars.append({
                    "datetime": f"{date[:4]}-{date[4:6]}-{date[6:]}",
                    "open": int(row["stck_oprc"]),
                    "high": int(row["stck_hgpr"]),
                    "low": int(row["stck_lwpr"]),
                    "close": int(row["stck_clpr"]),
                    "volume": int(row["acml_vol"]),
                })
            except (KeyError, ValueError):
                continue
        bars.reverse()
        return bars[-days:]
