#!/usr/bin/env python3
"""
kis_trader/utils/kr_trading_day.py — 한국 거래일(주말·휴장) 보정 공통
====================================================================
백테 웹 / CLI / API 날짜 기본값: 주말·휴장이면 **이전 장운영일**로 맞춤.

휴장일 목록: env ``KR_MARKET_HOLIDAYS`` (콤마구분 YYYY-MM-DD).
DB에 없으면 기본 CSV를 env_config_ext 에 1회 시드.
"""
from __future__ import annotations

from datetime import date, datetime, timedelta
from typing import List, Optional, Set, Tuple, Union

from kis_trader.utils.env import get_env_from_db, get_env_int

DateLike = Union[str, date, datetime]

# 한국거래소 휴장(주말 제외). DB/ env 로 갱신. get_env 기본값용.
_DEFAULT_KR_MARKET_HOLIDAYS = (
    # 2025
    "2025-01-01,2025-01-28,2025-01-29,2025-01-30,"
    "2025-03-01,2025-05-05,2025-05-06,2025-06-06,"
    "2025-08-15,2025-10-03,2025-10-06,2025-10-07,2025-10-08,2025-10-09,2025-12-25,"
    # 2026
    "2026-01-01,2026-02-16,2026-02-17,2026-02-18,"
    "2026-03-01,2026-05-05,2026-05-24,2026-06-06,"
    "2026-08-15,2026-09-24,2026-09-25,2026-09-26,"
    "2026-10-03,2026-10-09,2026-12-25,"
    # 2027 (주요)
    "2027-01-01,2027-02-08,2027-02-09,2027-02-10,"
    "2027-03-01,2027-05-05,2027-05-13,2027-06-06,"
    "2027-08-15,2027-09-14,2027-09-15,2027-09-16,"
    "2027-10-03,2027-10-09,2027-12-25"
)

_holidays_cache: Optional[Set[date]] = None
_holidays_cache_raw: Optional[str] = None
_seeded: bool = False


def _parse_ymd(raw: DateLike) -> date:
    if isinstance(raw, datetime):
        return raw.date()
    if isinstance(raw, date):
        return raw
    s = str(raw or "").strip()
    if not s:
        raise ValueError("empty date")
    if "-" in s and len(s) >= 10:
        return datetime.strptime(s[:10], "%Y-%m-%d").date()
    digits = "".join(c for c in s if c.isdigit())
    if len(digits) >= 8:
        return datetime.strptime(digits[:8], "%Y%m%d").date()
    raise ValueError(f"bad date: {raw!r}")


def ymd(d: date) -> str:
    return d.strftime("%Y-%m-%d")


def parse_holiday_csv(raw: str) -> Set[date]:
    out: Set[date] = set()
    for part in str(raw or "").replace(";", ",").split(","):
        p = part.strip()
        if not p or p.startswith("#"):
            continue
        try:
            out.add(_parse_ymd(p))
        except ValueError:
            continue
    return out


def ensure_kr_market_holidays_in_db() -> None:
    """KR_MARKET_HOLIDAYS / lookback 기본값을 DB/ext 에 없으면 1회 시드 (기존 값 유지)."""
    global _seeded
    if _seeded:
        return
    _seeded = True
    try:
        from kis_trader.utils.env import _get_db  # noqa: PLC2701

        db = _get_db()
        if not db:
            return
        now = datetime.now().strftime("%Y-%m-%d %H:%M:%S")

        def _seed_if_missing(key: str, value: str) -> None:
            cur = str(get_env_from_db(key, "") or "").strip()
            if cur:
                return
            row = db.conn.execute(
                "SELECT env_value FROM env_config_ext WHERE env_key=%s LIMIT 1",
                (key,),
            ).fetchone()
            if row and str((row["env_value"] if isinstance(row, dict) else row[0]) or "").strip():
                return
            db.conn.execute(
                "INSERT INTO env_config_ext (env_key, env_value, updated_at) "
                "VALUES (%s, %s, %s) "
                "ON DUPLICATE KEY UPDATE env_key=env_key",
                (key, value, now),
            )

        _seed_if_missing("KR_MARKET_HOLIDAYS", _DEFAULT_KR_MARKET_HOLIDAYS)
        _seed_if_missing("PARAM_SEARCH_DEFAULT_LOOKBACK_DAYS", "7")
        _seed_if_missing("KR_TRADING_DAY_MAX_BACK_DAYS", "14")
        try:
            from kis_trader.utils.env import invalidate_merged_env_cache

            invalidate_merged_env_cache()
        except Exception:
            pass
    except Exception:
        pass


