#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
키움 ka10001 로 유통주식수(dstr_stk)를 수집해 ``stock_share_meta`` 테이블에 저장.

백테스트 회전율 필터(BREAKOUT_MIN_TURNOVER_1M_PCT 등)는 유통주식수가 필요하다.
라이브 봇(WSManager)도 동일 테이블을 쓰므로, 백테 전에 한 번 채워두면 웹/파서치와 정합된다.

대상 종목 (기본)
----------------
* ``ws_candles`` 에 기간 내 데이터가 있는 DISTINCT code
* ``--codes`` 로 수동 지정 가능

사용 예
--------
    python3 kis_trader/scripts/fill_stock_share_meta.py
    python3 kis_trader/scripts/fill_stock_share_meta.py --days 14
    python3 kis_trader/scripts/fill_stock_share_meta.py --codes 005930,035810
    python3 kis_trader/scripts/fill_stock_share_meta.py --dry-run --codes 035810

주의
----
키움 ka10001 은 초당 호출 한도가 있어(오류 1700 / HTTP 429) 기본 호출 간격 1초.
로그의 ``ka10001 실패`` 대부분은 레이트리밋 — ``--sleep`` 을 늘리거나 재실행하면 된다.
"""
from __future__ import annotations

import argparse
import logging
import random
import sys
import time
from collections import Counter
from datetime import datetime, timedelta
from pathlib import Path
from typing import Dict, List, Optional, Sequence

HERE = Path(__file__).resolve()
ROOT = HERE.parents[2]
if str(ROOT) not in sys.path:
    sys.path.insert(0, str(ROOT))

from database import TradeDB  # noqa: E402
from kis_trader.share.stock_share import (  # noqa: E402
    apply_fetched_meta,
    codes_missing_dstr,
    load_stock_share_meta_map,
)
from kis_trader.utils.env import get_env_float  # noqa: E402
from kis_trader.ws.kis_ws import (  # noqa: E402
    _get_kiwoom_creds,
    fetch_kiwoom_stock_meta_detail,
)

logger = logging.getLogger("fill_stock_share_meta")


def _parse_codes(raw: str) -> List[str]:
    return [c.strip() for c in str(raw or "").split(",") if c.strip()]


def _codes_from_ws_candles(db: TradeDB, days: int) -> List[str]:
    end = datetime.now()
    start = end - timedelta(days=max(1, days))
    start_key = start.strftime("%Y%m%d") + "0000"
    end_key = end.strftime("%Y%m%d") + "2359"
    rows = db.conn.execute(
        "SELECT DISTINCT code FROM ws_candles WHERE timeframe=1 "
        "AND candle_time >= %s AND candle_time <= %s ORDER BY code",
        [start_key, end_key],
    ).fetchall()
    return [str(r["code"]).strip() for r in rows if r.get("code")]


def _format_fail_detail(res: Dict) -> str:
    http_st = res.get("http_status")
    rc = res.get("return_code")
    msg = str(res.get("return_msg") or "").strip()
    reason = str(res.get("reason") or "").strip()
    parts = [f"reason={reason}"]
    if http_st:
        parts.append(f"http={http_st}")
    if rc is not None:
        parts.append(f"rc={rc}")
    if msg:
        parts.append(f"msg={msg}")
    return " | ".join(parts)


def fill_share_meta(
    db: TradeDB,
    codes: Sequence[str],
    *,
    dry_run: bool = False,
    sleep_sec: Optional[float] = None,
) -> int:
    kw_key, kw_secret, kw_mock = _get_kiwoom_creds(db)
    if not kw_key or not kw_secret:
        logger.error("키움 API 키 없음 — env_config KIWOOM_APP_KEY_* 확인")
        return 0

    if sleep_sec is None:
        sleep_sec = get_env_float("STOCK_SHARE_FILL_SLEEP_SEC", 1.0)

    cache = load_stock_share_meta_map(db, codes)
    missing = codes_missing_dstr(cache, codes)
    if not missing:
        logger.info("모든 종목 stock_share_meta 적재 완료 (%d종)", len(codes))
        return 0

    logger.info(
        "ka10001 대상: %d/%d종목 (호출간격 %.2fs, 레이트리밋 시 자동 재시도)",
        len(missing), len(codes), sleep_sec,
    )
    ok = 0
    fail_reasons: Counter = Counter()
    fail_samples: Dict[str, List[str]] = {}

    for i, code in enumerate(missing, 1):
        if dry_run:
            logger.info("[%d/%d] dry-run %s", i, len(missing), code)
            continue

        res = fetch_kiwoom_stock_meta_detail(
            code, kw_key, kw_secret, is_mock=kw_mock,
        )
        meta = res.get("meta")
        if res.get("ok") and meta:
            apply_fetched_meta(cache, db, code, meta)
            ok += 1
            logger.info(
                "[%d/%d] %s flo=%s dstr=%s",
                i, len(missing), code, meta.get("flo_stk"), meta.get("dstr_stk"),
            )
        else:
            reason = str(res.get("reason") or "unknown")
            fail_reasons[reason] += 1
            fail_samples.setdefault(reason, [])
            if len(fail_samples[reason]) < 3:
                fail_samples[reason].append(code)
            logger.warning(
                "[%d/%d] %s ka10001 실패 — %s",
                i, len(missing), code, _format_fail_detail(res),
            )
        time.sleep(max(0.1, sleep_sec) + random.uniform(0.05, 0.2))

    if fail_reasons:
        logger.warning("── 실패 요약 ──")
        for reason, cnt in fail_reasons.most_common():
            samples = ", ".join(fail_samples.get(reason, [])[:3])
            logger.warning("  %s: %d건 (예: %s)", reason, cnt, samples or "-")
        if fail_reasons.get("rate_limit_1700", 0) > 0:
            logger.warning(
                "  💡 대부분 키움 API 호출한도(1700)입니다. "
                "--sleep 1.5 이상으로 재실행하거나 잠시 후 이어서 실행하세요.",
            )
    return ok


def main(argv: Optional[List[str]] = None) -> int:
    logging.basicConfig(level=logging.INFO, format="%(message)s")
    p = argparse.ArgumentParser(description="stock_share_meta 일괄 적재 (ka10001)")
    p.add_argument("--days", type=int, default=7, help="ws_candles 조회 일수 (기본 7)")
    p.add_argument("--codes", type=str, default="", help="종목코드 콤마 구분")
    p.add_argument("--dry-run", action="store_true", help="API 호출 없이 대상만 출력")
    p.add_argument(
        "--sleep", type=float, default=None,
        help="종목 간 대기초 (기본 env STOCK_SHARE_FILL_SLEEP_SEC=1.0)",
    )
    args = p.parse_args(argv)

    db = TradeDB()
    try:
        if args.codes:
            codes = _parse_codes(args.codes)
        else:
            codes = _codes_from_ws_candles(db, args.days)
        if not codes:
            logger.warning("대상 종목 없음")
            return 1
        n = fill_share_meta(db, codes, dry_run=args.dry_run, sleep_sec=args.sleep)
        if not args.dry_run:
            logger.info("완료: %d종목 저장 (대상 %d종 중)", n, len(codes))
    finally:
        db.close()
    return 0


if __name__ == "__main__":
    raise SystemExit(main())
