"""
kis_trader/engine/tail_env_keys.py — 꼬리잡기(TAIL) env 키 단일 정의
====================================================================
레거시(MIN_DROP_RATE, STOP_LOSS_PCT 등) 제거 — TAIL_* 접두사만 사용.
config_short 테이블 저장 · tail_engine · 웹 · 파라서치 공통.
"""
from __future__ import annotations

from typing import Any, Dict, Optional

# 레거시 → TAIL (1회 마이그레이션용, 코드 런타임 폴백 없음)
LEGACY_TO_TAIL: Dict[str, str] = {
    "MIN_DROP_RATE": "TAIL_MIN_DROP_RATE",
    "MIN_RECOVERY_RATIO_SHORT": "TAIL_MIN_RECOVERY_RATIO",
    "MAX_RECOVERY_RATIO_3M": "TAIL_MAX_RECOVERY_3M",
    "STOP_LOSS_PCT": "TAIL_STOP_LOSS_PCT",
    "TAKE_PROFIT_PCT": "TAIL_TAKE_PROFIT_PCT",
    "SHOULDER_MIN_HIGH_PCT": "TAIL_SHOULDER_MIN_HIGH_PCT",
    "SHOULDER_CUT_PCT": "TAIL_SHOULDER_CUT_PCT",
    "STOP_ATR_MULTIPLIER_TAIL": "TAIL_STOP_ATR_MULT",
    "TARGET_ATR_MULTIPLIER_TAIL": "TAIL_TARGET_ATR_MULT",
    "MIN_PRICE_TAIL": "TAIL_MIN_PRICE",
    "MAX_DAILY_CHANGE_PCT": "TAIL_MAX_DAILY_CHG",
    "MA20_MAX_ABOVE_PCT": "TAIL_MA20_MAX_ABOVE_PCT",
    "MAX_LOSS_PER_TRADE_KRW": "TAIL_MAX_LOSS_KRW",
    "MIN_DROP_PCT_FOR_LOSS_CUT": "TAIL_MIN_DROP_FOR_LOSS_CUT",
    "RSI_OVERHEAT_THRESHOLD": "TAIL_RSI_THRESHOLD",
    "RSI_PERIOD": "TAIL_RSI_PERIOD",
    "REENTRY_COOLDOWN_SEC": "TAIL_COOLDOWN_SEC",
    "MAX_DAILY_TAIL": "TAIL_MAX_DAILY",
    "HIGH_PRICE_CHASE_THRESHOLD": "TAIL_HIGH_CHASE_THR",
    "SHORT_TIME_START": "TAIL_TIME_START",
    "SHORT_TIME_END": "TAIL_TIME_END",
    # TIME_START/TIME_END 는 돌파·공통과 섞일 수 있어 마이그레이션 제외
    "SHORT_MAX_STOCKS": "TAIL_MAX_STOCKS",
    "SHORT_TOTAL_BUDGET_KRW": "TAIL_TOTAL_BUDGET_KRW",
    "SHORT_MAX_BUY_AMOUNT": "TAIL_MAX_BUY_AMOUNT",
    "TAIL_MAX_BUY_AMOUNT": "TAIL_MAX_BUY_AMOUNT",
    "SHORT_ENTRY_MODE": "TAIL_ENTRY_MODE",
    "SLOT_MONEY_DEFAULT": "TAIL_SLOT_MONEY",
    "MAX_BUY_AMOUNT_PER_STOCK": "TAIL_SLOT_MONEY",
}

