"""
kis_trader/utils/request_handler.py — SafeRequest 공통 HTTP 래퍼
================================================================
.cursorrules 규정:
  "모든 API 요청은 utils/request_handler.py 의 SafeRequest 클래스를
   상속받아 구현하세요. HTTP 429(Too Many Requests) 에러 발생 시
   재시도(Retry) 로직을 반드시 포함하세요."

- 한투 EGW00201 (초당 거래 초과)도 동일 취급.
- 최소 호출 간격 (`min_interval_sec`) 으로 클라이언트 측 스로틀.
- 재시도 시 지수 백오프 (1.0s → 2.0s → 4.0s ... cap 10s)
- 타임아웃 발생 시 1회 재시도 허용.
"""
from __future__ import annotations

import random
import threading
import time
from typing import Any, Optional

import requests

from .logger import get_logger

logger = get_logger("kis_trader.safe_request")


class SafeRequest:
    """
    HTTP 요청 공통 기반 클래스.  한투·키움 REST 클라이언트가 상속해서 사용.
    """

    # 429 상태 코드 or 한투 초당거래 초과 msg_cd
    RETRYABLE_STATUSES = {429, 500, 502, 503, 504}
    KIS_RATE_LIMIT_MSG_CD = {"EGW00201", "EGW00202"}

    def __init__(
        self,
        min_interval_sec: float = 0.22,
        max_retries: int = 5,
        backoff_base: float = 1.0,
        backoff_cap: float = 10.0,
        timeout_sec: float = 10.0,
    ):
        # 기본값은 안전 마진(한투 REST 공식 초당 5건 내외 감안)
        self.min_interval_sec = float(min_interval_sec)
        self.max_retries = int(max_retries)
        self.backoff_base = float(backoff_base)
        self.backoff_cap = float(backoff_cap)
        self.timeout_sec = float(timeout_sec)
        self._last_call_ts: float = 0.0
        self._throttle_lock = threading.Lock()

    # ------------------------------------------------------------------
    # 내부 유틸
    # ------------------------------------------------------------------
    def _throttle(self) -> None:
        """호출 간 최소 간격 보장 (클라이언트 측 429 방지)."""
        with self._throttle_lock:
            elapsed = time.time() - self._last_call_ts
            if elapsed < self.min_interval_sec:
                time.sleep(self.min_interval_sec - elapsed)
            self._last_call_ts = time.time()

    def _sleep_backoff(self, attempt: int) -> None:
        """지수 백오프 + 지터. attempt=1,2,3..."""
        wait = min(self.backoff_base * (2 ** (attempt - 1)), self.backoff_cap)
        wait += random.uniform(0, 0.3)
        time.sleep(wait)

    @staticmethod
    def _is_kis_rate_limited(json_body: dict) -> bool:
        if not isinstance(json_body, dict):
            return False
        return json_body.get("msg_cd") in SafeRequest.KIS_RATE_LIMIT_MSG_CD

    # ------------------------------------------------------------------
    # 외부 인터페이스
    # ------------------------------------------------------------------
    def request(
        self,
        method: str,
        url: str,
        *,
        headers: Optional[dict] = None,
        params: Optional[dict] = None,
        json_body: Optional[dict] = None,
        data: Optional[Any] = None,
        timeout: Optional[float] = None,
    ) -> requests.Response:
        """
        재시도/스로틀이 포함된 HTTP 호출.
        한투 EGW00201 같은 `status=200 but rate-limited` 도 재시도.
        실패 시 마지막 Response(또는 빈 Response) 반환.
        """
        timeout = timeout or self.timeout_sec
        last_resp: Optional[requests.Response] = None
        for attempt in range(1, self.max_retries + 1):
            self._throttle()
            try:
                resp = requests.request(
                    method.upper(),
                    url,
                    headers=headers,
                    params=params,
                    json=json_body,
                    data=data,
                    timeout=timeout,
                )
                last_resp = resp

                # [1] 재시도 대상 HTTP 상태
                if resp.status_code in self.RETRYABLE_STATUSES:
                    logger.warning(
                        "HTTP %d on %s %s (%d/%d) → 백오프 후 재시도",
                        resp.status_code, method.upper(), url, attempt, self.max_retries,
                    )
                    self._sleep_backoff(attempt)
                    continue

                # [2] 한투 EGW00201 (HTTP 200 but 초당거래 초과)
                if resp.status_code == 200:
                    try:
                        body = resp.json()
                    except Exception:
                        body = None
                    if body and self._is_kis_rate_limited(body):
                        logger.warning(
                            "KIS rate-limit %s on %s (%d/%d) → 백오프 후 재시도",
                            body.get("msg_cd"), url, attempt, self.max_retries,
                        )
                        self._sleep_backoff(attempt)
                        continue

                return resp
            except requests.exceptions.Timeout:
                logger.warning(
                    "⏰ Timeout %s %s (%d/%d)",
                    method.upper(), url, attempt, self.max_retries,
                )
                time.sleep(1.0)
            except requests.exceptions.RequestException as e:
                logger.warning(
                    "⚠️ RequestException %s %s (%d/%d): %s",
                    method.upper(), url, attempt, self.max_retries, e,
                )
                self._sleep_backoff(attempt)

        return last_resp if last_resp is not None else requests.Response()

    def get(self, url: str, **kw) -> requests.Response:
        return self.request("GET", url, **kw)

    def post(self, url: str, **kw) -> requests.Response:
        return self.request("POST", url, **kw)
