#!/usr/bin/env python3
"""
kis_trader/engine/dart_engine.py — DART 수주 공시 후 RSI 반등 TRIGGER
====================================================================
[SCAN] Open DART 단일판매·공급계약 → dart_disclosures / dart_watchlist
[TRIGGER] 공시 이후 이벤트 창 안에서 1분봉 RSI 과매도 후 재돌파 시 진입
[EXIT] 손절·익절·트레일·보유봉·EOD

실매·웹백테·Optuna 동일 판정 함수 사용.
"""
from __future__ import annotations

from typing import Any, Dict, List, Optional, Tuple

from kis_trader.engine.scalping_engine import compute_rsi_series
from kis_trader.utils.env import get_env_bool

DART_STRATEGY_ID = "DART"


def _to_bool(v: Any, default: bool = True) -> bool:
    if v is None:
        return default
    if isinstance(v, bool):
        return v
    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 dart_scan_enabled() -> bool:
    """SCAN 폴링 ON — DART_SCAN_ENABLED 우선, 없으면 DART_ENABLED."""
    if get_env_from_db_raw("DART_SCAN_ENABLED") not in (None, ""):
        return get_env_bool("DART_SCAN_ENABLED", True)
    return get_env_bool("DART_ENABLED", True)


def get_env_from_db_raw(key: str) -> Any:
    try:
        from kis_trader.utils.env import get_env_from_db
        return get_env_from_db(key, "")
    except Exception:
        return ""