# config_short 전용 TAIL 키 (오케스트레이터 SHORT_* 제외)
TAIL_CONFIG_KEYS = frozenset({
    "TAIL_MIN_DROP_RATE",
    "TAIL_MIN_RECOVERY_RATIO",
    "TAIL_MAX_RECOVERY_3M",
    "TAIL_RATIO_MIN",
    "TAIL_PCT_MIN",
    "TAIL_STOP_LOSS_PCT",
    "TAIL_TAKE_PROFIT_PCT",
    "TAIL_SHOULDER_MIN_HIGH_PCT",
    "TAIL_SHOULDER_CUT_PCT",
    "TAIL_STOP_ATR_MULT",
    "TAIL_TARGET_ATR_MULT",
    "TAIL_ATR_SL_MIN_PCT",
    "TAIL_ATR_SL_MAX_PCT",
    "TAIL_ATR_TP_MIN_PCT",
    "TAIL_ATR_TP_MAX_PCT",
    "TAIL_MIN_PRICE",
    "TAIL_MAX_DAILY_CHG",
    "TAIL_MA20_MAX_ABOVE_PCT",
    "TAIL_MAX_LOSS_KRW",
    "TAIL_MIN_DROP_FOR_LOSS_CUT",
    "TAIL_RSI_THRESHOLD",
    "TAIL_RSI_PERIOD",
    "TAIL_COOLDOWN_SEC",
    "TAIL_MAX_DAILY",
    "TAIL_HIGH_CHASE_THR",
    "TAIL_TIME_START",
    "TAIL_TIME_END",
    "TAIL_MAX_STOCKS",
    "TAIL_TOTAL_BUDGET_KRW",
    "TAIL_MAX_BUY_AMOUNT",
    "TAIL_SLOT_MONEY",
    "TAIL_ENTRY_MODE",
    "TAIL_LIMIT_ATR_MULT",
    "TAIL_LIMIT_ANCHOR",
    "TAIL_LIMIT_VALID_BARS",
    "TAIL_LIMIT_FILL_SLIP_PCT",
    "TAIL_SKIP_HTS_SCAN_DUPES",
    "TAIL_USE_INTRADAY_DROP",
    "TAIL_USE_MA20_FILTER",
    "TAIL_USE_RSI_FILTER",
    "TAIL_USE_DAILY_RANGE_FILTER",
    "TAIL_USE_HIGH_CHASE_FILTER",
    "TAIL_BAR_CHG_MIN_PCT",
    "TAIL_BAR_CHG_MAX_PCT",
    "TAIL_VOL_MULT",
    "TAIL_VOL_WIN",
    "TAIL_MIN_INVEST_RATIO_OF_SLOT",
    "TAIL_CANDLE_LOOKBACK",
    "TAIL_PATTERN_HAMMER",
    "TAIL_PATTERN_PIN",
    "TAIL_PATTERN_ENGULFING",
    "TAIL_PATTERN_PIERCING",
    "TAIL_PATTERN_HARAMI",
    "TAIL_PATTERN_DOJI",
    "TAIL_PATTERN_MORNING_STAR",
    "TAIL_PIN_CLOSE_UPPER_RATIO",
    "TAIL_PIN_MAX_UPPER_TAIL_RATIO",
    "TAIL_ENGULF_MIN_BODY_RATIO",
    "TAIL_PIERCING_PENETRATE_RATIO",
    "TAIL_HARAMI_MAX_BODY_RATIO",
    "TAIL_DOJI_BODY_MAX_RATIO",
    "TAIL_MORNING_STAR_BODY_MAX_RATIO",
})


def _row_val(row: Dict[str, Any], key: str, default: Any = None) -> Any:
    v = row.get(key)
    if v not in (None, "", "None"):
        return v
    return default


def tail_env_float(row: Dict[str, Any], key: str, default: float) -> float:
    v = _row_val(row, key)
    if v is None:
        return float(default)
    try:
        return float(v)
    except (TypeError, ValueError):
        return float(default)


def tail_env_int(row: Dict[str, Any], key: str, default: int) -> int:
    v = _row_val(row, key)
    if v is None:
        return int(default)
    try:
        return int(float(v))
    except (TypeError, ValueError):
        return int(default)


