"""태버지 노트북 공통 헬퍼 — yfinance rate limit 우회.

사용법 (각 노트북에서):
    import sys, os
    sys.path.insert(0, os.path.dirname(os.path.abspath("__file__")) if "__file__" in dir() else "/Users/loonshots/룬샤시_공시정리/태버지")
    from taebuji_common import yc_close_wide, yc_ohlcv_wide, tz_naive, SessionManager

    # Close 가격만 (wide DataFrame, 컬럼=ticker)
    close = yc_close_wide(tickers, range_str="1y")

    # OHLCV 전체 (MultiIndex columns, 'Close','Open','High','Low','Volume')
    ohlcv = yc_ohlcv_wide(tickers, range_str="1y")

    # 벤치마크 단일 종목 index timezone 정규화
    spy = yf.download("SPY", period="3y")["Close"]
    spy = tz_naive(spy)
"""
from __future__ import annotations

import logging
import os
import sys

import pandas as pd
import requests

# 태버지 폴더 경로 자동 추가
_HERE = os.path.dirname(os.path.abspath(__file__))
if _HERE not in sys.path:
    sys.path.insert(0, _HERE)

from yahoo_chart_client import fetch_ohlcv_batch
from yfinance_proxy import ip_rotating_session, is_available

logger = logging.getLogger(__name__)

_IP_ROTATION_AVAILABLE = is_available()


def _setup_korean_font() -> None:
    """matplotlib에 한글 폰트 자동 설정 (macOS AppleGothic)."""
    try:
        import matplotlib
        import matplotlib.pyplot as plt
        candidates = ["AppleGothic", "Apple SD Gothic Neo", "NanumGothic", "Malgun Gothic"]
        from matplotlib import font_manager
        available = {f.name for f in font_manager.fontManager.ttflist}
        for name in candidates:
            if name in available:
                matplotlib.rcParams["font.family"] = name
                matplotlib.rcParams["axes.unicode_minus"] = False
                return
    except Exception as e:
        logger.debug("korean font setup skipped: %s", e)


_setup_korean_font()

# === 전역 공유 세션 (노트북 실행 중 gateway 한 번만 시작) ===
_shared_session: requests.Session | None = None
_shared_rotation_cm = None


def get_shared_session(verbose: bool = True) -> requests.Session:
    """노트북 실행 중 모든 yc_* 호출이 공유할 session.

    첫 호출 시 AWS API Gateway를 한 번 열고, 그 후 모든 호출에서 재사용.
    노트북 실행 종료 시 close_shared_session()으로 정리.
    """
    global _shared_session, _shared_rotation_cm
    if _shared_session is not None:
        return _shared_session

    if _IP_ROTATION_AVAILABLE:
        _shared_rotation_cm = ip_rotating_session()
        _shared_session = _shared_rotation_cm.__enter__()
        if verbose:
            print("  → AWS API Gateway IP rotation 활성화 (shared)")
    else:
        _shared_session = requests.Session()
        _shared_session.headers.update(
            {
                "User-Agent": (
                    "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) "
                    "AppleWebKit/537.36 (KHTML, like Gecko) "
                    "Chrome/120.0.0.0 Safari/537.36"
                ),
            }
        )
        if verbose:
            print("  → 기본 requests.Session (IP rotation 비활성)")
    return _shared_session


def close_shared_session() -> None:
    """공유 session 정리 (AWS gateway shutdown)."""
    global _shared_session, _shared_rotation_cm
    if _shared_rotation_cm is not None:
        try:
            _shared_rotation_cm.__exit__(None, None, None)
        except Exception as e:
            logger.warning("close shared session failed: %s", e)
    _shared_session = None
    _shared_rotation_cm = None


class SessionManager:
    """IP rotation 세션 컨텍스트 매니저. 가능하면 rotation, 아니면 기본 session.

    예:
        with SessionManager() as session:
            data = fetch_ohlcv_batch(tickers, session=session, ...)
    """

    def __init__(self, *, verbose: bool = True) -> None:
        self._verbose = verbose
        self._session: requests.Session | None = None
        self._rotation_cm = None

    def __enter__(self) -> requests.Session:
        if _IP_ROTATION_AVAILABLE:
            self._rotation_cm = ip_rotating_session()
            self._session = self._rotation_cm.__enter__()
            if self._verbose:
                print("  → AWS API Gateway IP rotation 활성화")
        else:
            self._session = requests.Session()
            self._session.headers.update(
                {
                    "User-Agent": (
                        "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) "
                        "AppleWebKit/537.36 (KHTML, like Gecko) "
                        "Chrome/120.0.0.0 Safari/537.36"
                    ),
                }
            )
            if self._verbose:
                print("  → 기본 requests.Session (IP rotation 비활성)")
        return self._session

    def __exit__(self, *args) -> None:
        if self._rotation_cm is not None:
            self._rotation_cm.__exit__(*args)