def get_kr_market_holiday_set(*, refresh: bool = False) -> Set[date]:
    """휴장일 set. env/DB → 없으면 코드 기본 CSV."""
    global _holidays_cache, _holidays_cache_raw
    ensure_kr_market_holidays_in_db()
    raw = str(
        get_env_from_db("KR_MARKET_HOLIDAYS", _DEFAULT_KR_MARKET_HOLIDAYS) or ""
    ).strip()
    if not raw:
        raw = _DEFAULT_KR_MARKET_HOLIDAYS
    if (
        not refresh
        and _holidays_cache is not None
        and _holidays_cache_raw == raw
    ):
        return _holidays_cache
    _holidays_cache = parse_holiday_csv(raw)
    _holidays_cache_raw = raw
    return _holidays_cache


def is_kr_trading_day(d: DateLike, *, holidays: Optional[Set[date]] = None) -> bool:
    """월~금 이고 휴장 목록에 없으면 True."""
    day = _parse_ymd(d)
    if day.weekday() >= 5:
        return False
    hol = holidays if holidays is not None else get_kr_market_holiday_set()
    return day not in hol


def clamp_to_prev_kr_trading_day(
    d: DateLike,
    *,
    holidays: Optional[Set[date]] = None,
    max_back_days: Optional[int] = None,
) -> str:
    """
    거래일이 아니면 하루씩 과거로 이동해 **가장 가까운 이전(또는 당일) 장운영일** 반환.
    """
    day = _parse_ymd(d)
    hol = holidays if holidays is not None else get_kr_market_holiday_set()
    limit = int(
        max_back_days
        if max_back_days is not None
        else get_env_int("KR_TRADING_DAY_MAX_BACK_DAYS", 14)
    )
    limit = max(1, limit)
    for _ in range(limit + 1):
        if is_kr_trading_day(day, holidays=hol):
            return ymd(day)
        day -= timedelta(days=1)
    return ymd(day)


def default_kr_trading_end(*, as_of: Optional[DateLike] = None) -> str:
    """종료일 기본값 = as_of(기본 오늘) 을 이전 거래일로 보정."""
    base = _parse_ymd(as_of) if as_of is not None else date.today()
    return clamp_to_prev_kr_trading_day(base)


def default_kr_trading_start(
    lookback_days: int = 7,
    *,
    as_of: Optional[DateLike] = None,
    end: Optional[DateLike] = None,
) -> str:
    """시작일 기본값 = (종료일 − lookback) 을 이전 거래일로 보정."""
    end_d = _parse_ymd(end) if end is not None else _parse_ymd(
        default_kr_trading_end(as_of=as_of)
    )
    lb = max(1, int(lookback_days))
    start_raw = end_d - timedelta(days=lb)
    return clamp_to_prev_kr_trading_day(start_raw)


def resolve_kr_backtest_date_defaults(
    lookback_days: int = 7,
    *,
    as_of: Optional[DateLike] = None,
) -> Tuple[str, str]:
    """(start, end) 기본 구간. end·start 모두 거래일."""
    end = default_kr_trading_end(as_of=as_of)
    start = default_kr_trading_start(lookback_days, end=end)
    if start > end:
        start = end
    return start, end


def clamp_to_prev_weekday(d: DateLike, *, max_back_days: int = 14) -> str:
    """주말만 보정 (해외장 등 공휴일 목록 없이 토·일만 스킵)."""
    day = _parse_ymd(d)
    for _ in range(max(1, int(max_back_days)) + 1):
        if day.weekday() < 5:
            return ymd(day)
        day -= timedelta(days=1)
    return ymd(day)


def holiday_list_ymd() -> List[str]:
    return sorted(ymd(d) for d in get_kr_market_holiday_set())


def trading_dates_payload(lookback_days: int = 7) -> dict:
    """웹/API용 날짜 기본값 묶음."""
    start, end = resolve_kr_backtest_date_defaults(lookback_days)
    today = ymd(date.today())
    return {
        "today": today,
        "end": end,
        "start": start,
        "trading_day": end,  # 단일일 조회용 (= 보정된 종료일)
        "holidays": holiday_list_ymd(),
        "lookback_days": int(lookback_days),
        "is_today_trading_day": is_kr_trading_day(today),
    }