def tail_market_time_hm() -> tuple:
    """
    꼬리잡기(SHORT) 실매 장시간 HHMM — ``TAIL_TIME_START`` / ``TAIL_TIME_END`` 단일 소스.

    글로벌 ``TIME_START`` / ``TIME_END``(스캘핑 레거시) 는 **폴백하지 않음**.
    백테·tail_engine 기본값과 동일 (930~1500, DB에 값 있으면 DB 우선).
    """
    from kis_trader.utils.env import get_env_int

    start = get_env_int("TAIL_TIME_START", 930)
    end = get_env_int("TAIL_TIME_END", 1500)
    return int(start), int(end)


def tail_env_bool(row: Dict[str, Any], key: str, default: bool) -> bool:
    v = _row_val(row, key)
    if v is None:
        return default
    s = str(v).strip().lower()
    if s in ("1", "true", "t", "y", "yes", "on"):
        return True
    if s in ("0", "false", "f", "n", "no", "off", ""):
        return False
    return default


def params_to_tail_env_patch(p: Dict[str, Any]) -> Dict[str, str]:
    """파라서치·웹 JSON params → TAIL env 패치 (문자열)."""
    patch: Dict[str, str] = {}
    if not p:
        return patch

    def _set(k: str, v: Any) -> None:
        if v is not None and v != "":
            patch[k] = str(v)

    if "min_drop_rate" in p:
        _set("TAIL_MIN_DROP_RATE", p["min_drop_rate"])
    if "min_recovery_ratio" in p:
        _set("TAIL_MIN_RECOVERY_RATIO", p["min_recovery_ratio"])
    if "max_rec_3m" in p:
        _set("TAIL_MAX_RECOVERY_3M", p["max_rec_3m"])
    if "tail_ratio_min" in p:
        _set("TAIL_RATIO_MIN", p["tail_ratio_min"])
    if "tail_pct_min" in p:
        _set("TAIL_PCT_MIN", p["tail_pct_min"])
    if "sl_pct" in p:
        _set("TAIL_STOP_LOSS_PCT", -abs(float(p["sl_pct"])))
    if "tp_pct" in p:
        _set("TAIL_TAKE_PROFIT_PCT", p["tp_pct"])
    if "shoulder_cut_pct" in p:
        _set("TAIL_SHOULDER_CUT_PCT", p["shoulder_cut_pct"])
    if "shoulder_min_high" in p:
        _set("TAIL_SHOULDER_MIN_HIGH_PCT", p["shoulder_min_high"])
    if "trail_pct" in p:
        _set("TAIL_TRAIL_PCT", abs(float(p["trail_pct"])))
    if "trail_arm_pct" in p:
        _set("TAIL_TRAIL_ARM_PCT", abs(float(p["trail_arm_pct"])))
    if "ratchet_tiers" in p:
        patch["TAIL_RATCHET_TIERS"] = str(p.get("ratchet_tiers") or "").strip()
    if "max_hold_bars" in p and p["max_hold_bars"] is not None:
        _set("TAIL_MAX_HOLD_BARS", int(float(p["max_hold_bars"])))
    for js_k, env_k in (
        ("backtest_use_tick_db", "TAIL_BACKTEST_USE_TICK_DB"),
        ("backtest_tick_fallback_ohlc", "TAIL_BACKTEST_TICK_FALLBACK_OHLC"),
    ):
        if js_k in p:
            _set(env_k, str(p[js_k]).lower())
    if "rsi_threshold" in p:
        _set("TAIL_RSI_THRESHOLD", p["rsi_threshold"])
    if "rsi_period" in p:
        _set("TAIL_RSI_PERIOD", int(float(p["rsi_period"])))
    if "ma20_max_above" in p:
        _set("TAIL_MA20_MAX_ABOVE_PCT", p["ma20_max_above"])
    if "max_daily_change" in p:
        _set("TAIL_MAX_DAILY_CHG", p["max_daily_change"])
    if "stop_atr_mult" in p:
        _set("TAIL_STOP_ATR_MULT", p["stop_atr_mult"])
    if "target_atr_mult" in p:
        _set("TAIL_TARGET_ATR_MULT", p["target_atr_mult"])
    for js_k, env_k in (
        ("atr_sl_min_pct", "TAIL_ATR_SL_MIN_PCT"),
        ("atr_sl_max_pct", "TAIL_ATR_SL_MAX_PCT"),
        ("atr_tp_min_pct", "TAIL_ATR_TP_MIN_PCT"),
        ("atr_tp_max_pct", "TAIL_ATR_TP_MAX_PCT"),
    ):
        if js_k in p:
            _set(env_k, p[js_k])
    if "max_loss_krw" in p:
        _set("TAIL_MAX_LOSS_KRW", int(float(p["max_loss_krw"])))
    if "min_drop_pct_for_loss_cut" in p:
        v = float(p["min_drop_pct_for_loss_cut"])
        _set("TAIL_MIN_DROP_FOR_LOSS_CUT", round(v * 100, 2) if v < 1 else round(v, 2))
    if "high_chase_thr" in p and p["high_chase_thr"] is not None:
        _set("TAIL_HIGH_CHASE_THR", float(p["high_chase_thr"]))
    if "cooldown_min" in p and p["cooldown_min"] is not None:
        _set("TAIL_COOLDOWN_SEC", int(float(p["cooldown_min"]) * 60))
    if "time_start_hm" in p and p["time_start_hm"] is not None:
        _set("TAIL_TIME_START", int(float(p["time_start_hm"])))
    if "time_end_hm" in p and p["time_end_hm"] is not None:
        _set("TAIL_TIME_END", int(float(p["time_end_hm"])))
    if "max_daily" in p and p["max_daily"] is not None:
        _set("TAIL_MAX_DAILY", int(float(p["max_daily"])))
    if "eod_enabled" in p:
        v = p.get("eod_enabled")
        patch["TAIL_EOD_ENABLED"] = (
            "1" if str(v).strip().lower() in ("1", "true", "t", "y", "yes", "on") else "0"
        )
    if "eod_hm" in p and p["eod_hm"] is not None:
        eod_s = str(p["eod_hm"]).strip()
        if eod_s and ":" not in eod_s and len(eod_s) == 4 and eod_s.isdigit():
            eod_s = f"{eod_s[:2]}:{eod_s[2:]}"
        patch["TAIL_EOD_HM"] = eod_s
    if "min_price" in p and p["min_price"] is not None:
        _set("TAIL_MIN_PRICE", float(p["min_price"]))
    if "slot_money" in p and p["slot_money"] is not None:
        _set("TAIL_SLOT_MONEY", int(float(p["slot_money"])))
    if "max_stocks" in p and p["max_stocks"] is not None:
        _set("TAIL_MAX_STOCKS", int(float(p["max_stocks"])))
    if "total_budget_krw" in p and p["total_budget_krw"] is not None:
        _set("TAIL_TOTAL_BUDGET_KRW", int(float(p["total_budget_krw"])))
    if "entry_mode" in p and p["entry_mode"] is not None:
        _set("TAIL_ENTRY_MODE", str(p["entry_mode"]).strip().lower())
    for js_k, env_k in (
        ("limit_atr_mult", "TAIL_LIMIT_ATR_MULT"),
        ("limit_anchor", "TAIL_LIMIT_ANCHOR"),
        ("limit_valid_bars", "TAIL_LIMIT_VALID_BARS"),
        ("limit_fill_slip_pct", "TAIL_LIMIT_FILL_SLIP_PCT"),
    ):
        if js_k in p and p[js_k] is not None:
            _set(env_k, p[js_k])
    for js_k, env_k in (
        ("skip_hts_scan_dupes", "TAIL_SKIP_HTS_SCAN_DUPES"),
        ("use_intraday_drop", "TAIL_USE_INTRADAY_DROP"),
        ("use_ma20_filter", "TAIL_USE_MA20_FILTER"),
        ("use_rsi_filter", "TAIL_USE_RSI_FILTER"),
        ("use_daily_range_filter", "TAIL_USE_DAILY_RANGE_FILTER"),
        ("use_high_chase_filter", "TAIL_USE_HIGH_CHASE_FILTER"),
    ):
        if js_k in p:
            _set(env_k, str(p[js_k]).lower())
    if "bar_chg_min_pct" in p:
        _set("TAIL_BAR_CHG_MIN_PCT", p["bar_chg_min_pct"])
    if "bar_chg_max_pct" in p:
        _set("TAIL_BAR_CHG_MAX_PCT", p["bar_chg_max_pct"])
    if "tail_vol_mult" in p:
        _set("TAIL_VOL_MULT", p["tail_vol_mult"])
    if "tail_vol_win" in p:
        _set("TAIL_VOL_WIN", int(float(p["tail_vol_win"])))
    for js_k, env_k in (
        ("pattern_hammer", "TAIL_PATTERN_HAMMER"),
        ("pattern_pin", "TAIL_PATTERN_PIN"),
        ("pattern_engulfing", "TAIL_PATTERN_ENGULFING"),
        ("pattern_piercing", "TAIL_PATTERN_PIERCING"),
        ("pattern_harami", "TAIL_PATTERN_HARAMI"),
        ("pattern_doji", "TAIL_PATTERN_DOJI"),
        ("pattern_morning_star", "TAIL_PATTERN_MORNING_STAR"),
    ):
        if js_k in p:
            _set(env_k, str(p[js_k]).lower())
    for js_k, env_k in (
        ("pin_close_upper_ratio", "TAIL_PIN_CLOSE_UPPER_RATIO"),
        ("pin_max_upper_tail_ratio", "TAIL_PIN_MAX_UPPER_TAIL_RATIO"),
        ("engulf_min_body_ratio", "TAIL_ENGULF_MIN_BODY_RATIO"),
        ("piercing_penetrate_ratio", "TAIL_PIERCING_PENETRATE_RATIO"),
        ("harami_max_body_ratio", "TAIL_HARAMI_MAX_BODY_RATIO"),
        ("doji_body_max_ratio", "TAIL_DOJI_BODY_MAX_RATIO"),
        ("morning_star_body_max_ratio", "TAIL_MORNING_STAR_BODY_MAX_RATIO"),
        ("candle_lookback", "TAIL_CANDLE_LOOKBACK"),
    ):
        if js_k in p and p[js_k] is not None:
            _set(env_k, p[js_k])
    return patch


