"""
kis_trader/strategies/scalping.py — 스캘핑 전략 (1분봉 RSI 과매도 되돌림 / Reversal 고정)
==================================================================================
[전략 컨셉 — SCAN vs TRIGGER]
- **SCAN (HTS/랭킹, 널넬하게)**: K/L/제외 또는 거래량 랭킹 → 감시 대상만 선정.
- **TRIGGER (코드, 엄격하게)** — ``SCALP_USE_MACD_CROSS`` 로 선택:
  - **false (기본)**: RSI(3) 과매도(<25) → 양봉 전환 V자 반등 ("바닥잡기").
  - **true**: HTS C — MACD+Stochastic [12,26,5,3,3] 골든크로스 (0봉전 상향돌파).
- 청산: TP/SL/EOD/이동평균 이탈 등 (scalping_engine.check_sell_signal_live).
- ⚠️ 모멘텀 추종은 MomentumStrategy 분리 (MACD 골든크로스 HTS 는 SCALP TRIGGER 로 이전).

[엔진 함수 의존]
- 진입 시그널: ``scalping_engine.check_buy_signal_live``       (백테스트 동일 로직)
- 청산 시그널: ``scalping_engine.check_sell_signal_live``      (백테스트 동일 로직)

[Deprecated]
- 과거 ``SCALP_MODE`` 토글(reversal/momentum) 은 폐기되었습니다.
  ``SCALP_MODE=momentum`` 이 DB 에 남아있으면 시작 시 1회 경고 + reversal 폴백.
  추세추종(모멘텀) 전략은 ``STRATEGY_MOMENTUM_ENABLED=true`` 로 별도 켜세요.

[주문 실행]
- ``OrderManager.place(OrderRequest(strategy_id="SCALP", ...))``
  → ODNO 저장, 종목 Lock, 실 잔고 검증까지 한 번에 처리.
"""
from __future__ import annotations

import time
from datetime import datetime as dt
from typing import Dict, List, Optional

try:
    from ..engine import scalping_engine as se
except ImportError:
    se = None

from ..utils.env import get_env_bool, get_env_float, get_env_from_db, get_env_int
from .base import BaseStrategy


