"""
kis_trader/engine/orderbook_filter.py — TRIGGER 진입 호가 필터 (키움 0D 캐시)
============================================================================
실매 전용 — ``params['_orderbook_ws']`` / ``_whipsaw_ws`` 의 ``get_orderbook`` 사용.
백테는 호가 없으면 스킵(OFF) 또는 env 로 비활성.

모든 임계값 env/DB — 하드코딩 금지.
"""
from __future__ import annotations

from typing import Any, Dict, Optional, Tuple

from kis_trader.utils.env import get_env_bool, get_env_float, get_env_from_db, get_env_int
from kis_trader.ws.orderbook_cache import OrderbookSnapshot
from kis_trader.ws.trigger_eval_recorder import get_trigger_eval_recorder

LOG_BACKFILL_SOURCE = "log_backfill"


def _strategy_prefix(strategy: str) -> str:
    s = (strategy or "").strip().upper()
    if s in ("BREAKOUT", "BO"):
        return "BREAKOUT"
    if s in ("MOMENTUM", "MOM"):
        return "MOMENTUM"
    if s in ("TAIL", "SHORT", "TAIL_CATCH"):
        return "TAIL"
    return s or "ORDERBOOK"


def orderbook_filter_enabled(strategy: str) -> bool:
    prefix = _strategy_prefix(strategy)
    sk = f"{prefix}_ORDERBOOK_FILTER_ENABLED"
    raw = get_env_from_db(sk, "")
    if str(raw).strip() != "":
        return get_env_bool(sk, True)
    return get_env_bool("ORDERBOOK_FILTER_ENABLED", True)


def _orderbook_filter_enabled_for_entry(params: Dict[str, Any], strategy: str) -> bool:
    """백테 폼값(임시 오버라이드) 우선 → 없으면 DB(실매와 동일).

    백테 탭에서 켜고/끄고 돌릴 때만 ``params['_orderbook_filter_enabled']`` 가
    채워지며(그 1회 백테에만 적용), 실매 루프는 이 키가 없어 DB 값만 읽는다.
    """
    ov = params.get("_orderbook_filter_enabled")
    if ov is not None:
        return bool(ov)
    return orderbook_filter_enabled(strategy)


def _f(common: str, specific: str, default: float) -> float:
    if str(get_env_from_db(specific, "")).strip():
        return get_env_float(specific, default)
    return get_env_float(common, default)


def _i(common: str, specific: str, default: int) -> int:
    sk = specific
    if str(get_env_from_db(sk, "")).strip():
        return get_env_int(sk, default)
    return get_env_int(common, default)