def tz_naive(series_or_df):
    """Index를 tz-naive normalized datetime으로 맞춤 (yfinance 호환)."""
    idx = pd.to_datetime(series_or_df.index)
    if getattr(idx, "tz", None) is not None:
        idx = idx.tz_localize(None)
    series_or_df.index = idx.normalize()
    return series_or_df


def yc_ohlcv_wide(
    tickers: list[str],
    range_str: str = "1y",
    interval: str = "1d",
    session: requests.Session | None = None,
    adjusted: bool = False,
) -> pd.DataFrame:
    """batch OHLCV 조회. yfinance.download() 호환 MultiIndex DataFrame.

    Args:
        tickers: 티커 리스트
        adjusted: True면 adjclose (배당·분할 조정) 사용. yfinance auto_adjust=True 동일.
        range_str: "1y", "3y", "60d" 등
        interval: "1d", "1h" 등
        session: 재사용할 session (없으면 SessionManager 자동 사용)

    Returns:
        wide MultiIndex DataFrame (columns: OHLCV level × ticker level).
        columns['Close'], columns['Open'] 등으로 yfinance['Close']와 동일 호환.
    """
    if session is None:
        # 공유 세션 사용 (gateway 한 번만 생성)
        session = get_shared_session(verbose=True)
    df = fetch_ohlcv_batch(
        tickers,
        session=session,
        range_str=range_str,
        interval=interval,
        use_adjclose=adjusted,
    )

    if df.empty:
        return pd.DataFrame()

    # 원본 shape: MultiIndex (Ticker, OHLCV)
    # yfinance.download 호환: (OHLCV, Ticker)로 swap
    df = df.swaplevel(0, 1, axis=1).sort_index(axis=1)
    # tz-naive normalize
    df.index = pd.to_datetime(df.index).tz_localize(None).normalize()
    return df


def yc_close_wide(
    tickers: list[str],
    range_str: str = "1y",
    interval: str = "1d",
    session: requests.Session | None = None,
    min_coverage: float = 0.8,
    ffill: bool = True,
    adjusted: bool = False,
) -> pd.DataFrame:
    """Close 가격만 wide DataFrame 반환 (컬럼=ticker).

    yfinance.download(tickers, ...)['Close']와 동일한 형식.

    Args:
        min_coverage: 각 종목 컬럼이 최소 이 비율 이상의 거래일에 데이터를 가져야 유지.
            기본 0.8 = 80%. 1.0 = 모든 거래일 데이터 있는 종목만 (yfinance dropna 동작).
        ffill: True면 남은 NaN을 앞 값으로 채움. False면 NaN 그대로 (원본 yfinance 동작).
        adjusted: True면 adjclose (배당·분할 조정) 사용. yfinance auto_adjust=True 동일.
            기본 False = unadjusted raw close (yfinance auto_adjust=False 동일).
    """
    df = yc_ohlcv_wide(
        tickers,
        range_str=range_str,
        interval=interval,
        session=session,
        adjusted=adjusted,
    )
    close = df["Close"] if not df.empty else pd.DataFrame()

    # Yahoo가 못 받은 종목을 토스증권 Open API 로 보충 (전체 실패 + 부분 실패 모두).
    got = set(close.columns) if not close.empty else set()
    missing = [t for t in tickers if t not in got]
    if missing:
        toss_df = _toss_close_fallback(missing, range_str, interval)
        if toss_df is not None and not toss_df.empty:
            close = pd.concat([close, toss_df], axis=1) if not close.empty else toss_df
            close = close.sort_index()

    if close.empty:
        return pd.DataFrame()
    # 커버리지 필터
    if min_coverage > 0:
        coverage = close.notna().mean(axis=0)
        keep = coverage[coverage >= min_coverage].index
        close = close[keep]
        if ffill:
            close = close.ffill().dropna(how="all")
    return close


def _range_to_count(range_str: str) -> int:
    """'1y'/'3y'/'60d' → 대략 거래일 수 (토스 candles count)."""
    s = str(range_str).strip().lower()
    try:
        if s.endswith("y"):
            return min(int(float(s[:-1]) * 252) + 10, 2000)
        if s.endswith("mo"):
            return min(int(float(s[:-2]) * 21) + 5, 2000)
        if s.endswith("d"):
            return min(int(s[:-1]) + 5, 2000)
    except ValueError:
        pass
    return 252


def _toss_close_fallback(tickers: list[str], range_str: str, interval: str) -> pd.DataFrame:
    """토스증권 Open API로 종가 wide DataFrame 폴백. 실패 시 빈 DF."""
    try:
        from toss_client import get_client
        cnt = _range_to_count(range_str)
        df = get_client().close_wide(list(tickers), count=cnt, interval=interval)
        if df is not None and not df.empty:
            print(f"  ↪︎ Yahoo 실패 → 토스증권 폴백: {df.shape[1]}종목 / {df.shape[0]}일")
        return df if df is not None else pd.DataFrame()
    except Exception as e:
        print(f"  ⚠️ 토스 폴백 실패: {type(e).__name__}: {e}")
        return pd.DataFrame()