class ScalpingStrategy(BaseStrategy):
    strategy_id = "SCALP"
    loop_min_sleep = 1.0
    loop_max_sleep = 2.0

    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        self.candle_tf = 1  # 1분봉
        self._scan_engine_params: Optional[Dict] = None
        # SCALP_MODE deprecate 경고는 시작 시 1회만 (루프마다 reload_config 호출돼 스팸 방지)
        self._deprecation_warned: bool = False
        self.reload_config()

    # ------------------------------------------------------------------
    def reload_config(self) -> None:
        """env_config 기반 파라미터 리로드 (루프 1회 당 1번)."""
        self.min_price = get_env_float("MIN_STOCK_PRICE", 1000.0)
        self.high_chase_thr = get_env_float("HIGH_CHASE_THR", 0.96)
        self.max_daily_chg = get_env_float("MAX_DAILY_CHG", 20.0)
        self.vol_multiplier = get_env_float("VOL_MULTIPLIER", 0.0)

        # 부호 무관하게 항상 손절은 음수, 익절은 양수로 정규화
        # (DB에 0.012/-0.012 어느 쪽이 들어와도 stop_price 가 매수가 아래로 잡히도록.)
        self.scalp_stop_loss_pct = -abs(get_env_float("SCALP_STOP_LOSS_PCT", 0.015))
        self.scalp_take_profit_pct = abs(get_env_float("SCALP_TAKE_PROFIT_PCT", 0.015))
        self.scalp_tp_max_pct = abs(get_env_float("SCALP_TP_MAX_PCT", 0.02))
        self.scalp_min_drop_rate = get_env_float("SCALP_MIN_DROP_RATE", 0.015)
        self.atr_down_mult = get_env_float("ATR_DOWN_MULT", 1.5)

        self.rsi_oversold = get_env_float("SCALP_RSI_OVERSOLD", 25.0)
        self.rsi_overbought = get_env_float("SCALP_RSI_OVERBOUGHT", 75.0)
        self.slot_money = get_env_int("SLOT_MONEY_DEFAULT", 3000000)

        # ── SCALP_MODE deprecate 가드 ────────────────────────────────
        # 과거에 reversal/momentum 토글이 있었지만, 모멘텀은 MomentumStrategy 로 분리됨.
        # SCALP 는 항상 reversal 로 동작. DB 에 'momentum' 이 남아있으면 시작 시 1회 경고.
        legacy_mode = (get_env_from_db("SCALP_MODE", "reversal") or "reversal").strip().lower()
        if legacy_mode == "momentum" and not self._deprecation_warned:
            self.logger.warning(
                "⚠️ [DEPRECATED] SCALP_MODE=momentum 은 더 이상 지원되지 않습니다. "
                "SCALP 는 reversal 로 고정 동작합니다. "
                "추세추종을 원하시면 STRATEGY_MOMENTUM_ENABLED=true 로 MomentumStrategy 를 켜주세요."
            )
            self._deprecation_warned = True
        self.scalp_mode = "reversal"  # 고정

        if se is not None:
            try:
                _d = se.get_scalping_defaults_from_db()
                self._scan_engine_params = {
                    **_d,
                    "rsi_oversold": self.rsi_oversold,
                    "rsi_overbought": self.rsi_overbought,
                    "sl_pct": abs(self.scalp_stop_loss_pct),
                    "tp_pct": self.scalp_take_profit_pct,
                    "drop_rate": self.scalp_min_drop_rate,
                    "vol_mult": self.vol_multiplier if self.vol_multiplier > 0 else 0,
                    "require_reversal_candle": get_env_bool(
                        "SCALP_REQUIRE_REVERSAL_CANDLE", True,
                    ),
                    # True: 백테스트와 동일 — 신호봉(직전 확정봉) 조건 충족 시 현재봉에서 진입
                    "live_backtest_align": get_env_bool(
                        "SCALP_LIVE_BACKTEST_ALIGN", True,
                    ),
                    "live_signal_lookback_bars": get_env_int(
                        "SCALP_LIVE_SIGNAL_LOOKBACK_BARS", 1,
                    ),
                    # 일일 진입 횟수 (reversal 기본 3회)
                    "max_daily":     get_env_int(
                        "SCALP_MAX_DAILY", _d.get("max_daily", 3),
                    ),
                    "use_macd_cross": get_env_bool("SCALP_USE_MACD_CROSS", False),
                    "macd_fast": get_env_int("SCALP_MACD_FAST", 12),
                    "macd_slow": get_env_int("SCALP_MACD_SLOW", 26),
                    "macd_signal": get_env_int("SCALP_MACD_SIGNAL", 5),
                    "stoch_k_period": get_env_int("SCALP_STOCH_K_PERIOD", 5),
                    "stoch_d_period": get_env_int("SCALP_STOCH_D_PERIOD", 3),
                    "stoch_slow": get_env_int("SCALP_STOCH_SLOW", 3),
                }
            except Exception as e:
                self.logger.debug("scalping_engine defaults 조회 실패: %s", e)

    def _candidate_filter(self, candidate: Dict) -> bool:
        """scalp_on 이 True 인 후보만 대상."""
        return bool(candidate.get("scalp_on", True))

    # ------------------------------------------------------------------
    # 매수
    # ------------------------------------------------------------------
    def check_buy(self, code: str, name: str) -> Optional[Dict]:
        if se is None:
            self.logger.warning("scalping_engine 미탑재 → 매수 체크 스킵")
            return None
        try:
            if get_env_bool("FORCE_BUY_TEST", False):
                return self._force_buy_test(code, name)

            candles_raw = self.ws.get_candles(code, self.candle_tf, n=50)
            if len(candles_raw) < 5:
                return None
            candles = [self._norm_candle(c) for c in candles_raw]

            # 엔진 state
            today = dt.now().strftime("%Y%m%d")
            last_exit_dt = None
            if code in self.recently_sold:
                try:
                    last_exit_dt = dt.fromtimestamp(self.recently_sold[code])
                    if last_exit_dt.strftime("%Y%m%d") != today:
                        last_exit_dt = None
                except Exception:
                    pass
            try:
                today_trades = self.db.get_trades_by_date(today)
                daily_cnt = len([
                    t for t in today_trades
                    if t.get("code") == code and str(t.get("strategy", "")).startswith("SCALP")
                ])
            except Exception:
                daily_cnt = 0
            state = {"last_exit_dt": last_exit_dt, "daily_cnt": daily_cnt}

            params = self._scan_engine_params or {}
            # SCALP 는 reversal 단일 모드 (momentum 은 MomentumStrategy 로 분리됨)
            reject, msg, sig = se.check_buy_signal_live(candles, params, state)
            if reject:
                self.logger.info("🔍 [%s] %s %s: %s", reject, name, code, msg or "")
                return None
            if not sig:
                return None

            latest = candles[-1]
            curr_price = float(latest["close"])
            if curr_price < self.min_price:
                return None

            # 현재가 보정 (WS → 없으면 REST)
            wsd = self.ws.get_price(code)
            if wsd:
                try:
                    curr_price = abs(float(str(wsd.get("stck_prpr", curr_price)).replace(",", ""))) or curr_price
                except Exception:
                    pass
            if curr_price <= 0:
                return None

            hard_cap = get_env_int("SCALP_MAX_BUY_AMOUNT", 0) \
                or get_env_int("MAX_BUY_AMOUNT_PER_STOCK", 0)
            qty, rej = self._resolve_buy_qty_live(
                curr_price, hard_cap=hard_cap,
            )
            if rej:
                self.logger.info(
                    "🔍 [탈락-%s] %s(%s) price=%.0f",
                    rej, name, code, curr_price,
                )
                return None

            stop_price = curr_price * (1 + self.scalp_stop_loss_pct)
            eff_tp = se.resolve_effective_tp_pct(
                self.scalp_take_profit_pct, self.scalp_tp_max_pct,
            )
            target_price = curr_price * (1 + eff_tp)

            self.logger.info(
                "🎯 [SCALP-REVERSAL 시그널] %s(%s) price=%.0f qty=%d RSI=%.1f",
                name, code, curr_price, qty, sig.get("rsi", 0),
            )
            return {
                "code": code,
                "name": name,
                "price": curr_price,
                "qty": qty,
                "stop_price": stop_price,
                "target_price": target_price,
                "atr_entry": 0.0,
                "size_class": "",
                "entry_features": {"rsi": sig.get("rsi", 0)},
            }
        except Exception as e:
            self.logger.info("🔍 [탈락-예외] %s %s: %s", name, code, e)
            return None

    def _force_buy_test(self, code: str, name: str) -> Optional[Dict]:
        wsd = self.ws.get_price(code)
        px = 0.0
        if wsd:
            try:
                px = abs(float(str(wsd.get("stck_prpr", 0)).replace(",", "")))
            except Exception:
                px = 0.0
        if px <= 0:
            pd_ = self.client.inquire_price(code)
            if pd_:
                try:
                    px = abs(float(str(pd_.get("stck_prpr", 0)).replace(",", "")))
                except Exception:
                    px = 0.0
        if px <= 0:
            return None
        qty = max(1, int(self.slot_money / px))
        return {
            "code": code,
            "name": name,
            "price": px,
            "qty": qty,
            "stop_price": px * (1 + self.scalp_stop_loss_pct),
            "target_price": px * (1 + se.resolve_effective_tp_pct(
                self.scalp_take_profit_pct, self.scalp_tp_max_pct,
            )),
            "atr_entry": 0.0,
            "size_class": "",
            "entry_features": {},
        }

    # ------------------------------------------------------------------
    # 매도
    # ------------------------------------------------------------------
    def check_sell_signals(self) -> List[Dict]:
        """엔진 check_sell_signal_live 사용 (백테스트 동일)."""
        if not self.holdings:
            return []
        if se is None:
            return []

        signals: List[Dict] = []
        now = dt.now()
        is_eod = (now.hour == 15 and now.minute >= 25) or now.hour > 15

        try:
            params = se.get_scalping_defaults_from_db()
        except Exception:
            params = {}
        params.update({
            "max_loss_krw": float(
                get_env_int("SCALP_MAX_LOSS_PER_TRADE_KRW", 0)
                or get_env_int("MAX_LOSS_PER_TRADE_KRW", 200000)
            ),
            "min_drop_pct_for_loss_cut": get_env_float(
                "SCALP_MIN_DROP_PCT_FOR_LOSS_CUT", 0.015
            ),
            "fee_rate": get_env_float("FEE_RATE_PCT", 0.015) / 100,
            "sell_tax": get_env_float("SELL_TAX_RATE_PCT", 0.18) / 100,
            "min_margin": get_env_float("SCALP_MIN_PROFIT_PCT", 0.2) / 100,
            "shoulder_min_high": float(params.get("shoulder_min_high", 0.005)),
            "shoulder_cut_pct": float(params.get("shoulder_cut_pct", 0.003)),
            "min_hold_sec": float(get_env_int("SCALP_MIN_HOLD_SEC", 30)),
        })

        for code, holding in list(self.holdings.items()):
            try:
                name = holding.get("name", code)
                buy_price = float(holding.get("buy_price", 0))
                qty = int(holding.get("qty", 0))
                stop = float(holding.get("stop_price", 0))
                target = float(holding.get("target_price", 0))
                max_price = float(holding.get("max_price", buy_price))

                if qty <= 0 or buy_price <= 0:
                    continue

                # 현재가 (WS → REST)
                current_price = 0.0
                wsd = self.ws.get_price(code)
                if wsd:
                    try:
                        current_price = abs(float(str(wsd.get("stck_prpr", 0)).replace(",", "")))
                    except Exception:
                        current_price = 0.0
                if current_price <= 0:
                    pd_ = self.client.inquire_price(code)
                    if pd_:
                        try:
                            current_price = abs(float(str(pd_.get("stck_prpr", 0)).replace(",", "")))
                        except Exception:
                            current_price = 0.0
                if current_price <= 0:
                    continue

                # max_price 업데이트
                if current_price > max_price:
                    max_price = current_price
                    holding["max_price"] = max_price

                profit_pct = (current_price - buy_price) / buy_price if buy_price > 0 else 0

                position = {
                    "entry_price": buy_price,
                    "entry_time": holding.get("buy_time", ""),
                    "qty": qty,
                    "stop": stop,
                    "target": target,
                    "max_price": max_price,
                }
                candle = {
                    "high": max_price,
                    "low": current_price,
                    "close": current_price,
                    "candle_time": now.strftime("%Y%m%d%H%M"),
                }

                res = se.check_sell_signal_live(position, candle, params, is_eod=is_eod)
                if not res:
                    continue
                reason, exit_price = res

                signals.append({
                    "code": code,
                    "name": name,
                    "current_price": current_price,
                    "price": exit_price,
                    "qty": qty,
                    "buy_price": buy_price,
                    "profit_pct": profit_pct,
                    "reason": reason,
                })
            except Exception as e:
                self.logger.error("매도 시그널 체크 오류(%s): %s", code, e)

        return signals

    # ------------------------------------------------------------------
    def _norm_candle(self, c: dict) -> dict:
        ct = c.get("candle_time") or c.get("candle_time_str", "")
        if isinstance(ct, str) and len(ct) == 19 and " " in ct:
            ct = ct.replace("-", "").replace(" ", "").replace(":", "")[:12]
        return {
            "candle_time": ct,
            "open": float(c.get("open", 0)),
            "high": float(c.get("high", 0)),
            "low": float(c.get("low", 0)),
            "close": float(c.get("close", 0)),
            "volume": float(c.get("volume", 0)),
        }