def get_dart_defaults_from_db(*, env_row: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
    """DB/env → 엔진 params (웹·실매·Optuna 공통)."""
    r: Dict[str, Any] = {}
    if env_row:
        r = dict(env_row)
    else:
        try:
            from database import TradeDB
            db = TradeDB()
            try:
                r = dict(db.get_merged_env_snapshot() or {})
            finally:
                db.close()
        except Exception:
            r = {}

    def _f(key: str, default: float) -> float:
        try:
            v = r.get(key)
            if v in (None, "", "None"):
                return float(default)
            return float(v)
        except Exception:
            return float(default)

    def _i(key: str, default: int) -> int:
        try:
            v = r.get(key)
            if v in (None, "", "None"):
                return int(default)
            return int(float(v))
        except Exception:
            return int(default)

    def _b(key: str, default: bool) -> bool:
        v = r.get(key)
        if v in (None, "", "None"):
            return default
        return _to_bool(v, default)

    return {
        "slot_money": _i("DART_SLOT_MONEY", _i("SLOT_MONEY_DEFAULT", 300_000)),
        "max_stocks": _i("DART_MAX_STOCKS", 5),
        "total_budget_krw": _i("DART_TOTAL_BUDGET_KRW", 1_500_000),
        "short_max_buy_amount": _i("DART_MAX_BUY_AMOUNT", 0),
        "time_start_hm": _i("DART_TIME_START", 930),
        "time_end_hm": _i("DART_TIME_END", 1520),
        "rsi_period": _i("DART_RSI_PERIOD", 5),
        "rsi_oversold": _f("DART_RSI_OVERSOLD", 30.0),
        "rsi_reclaim": _f("DART_RSI_RECLAIM", 35.0),
        "sl_pct": abs(_f("DART_STOP_LOSS_PCT", 0.02)),
        "tp_pct": abs(_f("DART_TAKE_PROFIT_PCT", 0.04)),
        "trail_pct": abs(_f("DART_TRAIL_PCT", 0.015)),
        "trail_arm_pct": abs(_f("DART_TRAIL_ARM_PCT", 0.02)),
        "max_hold_bars": _i("DART_MAX_HOLD_BARS", 60),
        "event_window_bars": _i("DART_EVENT_WINDOW_BARS", 120),
        "min_price": _f("DART_MIN_PRICE", 1000.0),
        "vol_mult": _f("DART_VOL_MULT", 1.5),
        "vol_window": _i("DART_VOL_WINDOW", 10),
        "max_loss_krw": _i("DART_MAX_LOSS_PER_TRADE_KRW", 150_000),
        "force_eod_exit": _b("DART_FORCE_EOD_EXIT", True),
        "trade_enabled": _b("DART_TRADE_ENABLED", False),
        "subscribe_enabled": _b("DART_SUBSCRIBE_ENABLED", False),
        "watch_ttl_hours": _i("DART_WATCH_TTL_HOURS", 24),
        "watch_max": _i("DART_WATCH_MAX", 15),
        "scan_enabled": _b("DART_SCAN_ENABLED", _b("DART_ENABLED", True)),
    }


def dart_min_bars_required(params: Optional[Dict[str, Any]] = None) -> int:
    p = params or {}
    return max(
        int(p.get("rsi_period", 5)) + 5,
        int(p.get("vol_window", 10)) + 3,
        20,
    )


def _candle_hm(ct: str) -> Optional[int]:
    s = str(ct or "")
    if len(s) < 12:
        return None
    try:
        return int(s[8:12])
    except Exception:
        return None


def _bars_since_event(candles: List[Dict], i: int, event_ct: str) -> Optional[int]:
    """신호봉 i 기준으로 event_ct(YYYYMMDDHHMM…) 이후 경과 봉 수."""
    ev = str(event_ct or "")[:12]
    if len(ev) < 12:
        return None
    n = 0
    for j in range(i + 1):
        ct = str(candles[j].get("candle_time") or "")[:12]
        if ct >= ev:
            n += 1
    return n


def eval_dart_buy_at_index(
    candles: List[Dict],
    i: int,
    params: Dict[str, Any],
    *,
    event_candle_time: str,
) -> Tuple[Optional[str], Optional[str], Optional[Dict[str, Any]]]:
    """
    DART TRIGGER — 공시 시각 이후 event_window_bars 안에서
    RSI 과매도 후 rsi_reclaim 상향 돌파 + (선택) 거래량 배수.
    """
    rsi_period = int(params.get("rsi_period", 5))
    rsi_os = float(params.get("rsi_oversold", 30.0))
    rsi_rc = float(params.get("rsi_reclaim", 35.0))
    vol_mult = float(params.get("vol_mult", 1.5))
    vol_win = int(params.get("vol_window", 10))
    time_start = int(params.get("time_start_hm", 930))
    time_end = int(params.get("time_end_hm", 1520))
    min_price = float(params.get("min_price", 1000.0))
    event_win = int(params.get("event_window_bars", 120))

    need = dart_min_bars_required(params)
    if i < need or i >= len(candles):
        return ("탈락-봉부족", "need=%d i=%d" % (need, i), None)

    c = candles[i]
    ct = str(c.get("candle_time") or "")
    hm = _candle_hm(ct)
    if hm is None:
        return ("탈락-시간없음", ct, None)
    if hm < time_start or hm >= time_end:
        return (None, None, None)

    since = _bars_since_event(candles, i, event_candle_time)
    if since is None or since <= 0:
        return ("탈락-공시전이벤트", str(event_candle_time)[:12], None)
    if since > event_win:
        return ("탈락-이벤트창초과", "since=%d win=%d" % (since, event_win), None)

    try:
        cl = float(c["close"])
        vol = float(c.get("volume", 0) or 0)
    except Exception as e:
        return ("탈락-캔들파싱", str(e), None)
    if cl < min_price:
        return ("탈락-최소가격", "%.0f" % cl, None)

    closes = [float(x["close"]) for x in candles[: i + 1]]
    rsis = compute_rsi_series(closes, rsi_period)
    rsi = rsis[i]
    prev = rsis[i - 1] if i >= 1 else None
    if rsi is None or prev is None:
        return ("탈락-RSI없음", None, None)

    # 직전 과매도 구간을 찍고, 현재봉에서 reclaim 상향
    if not (prev <= rsi_os and rsi >= rsi_rc and rsi > prev):
        return (None, None, None)

    if vol_win > 0 and vol_mult > 0:
        vs = [
            float(candles[k].get("volume", 0) or 0)
            for k in range(max(0, i - vol_win), i)
        ]
        avg = sum(vs) / len(vs) if vs else 0.0
        if avg > 0 and vol < avg * vol_mult:
            return (
                "탈락-거래량부족",
                "%.0f < %.0f×%.2f" % (vol, avg, vol_mult),
                None,
            )

    entry = float(c.get("open") or cl)  # align: 신호=T-1 확정 후 진입은 호출측
    return (
        None,
        None,
        {
            "entry_price": entry,
            "rsi": round(float(rsi), 2),
            "signal_time": ct[:12],
            "event_bars": since,
        },
    )


def check_buy_signal_dart_live(
    candles: List[Dict],
    params: Dict[str, Any],
    *,
    event_candle_time: str,
) -> Tuple[Optional[str], Optional[str], Optional[Dict[str, Any]]]:
    """
    실매: 마지막 확정봉(T-1)에서 신호, 진입가는 호출측이 T 시가/틱 정렬.
    live_backtest_align — ±1 보정 금지.
    """
    if not candles:
        return ("탈락-봉없음", None, None)
    # 확정봉만
    conf = [c for c in candles if _to_bool(c.get("is_confirmed", 1), True)]
    if len(conf) < 2:
        conf = list(candles)
    # 신호 = 직전 확정봉 (마지막이 진행중일 수 있음)
    sig_i = len(conf) - 2 if len(conf) >= 2 else len(conf) - 1
    if sig_i < 0:
        return ("탈락-봉부족", None, None)
    reject, msg, sig = eval_dart_buy_at_index(
        conf, sig_i, params, event_candle_time=event_candle_time,
    )
    if reject or not sig:
        return (reject, msg, None)
    # 진입 참고가: 다음 봉 시가(있으면) else 신호봉 종가
    if sig_i + 1 < len(conf):
        nxt = conf[sig_i + 1]
        try:
            sig["entry_price"] = float(nxt.get("open") or sig["entry_price"])
            sig["entry_time"] = str(nxt.get("candle_time") or "")[:12]
        except Exception:
            pass
    else:
        sig["entry_time"] = sig.get("signal_time")
    return (None, None, sig)


def check_sell_signal_dart_live(
    *,
    buy_price: float,
    highest: float,
    last_price: float,
    bars_held: int,
    params: Dict[str, Any],
    now_hm: Optional[int] = None,
) -> Tuple[bool, str]:
    """손절·익절·트레일·보유한도·EOD."""
    if buy_price <= 0 or last_price <= 0:
        return False, ""
    sl = abs(float(params.get("sl_pct", 0.02)))
    tp = abs(float(params.get("tp_pct", 0.04)))
    trail = abs(float(params.get("trail_pct", 0.015)))
    arm = abs(float(params.get("trail_arm_pct", 0.02)))
    max_hold = int(params.get("max_hold_bars", 60))
    pnl = (last_price - buy_price) / buy_price
    if pnl <= -sl:
        return True, "손절"
    if pnl >= tp:
        return True, "익절"
    hi = max(float(highest or buy_price), last_price)
    peak = (hi - buy_price) / buy_price
    if peak >= arm and hi > 0:
        dd = (hi - last_price) / hi
        if dd >= trail:
            return True, "트레일"
    if max_hold > 0 and bars_held >= max_hold:
        return True, "보유한도"
    if _to_bool(params.get("force_eod_exit"), True) and now_hm is not None:
        if now_hm >= 1520:
            return True, "EOD"
    return False, ""


def run_dart_backtest_code(
    candles: List[Dict],
    params: Dict[str, Any],
    *,
    event_candle_time: str,
    slot_money: float,
    fee_rate: float = 0.00015,
    sell_tax: float = 0.0018,
) -> List[Dict[str, Any]]:
    """단일 종목·단일 공시 이벤트 백테 (간단 포트 외 호출용)."""
    trades: List[Dict[str, Any]] = []
    if not candles:
        return trades
    conf = [c for c in candles if _to_bool(c.get("is_confirmed", 1), True)]
    if len(conf) < dart_min_bars_required(params):
        return trades

    position = None
    for i in range(1, len(conf)):
        # 신호 = i-1, 진입 = i (align)
        reject, _msg, sig = eval_dart_buy_at_index(
            conf, i - 1, params, event_candle_time=event_candle_time,
        )
        if position is None and sig and not reject:
            entry = float(conf[i].get("open") or conf[i].get("close") or 0)
            if entry <= 0:
                continue
            qty = max(1, int(slot_money / entry))
            position = {
                "entry_price": entry,
                "entry_time": str(conf[i].get("candle_time") or "")[:12],
                "qty": qty,
                "highest": entry,
                "bars": 0,
                "rsi": sig.get("rsi"),
            }
            continue
        if position is None:
            continue
        position["bars"] += 1
        cl = float(conf[i].get("close") or 0)
        hi = float(conf[i].get("high") or cl)
        position["highest"] = max(position["highest"], hi)
        hm = _candle_hm(str(conf[i].get("candle_time") or ""))
        sell, reason = check_sell_signal_dart_live(
            buy_price=position["entry_price"],
            highest=position["highest"],
            last_price=cl,
            bars_held=position["bars"],
            params=params,
            now_hm=hm,
        )
        if not sell:
            continue
        buy_p = position["entry_price"]
        sell_p = cl
        qty = position["qty"]
        gross = (sell_p - buy_p) * qty
        fee = (buy_p + sell_p) * qty * fee_rate
        tax = sell_p * qty * sell_tax
        pnl = gross - fee - tax
        trades.append({
            "entry_time": position["entry_time"],
            "exit_time": str(conf[i].get("candle_time") or "")[:12],
            "buy_price": buy_p,
            "sell_price": sell_p,
            "qty": qty,
            "pnl": pnl,
            "reason": reason,
            "rsi_entry": position.get("rsi"),
        })
        position = None
    return trades