def _load_filter_params(strategy: str, params: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
    pfx = _strategy_prefix(strategy)
    cfg = {
        "max_spread_pct": _f("ORDERBOOK_MAX_SPREAD_PCT", f"{pfx}_ORDERBOOK_MAX_SPREAD_PCT", 0.45),
        "entry_bid_levels": _i("ORDERBOOK_ENTRY_BID_LEVELS", f"{pfx}_ORDERBOOK_ENTRY_BID_LEVELS", 3),
        "entry_bid_depth_mult": _f(
            "ORDERBOOK_ENTRY_BID_DEPTH_MULT", f"{pfx}_ORDERBOOK_ENTRY_BID_DEPTH_MULT", 1.2,
        ),
        "entry_ask_max_mult": _f(
            "ORDERBOOK_ENTRY_ASK_MAX_MULT", f"{pfx}_ORDERBOOK_ENTRY_ASK_MAX_MULT", 3.0,
        ),
        "min_bid_ask_ratio": _f(
            "ORDERBOOK_MIN_BID_ASK_RATIO", f"{pfx}_ORDERBOOK_MIN_BID_ASK_RATIO", 0.85,
        ),
        "breakout_ask_wall_max_qty": _i(
            "ORDERBOOK_BREAKOUT_ASK_WALL_MAX_QTY",
            f"{pfx}_ORDERBOOK_ASK_WALL_MAX_QTY",
            5000,
        ),
    }
    # 백테/파람서치 per-run 오버라이드 — 본체(kiwoom_0d) 재계산 시에만 의미.
    # 웹 입력칸·그리드 호가필터 축이 이 키들을 채워 임계값을 바꾼다.
    # (실매 루프는 이 키가 없어 env/DB 값 그대로 사용)
    #   _ob_max_spread_pct     → 스프레드 상한(%)            (cfg.max_spread_pct)
    #   _ob_min_bid_ask_ratio  → 매수/매도 잔량비 하한        (cfg.min_bid_ask_ratio)
    #   _ob_ask_max_mult       → 매도벽 허용배수(클수록 관대) (cfg.entry_ask_max_mult)
    if params:
        _overrides = (
            ("_ob_max_spread_pct", "max_spread_pct"),
            ("_ob_min_bid_ask_ratio", "min_bid_ask_ratio"),
            ("_ob_ask_max_mult", "entry_ask_max_mult"),
            ("_ob_ask_wall_max_qty", "breakout_ask_wall_max_qty"),  # 돌파 저항 근처 매도벽 수량
        )
        for src_key, cfg_key in _overrides:
            ov = params.get(src_key)
            if ov is not None and str(ov).strip() != "":
                try:
                    cfg[cfg_key] = float(ov)
                except (TypeError, ValueError):
                    pass
    return cfg


def _resolve_ws(params: Dict[str, Any]):
    return params.get("_orderbook_ws") or params.get("_whipsaw_ws")


def _resolve_code(params: Dict[str, Any]) -> str:
    return str(params.get("_orderbook_code") or params.get("_whipsaw_code") or "").strip()


def _fetch_snapshot(params: Dict[str, Any]) -> Optional[OrderbookSnapshot]:
    bt = params.get("_backtest_orderbook_snapshot")
    if isinstance(bt, OrderbookSnapshot):
        return bt

    ws = _resolve_ws(params)
    code = _resolve_code(params)
    if not ws or not code:
        return None
    getter = getattr(ws, "get_orderbook_snapshot", None)
    if callable(getter):
        try:
            snap = getter(code)
            if snap is not None:
                return snap
        except Exception:
            pass
    getter2 = getattr(ws, "get_orderbook", None)
    if not callable(getter2):
        return None
    try:
        raw = getter2(code)
    except Exception:
        return None
    if raw is None:
        return None
    if isinstance(raw, OrderbookSnapshot):
        return raw
    return None


def _estimate_entry_qty(params: Dict[str, Any], price: float) -> int:
    slot = float(params.get("slot_money") or params.get("_slot_money") or 0)
    if slot <= 0:
        slot = float(get_env_int("SLOT_MONEY_DEFAULT", 200_000))
    if price <= 0:
        return 0
    return max(1, int(slot / price))


def _persist_orderbook_eval(
    params: Dict[str, Any],
    strategy: str,
    snap: OrderbookSnapshot,
    reject_code: Optional[str],
    reject_msg: Optional[str],
    current_price: float,
) -> None:
    """실매 필터 판정 직후 RAM 스냅샷을 DB에 저장 (백테 재생용)."""
    if params.get("_backtest_orderbook_snapshot") is not None:
        return
    if params.get("_skip_trigger_eval_persist"):
        return
    rec = get_trigger_eval_recorder()
    if rec is None:
        return
    code = _resolve_code(params) or (snap.code or "")
    rec.on_orderbook_eval(
        strategy=strategy,
        code=code,
        snap=snap,
        reject_code=reject_code,
        reject_msg=reject_msg,
        current_price=current_price,
    )


def orderbook_reject_for_entry(
    params: Dict[str, Any],
    strategy: str,
    *,
    current_price: float,
    resistance: Optional[float] = None,
) -> Tuple[Optional[str], Optional[str]]:
    """
    TRIGGER 통과 직전 호가 검사.
    Returns: (reject_code, message) — 통과 시 (None, None)
    """
    if not _orderbook_filter_enabled_for_entry(params, strategy):
        return (None, None)

    # log_backfill 판정 재생 — 호가 본체 없이 실매와 동일 pass/reject
    log_verdict = params.get("_backtest_log_orderbook_verdict")
    bt_snap = params.get("_backtest_orderbook_snapshot")
    if log_verdict is not None and (
        bt_snap is None or str(getattr(bt_snap, "source", "") or "") == LOG_BACKFILL_SOURCE
    ):
        rej_code, rej_msg = log_verdict
        if rej_code:
            return (rej_code, rej_msg or "")
        return (None, None)

    snap = _fetch_snapshot(params)
    if snap is None:
        # 실매 WS 미구독·만료 — 필터 스킵 (REST 부하 없음)
        return (None, None)

    cfg = _load_filter_params(strategy, params)
    pfx = _strategy_prefix(strategy)

    spread = snap.spread_pct()
    max_spread = float(cfg["max_spread_pct"])
    if spread > max_spread:
        rej = (
            "탈락-호가스프레드",
            f"스프레드 {spread:.2f}% > {max_spread:.2f}%",
        )
        _persist_orderbook_eval(params, strategy, snap, rej[0], rej[1], current_price)
        return rej

    bid_lv = int(cfg["entry_bid_levels"])
    bid_sum = snap.bid_qty_sum(bid_lv)
    ask_sum = snap.ask_qty_sum(bid_lv)
    if ask_sum > 0:
        ratio = bid_sum / ask_sum
        min_ratio = float(cfg["min_bid_ask_ratio"])
        if ratio < min_ratio:
            rej = (
                "탈락-호가수급",
                f"매수/매도잔량({bid_lv}호가) {ratio:.2f} < {min_ratio:.2f}",
            )
            _persist_orderbook_eval(params, strategy, snap, rej[0], rej[1], current_price)
            return rej

    qty_need = _estimate_entry_qty(params, current_price)
    depth_mult = float(cfg["entry_bid_depth_mult"])
    if qty_need > 0 and bid_sum < int(qty_need * depth_mult):
        rej = (
            "탈락-매수호가얇음",
            f"매수{bid_lv}호가 합 {bid_sum}주 < 필요 {int(qty_need * depth_mult)}주",
        )
        _persist_orderbook_eval(params, strategy, snap, rej[0], rej[1], current_price)
        return rej

    ask_max_mult = float(cfg["entry_ask_max_mult"])
    if qty_need > 0 and ask_sum > int(qty_need * ask_max_mult):
        rej = (
            "탈락-매도벽",
            f"매도{bid_lv}호가 합 {ask_sum}주 > 허용 {int(qty_need * ask_max_mult)}주",
        )
        _persist_orderbook_eval(params, strategy, snap, rej[0], rej[1], current_price)
        return rej

    if pfx == "BREAKOUT" and resistance and resistance > 0:
        wall_max = int(cfg["breakout_ask_wall_max_qty"])
        ask_above = sum(
            lv.qty for lv in snap.asks
            if lv.price > 0 and lv.price <= resistance * 1.002
        )
        if ask_above > wall_max:
            rej = (
                "탈락-돌파매도벽",
                f"저항 {resistance:,.0f} 근처 매도잔량 {ask_above} > {wall_max}",
            )
            _persist_orderbook_eval(params, strategy, snap, rej[0], rej[1], current_price)
            return rej

    if pfx == "TAIL" and snap.total_bid_qty > 0 and snap.total_ask_qty > 0:
        tail_ratio = snap.total_bid_qty / snap.total_ask_qty
        min_tail = float(cfg["min_bid_ask_ratio"])
        if tail_ratio < min_tail:
            rej = (
                "탈락-꼬리지지부족",
                f"총매수/총매도 {tail_ratio:.2f} < {min_tail:.2f}",
            )
            _persist_orderbook_eval(params, strategy, snap, rej[0], rej[1], current_price)
            return rej

    _persist_orderbook_eval(params, strategy, snap, None, None, current_price)
    return (None, None)
