"""
kis_trader/strategies/tail_catch.py — 꼬리잡기 전략 (3분봉 망치형 되돌림)
==========================================================================
- 신호 생성: ``tail_engine.check_buy_signal_live`` / ``check_sell_signal_live``
- 주문 실행: ``OrderManager.place(OrderRequest(strategy_id="SHORT", ...))``
"""
from __future__ import annotations

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

try:
    from ..engine import tail_engine as te
except ImportError:
    te = None

from ..engine.limit_entry_common import (
    compute_atr_limit_price,
    floor_limit_price_krw,
    is_limit_atr_entry,
    limit_valid_until_bar_key,
    resolve_limit_anchor_price,
    short_entry_mode,
    should_cancel_unfilled_limit,
    tail_limit_params,
)
from ..utils.env import get_env_bool, get_env_float, get_env_from_db, get_env_int
from .base import BaseStrategy, is_live_eod_now


class TailCatchStrategy(BaseStrategy):
    strategy_id = "SHORT"
    loop_min_sleep = 1.5
    loop_max_sleep = 2.5

    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        self.candle_tf = 3  # 3분봉
        self._engine_params: Optional[Dict] = None
        self._pending_limit_orders: Dict[str, Dict] = {}
        self.reload_config()

    # ------------------------------------------------------------------
    def reload_config(self) -> None:
        self.min_price = get_env_float("MIN_STOCK_PRICE", 1000.0)
        self.stop_loss_pct = get_env_float("STOP_LOSS_PCT", -0.04)
        self.take_profit_pct = get_env_float("TAKE_PROFIT_PCT", 0.05)
        self.slot_money = (
            get_env_int("TAIL_SLOT_MONEY", 0)
            or get_env_int("SLOT_MONEY_DEFAULT", 3_000_000)
        )
        if te is not None:
            try:
                p = te.get_tail_defaults_from_db(self.db)
                p["live_backtest_align"] = get_env_bool(
                    "SHORT_LIVE_BACKTEST_ALIGN", True,
                )
                p["live_signal_lookback_bars"] = get_env_int(
                    "SHORT_LIVE_SIGNAL_LOOKBACK_BARS", 1,
                )
                p["entry_mode"] = short_entry_mode()
                self._engine_params = p
            except Exception as e:
                self.logger.debug("tail_engine defaults 조회 실패: %s", e)
        self.eod_enabled = get_env_bool("TAIL_EOD_ENABLED", True)
        self.eod_hm = get_env_from_db("TAIL_EOD_HM", "15:25")

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

    def manage_pending_orders(self) -> None:
        """ATR 지정가 미체결 — 유효 봉 지나면 취소, 체결 시 DB 반영."""
        if not self._pending_limit_orders:
            return
        from ..execution.order_manager import OrderRequest

        for code in list(self._pending_limit_orders.keys()):
            pend = self._pending_limit_orders.get(code)
            if not pend:
                continue
            if code in self.holdings:
                self._pending_limit_orders.pop(code, None)
                continue
            req = pend.get("request")
            ord_no = pend.get("ord_no")
            if req and ord_no:
                fin = self.order_mgr.try_finalize_limit_buy(req, ord_no)
                if fin.success and fin.filled_qty > 0:
                    self._load_holdings_from_db()
                    self._pending_limit_orders.pop(code, None)
                    self.logger.info(
                        "✅ [지정가체결-반영] %s ODNO=%s", code, ord_no,
                    )
                    continue
            candles_raw = self.ws.get_candles(code, self.candle_tf, n=30)
            if not candles_raw:
                continue
            candles = [self._norm_candle(c) for c in candles_raw]
            latest_key = str(candles[-1].get("candle_time") or "")[:12]
            vu = str(pend.get("valid_until_bar_key") or "")[:12]
            if not should_cancel_unfilled_limit(latest_key, vu):
                continue
            disp = pend.get("name") or code
            if ord_no and self.order_mgr.client.cancel_order(str(ord_no)):
                self.logger.info(
                    "🚫 [지정가취소] %s %s — 유효봉 종료(%s→%s) 미체결",
                    disp, code, vu, latest_key,
                )
            else:
                self.logger.info(
                    "🚫 [지정가만료] %s — 유효봉 %s 지남 (취소 API 실패 시 HTS 확인)",
                    code, vu,
                )
            self._pending_limit_orders.pop(code, None)

    def on_limit_buy_submitted(self, signal: Dict, result) -> None:
        from ..execution.order_manager import OrderRequest

        code = signal["code"]
        self._pending_limit_orders[code] = {
            "ord_no": result.ord_no,
            "valid_until_bar_key": signal.get("valid_until_bar_key"),
            "signal_bar_key": signal.get("signal_bar_key"),
            "name": signal.get("name", code),
            "request": OrderRequest(
                strategy_id=self.strategy_id,
                code=code,
                name=signal.get("name", code),
                side="BUY",
                qty=int(signal.get("qty", 0)),
                price_ref=float(signal.get("price", 0)),
                stop_price=float(signal.get("stop_price", 0)),
                target_price=float(signal.get("target_price", 0)),
                atr_entry=float(signal.get("atr_entry", 0)),
                size_class=signal.get("size_class"),
                entry_features=signal.get("entry_features"),
                use_limit_buy=True,
            ),
        }

    # ------------------------------------------------------------------
    # 매수
    # ------------------------------------------------------------------
    def check_buy(self, code: str, name: str) -> Optional[Dict]:
        if te is None:
            self.logger.warning("tail_engine 미탑재 → 매수 체크 스킵")
            return None
        # 대형 주도주 등 하락매수 제외 종목 차단 (DIP_BUY_EXCLUDE_CODES 비면 무효)
        if self.is_dip_buy_excluded(code):
            self.logger.info("🔍 [탈락-대형주제외] %s %s: DIP_BUY_EXCLUDE_CODES", name, code)
            return None
        try:
            if get_env_bool("FORCE_BUY_TEST", False):
                return self._force_buy_test(code, name)

            min_len = get_env_int("MIN_CANDLE_LEN_TAIL", 14)
            candles_raw = self.ws.get_candles(code, self.candle_tf, n=50)
            if len(candles_raw) < min_len:
                return None
            candles = [self._norm_candle(c) for c in candles_raw]
            if len(candles) < 10:
                return None

            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("SHORT")
                ])
            except Exception:
                daily_cnt = 0
            state = {"last_exit_dt": last_exit_dt, "daily_cnt": daily_cnt}

            params = dict(self._engine_params or {})
            params["_whipsaw_ws"] = self.ws
            params["_whipsaw_code"] = code
            params["_orderbook_ws"] = self.ws
            params["_orderbook_code"] = code
            params["_program_ws"] = self.ws
            params["_program_code"] = code
            params["slot_money"] = self.slot_money
            reject, msg, sig = te.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

            if code in self._pending_limit_orders:
                return None

            params = self._engine_params or {}
            eng = params if params else te.get_tail_defaults_from_db(self.db)
            atr_period = int(eng.get("atr_period", 14))
            atr_series = te.compute_atr_series(candles, atr_period)

            if is_limit_atr_entry(short_entry_mode(eng)):
                if len(candles) < 2:
                    return None
                sig_i = len(candles) - 2
                sig_bar = candles[sig_i]
                lp_cfg = tail_limit_params(eng)
                anchor_px = resolve_limit_anchor_price(
                    lp_cfg["anchor"], sig_bar, candles, sig_i,
                )
                atr_val = atr_series[sig_i] if sig_i < len(atr_series) else None
                limit_px = compute_atr_limit_price(
                    anchor_px, atr_val, lp_cfg["mult"], min_price=self.min_price,
                )
                limit_int = floor_limit_price_krw(limit_px)
                if limit_int <= 0:
                    return None
                stop_price, target_price = te.compute_tail_atr_prices(
                    float(limit_int), float(atr_val or limit_int * 0.01), eng,
                )
                valid_until = limit_valid_until_bar_key(
                    candles, sig_i, lp_cfg["valid_bars"],
                )
                hard_cap = get_env_int("SHORT_MAX_BUY_AMOUNT", 0) \
                    or get_env_int("TAIL_MAX_BUY_AMOUNT", 0) \
                    or get_env_int("MAX_BUY_AMOUNT_PER_STOCK", 0)
                qty, rej = self._resolve_buy_qty_live(
                    float(limit_int), hard_cap=hard_cap,
                )
                if rej:
                    self.logger.info(
                        "🔍 [탈락-%s] %s(%s) limit=%s",
                        rej, name, code, f"{limit_int:,}",
                    )
                    return None
                self.logger.info(
                    "🎯 [SHORT 지정가] %s(%s) limit=%s원 유효~%s qty=%d",
                    name, code, f"{limit_int:,}", valid_until, qty,
                )
                return {
                    "code": code,
                    "name": name,
                    "price": float(limit_int),
                    "qty": qty,
                    "use_limit_buy": True,
                    "valid_until_bar_key": valid_until,
                    "signal_bar_key": str(sig_bar.get("candle_time") or "")[:12],
                    "stop_price": stop_price,
                    "target_price": target_price,
                    "atr_entry": float(atr_val or 0),
                    "session_low": float(limit_int),
                    "max_price": float(limit_int),
                    "size_class": "",
                    "entry_features": {
                        "rsi": sig.get("rsi_val", 50),
                        "tail_length_pct": sig.get("tail_pct", 0) * 100,
                        "entry_mode": "limit_atr",
                    },
                }

            # align — 다음 3분봉 시가 시장가
            align_on = get_env_bool("SHORT_LIVE_BACKTEST_ALIGN", True)
            entry_open = float(sig.get("entry_price", 0) or 0)
            if align_on and entry_open > 0:
                curr_price = entry_open
            else:
                curr_price = float(candles[-1]["close"])
                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 or curr_price < self.min_price:
                return None

            hard_cap = get_env_int("SHORT_MAX_BUY_AMOUNT", 0) \
                or get_env_int("TAIL_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.stop_loss_pct)
            target_price = curr_price * (1 + self.take_profit_pct)
            atr_entry = 0.0
            try:
                atr_val = atr_series[-1] if atr_series else None
                if atr_val is not None and float(atr_val) > 0:
                    atr_entry = float(atr_val)
                    stop_price, target_price = te.compute_tail_atr_prices(
                        curr_price, atr_entry, eng,
                    )
                    self.logger.info(
                        "📊 [SHORT ATR] %s(%s) ATR=%.0f 손절=%.0f 목표=%.0f",
                        name, code, atr_entry, stop_price, target_price,
                    )
            except Exception as e:
                self.logger.debug("SHORT ATR 손절/목표 계산 스킵(%s): %s", code, e)
                if atr_entry <= 0:
                    atr_entry = curr_price * 0.01

            self.logger.info(
                "🎯 [SHORT 시그널] %s(%s) price=%.0f qty=%d pat=%s tail=%.2f rec=%.0f%% RSI=%.1f",
                name, code, curr_price, qty,
                sig.get("pattern", "hammer"),
                sig.get("tail_ratio", 0), sig.get("recovery_pos", 0) * 100, sig.get("rsi_val", 0),
            )
            return {
                "code": code,
                "name": name,
                "price": curr_price,
                "qty": qty,
                "stop_price": stop_price,
                "target_price": target_price,
                "atr_entry": atr_entry if atr_entry > 0 else float(sig.get("atr_calc_val") or 0.0),
                "session_low": curr_price,
                "max_price": curr_price,
                "size_class": "",
                "entry_features": {
                    "rsi": sig.get("rsi_val", 50),
                    "tail_length_pct": sig.get("tail_pct", 0) * 100,
                    "pattern": sig.get("pattern", "hammer"),
                },
            }
        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.stop_loss_pct),
            "target_price": px * (1 + self.take_profit_pct),
            "atr_entry": 0.0,
            "size_class": "",
            "entry_features": {},
        }

    # ------------------------------------------------------------------
    # 매도
    # ------------------------------------------------------------------
    def check_sell_signals(self) -> List[Dict]:
        if not self.holdings or te is None:
            return []

        now = dt.now()
        is_eod = is_live_eod_now(
            getattr(self, "eod_enabled", True),
            getattr(self, "eod_hm", "15:25"),
            now,
            default_hm="15:25",
        )
        try:
            params = te.get_tail_defaults_from_db(self.db)
        except Exception:
            params = {}

        signals: List[Dict] = []
        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))
                if qty <= 0 or buy_price <= 0:
                    continue

                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 = float(holding.get("max_price") or buy_price)
                if current_price > max_price:
                    max_price = current_price
                    holding["max_price"] = max_price

                session_low = float(holding.get("session_low") or buy_price)
                if current_price < session_low:
                    session_low = current_price
                    holding["session_low"] = session_low

                position = {
                    "entry_price": buy_price,
                    "entry_time": holding.get("buy_time", ""),
                    "qty": qty,
                    "stop": float(holding.get("stop_price", buy_price * (1 + self.stop_loss_pct))),
                    "target": float(holding.get("target_price", buy_price * (1 + self.take_profit_pct))),
                    "max_price": max_price,
                }
                candle = {
                    "high": max_price,
                    "low": session_low,
                    "close": current_price,
                    "candle_time": now.strftime("%Y%m%d%H%M"),
                }
                res = te.check_sell_signal_live(position, candle, params, is_eod=is_eod)
                if not res:
                    continue
                reason, exit_price = res
                profit_pct = (current_price - buy_price) / buy_price if buy_price > 0 else 0
                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)),
        }