def web_body_to_tail_env_patch(body: Dict[str, Any]) -> Dict[str, str]:
    """웹 saveTailConfig POST → TAIL env (퍼센트·분 단위 변환)."""
    if not isinstance(body, dict):
        return {}

    def _get(key: str) -> Any:
        v = body.get(key)
        if v is None or v == "":
            return None
        return v

    p: Dict[str, Any] = {}
    md = _get("min_drop_rate")
    if md is not None:
        p["min_drop_rate"] = float(md) / 100.0
    mr = _get("min_recovery_ratio")
    if mr is not None:
        p["min_recovery_ratio"] = float(mr) / 100.0
    tr = _get("tail_ratio_min")
    if tr is not None:
        p["tail_ratio_min"] = float(tr)
    tp = _get("tail_pct_min")
    if tp is not None:
        p["tail_pct_min"] = float(tp) / 100.0
    m3 = _get("max_rec_3m")
    if m3 is not None:
        x = float(m3)
        p["max_rec_3m"] = x if 0 < x <= 1 else x / 100.0
    sl = _get("sl_pct")
    if sl is not None:
        p["sl_pct"] = float(sl) / 100.0
    tpp = _get("tp_pct")
    if tpp is not None:
        p["tp_pct"] = float(tpp) / 100.0
    smh = _get("shoulder_min_high")
    if smh is not None:
        p["shoulder_min_high"] = float(smh) / 100.0
    sc = _get("shoulder_cut_pct")
    if sc is not None:
        p["shoulder_cut_pct"] = float(sc) / 100.0
    trp = _get("trail_pct")
    if trp is not None:
        p["trail_pct"] = float(trp) / 100.0
    tra = _get("trail_arm_pct")
    if tra is not None:
        p["trail_arm_pct"] = float(tra) / 100.0
    hc = _get("high_chase_thr")
    if hc is not None:
        x = float(hc)
        p["high_chase_thr"] = x if 0 < x <= 1 else x / 100.0
    for k in (
        "cooldown_min", "rsi_threshold", "rsi_period", "time_start", "time_end",
        "max_daily", "min_price", "max_daily_change", "ma20_max_above",
        "max_loss_krw", "stop_atr_mult", "target_atr_mult",
        "atr_sl_min_pct", "atr_sl_max_pct", "atr_tp_min_pct", "atr_tp_max_pct",
        "slot_money", "max_stocks", "total_budget_krw", "entry_mode",
        "limit_atr_mult", "limit_anchor", "limit_valid_bars", "limit_fill_slip_pct",
        "skip_hts_scan_dupes", "use_intraday_drop", "use_ma20_filter",
        "use_rsi_filter", "use_daily_range_filter", "use_high_chase_filter",
        "bar_chg_min_pct", "bar_chg_max_pct", "max_hold_bars",
        "tail_vol_mult", "tail_vol_win",
        "backtest_use_tick_db", "backtest_tick_fallback_ohlc",
        "pattern_hammer", "pattern_pin", "pattern_engulfing", "pattern_piercing",
        "pattern_harami", "pattern_doji", "pattern_morning_star",
        "pin_close_upper_ratio", "pin_max_upper_tail_ratio",
        "engulf_min_body_ratio", "piercing_penetrate_ratio",
        "harami_max_body_ratio", "doji_body_max_ratio",
        "morning_star_body_max_ratio", "candle_lookback",
    ):
        v = _get(k)
        if v is not None:
            p[k] = v
    if "ratchet_tiers" in body:
        p["ratchet_tiers"] = str(body.get("ratchet_tiers") or "").strip()
    if "eod_enabled" in body:
        p["eod_enabled"] = body.get("eod_enabled")
    eod_hm = _get("eod_hm")
    if eod_hm is not None:
        p["eod_hm"] = str(eod_hm).strip()
    mdloss = _get("min_drop_pct_for_loss_cut")
    if mdloss is not None:
        p["min_drop_pct_for_loss_cut"] = float(mdloss)
    if "time_start" in p:
        p["time_start_hm"] = int(float(p.pop("time_start")))
    if "time_end" in p:
        p["time_end_hm"] = int(float(p.pop("time_end")))
    return params_to_tail_env_patch(p)


def migrate_config_short_legacy(db=None) -> Dict[str, str]:
    """config_short 최신 행: 레거시 값 → TAIL_* 복사 후 UPDATE."""
    own_db = None
    applied: Dict[str, str] = {}
    try:
        if db is None:
            from database import TradeDB
            own_db = TradeDB()
            db = own_db
        row = db.conn.execute(
            "SELECT * FROM config_short ORDER BY id DESC LIMIT 1"
        ).fetchone()
        if not row:
            return applied
        r = dict(row)
        rid = r.get("id")
        updates: Dict[str, str] = {}
        for old_k, new_k in LEGACY_TO_TAIL.items():
            if new_k in updates:
                continue
            ov = r.get(old_k)
            nv = r.get(new_k)
            if ov not in (None, "", "None") and (nv in (None, "", "None")):
                updates[new_k] = str(ov)
        if not updates or rid is None:
            return applied
        cols = ", ".join(f"{k}=%s" for k in updates)
        db.conn.execute(
            f"UPDATE config_short SET {cols} WHERE id=%s",
            list(updates.values()) + [rid],
        )
        try:
            db.conn.commit()
        except Exception:
            pass
        applied = updates
    finally:
        if own_db is not None:
            try:
                own_db.close()
            except Exception:
                pass
    return applied
