"""
kis_trader/engine/whipsaw_filter.py — 틱 30초봉 + 휩쏘(TRIGGER) 필터
====================================================================
실매 · 백테 · 파라서치 **동일 TRIGGER** — ``_eval_*`` / ``breakout_scan_buy_at_bar`` 공통.

[패턴]
- resistance_fake (돌파): 저항 터치 → 저항 아래 이탈 → 회복 = 가짜돌파
- support_shake  (모멘텀/꼬리): 지지(신호봉 저가) 이탈 → 재회복 = 세력 흔들기

[틱 소스 우선순위]
1) params[``_whipsaw_ticks``] — 백테 ws_ticks (포트폴리오가 주입)
2) params[``_whipsaw_ws``].get_recent_ticks — 실매 RAM
3) OHLC 폴백 — ``WHIPSAW_BACKTEST_OHLC_FALLBACK=true`` (틱 없을 때 1분봉 근사)

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

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

from kis_trader.utils.env import get_env_bool, get_env_float, get_env_from_db, get_env_int


def whipsaw_backtest_ohlc_fallback() -> bool:
    """틱 없을 때 신호봉 OHLC로 휩쏘 근사 (백테·파라서치 기본 ON)."""
    return get_env_bool("WHIPSAW_BACKTEST_OHLC_FALLBACK", True)


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 "WHIPSAW"


def whipsaw_filter_enabled(strategy: str) -> bool:
    """전략별 ON/OFF — {STRATEGY}_WHIPSAW_FILTER_ENABLED, 없으면 WHIPSAW_FILTER_ENABLED."""
    prefix = _strategy_prefix(strategy)
    strat_key = f"{prefix}_WHIPSAW_FILTER_ENABLED"
    raw = get_env_from_db(strat_key, "")
    if str(raw).strip() != "":
        return get_env_bool(strat_key, True)
    return get_env_bool("WHIPSAW_FILTER_ENABLED", True)


# 전략별 휩쏘 파라미터 캐시 — (env세대, cfg). env 는 프로세스당 병합캐시라
# 후보/조합마다 재계산(get_env_from_db 수백만 회)하던 것을 세대당 1회로 줄인다.
# 설정 저장(invalidate_merged_env_cache) 시 세대가 바뀌어 자동 재계산 → 값 불변.
_whipsaw_params_cache: Dict[str, Any] = {}


def load_whipsaw_params(strategy: str) -> Dict[str, Any]:
    """전략별 휩쏘 파라미터 — 공통 WHIPSAW_* + {PREFIX}_WHIPSAW_* 오버라이드.

    ※ env 세대 단위 캐시. 동일 세대 내에서는 계산 결과가 항상 같으므로 재사용해도
      결과가 완전히 동일하다(백테/파라서치 핫패스 가속). 캐시된 dict 는 호출부에서
      읽기 전용으로만 쓰인다.
    """
    from kis_trader.utils.env import env_cache_generation
    _gen = env_cache_generation()
    _cached = _whipsaw_params_cache.get(strategy)
    if _cached is not None and _cached[0] == _gen:
        return _cached[1]

    prefix = _strategy_prefix(strategy)

    def _f(common: str, specific: str, default: float) -> float:
        sk = f"{prefix}_{specific}"
        if str(get_env_from_db(sk, "")).strip():
            return get_env_float(sk, default)
        return get_env_float(common, default)

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

    mode_raw = ""
    try:
        from kis_trader.utils.env import get_env_from_db
        mode_raw = str(
            get_env_from_db(f"{prefix}_WHIPSAW_MODE", "")
            or get_env_from_db("WHIPSAW_MODE", "")
            or "",
        ).strip().lower()
    except Exception:
        pass
    if not mode_raw:
        mode_raw = "resistance_fake" if prefix == "BREAKOUT" else "support_shake"

    cfg = {
        "enabled": whipsaw_filter_enabled(strategy),
        "mode": mode_raw,
        "subbar_sec": _i("WHIPSAW_SUBBAR_SEC", "WHIPSAW_SUBBAR_SEC", 30),
        "lookback_sec": _i("WHIPSAW_LOOKBACK_SEC", "WHIPSAW_LOOKBACK_SEC", 90),
        "dip_pct": _f("WHIPSAW_DIP_PCT", "WHIPSAW_DIP_PCT", 0.003),
        "recovery_tol_pct": _f("WHIPSAW_RECOVERY_TOL_PCT", "WHIPSAW_RECOVERY_TOL_PCT", 0.001),
        "min_subbars": _i("WHIPSAW_MIN_SUBBARS", "WHIPSAW_MIN_SUBBARS", 1),
        "tick_limit": _i("WHIPSAW_TICK_BUFFER_LIMIT", "WHIPSAW_TICK_BUFFER_LIMIT", 300),
    }
    _whipsaw_params_cache[strategy] = (_gen, cfg)
    return cfg


def _tick_epoch_sec(tick_time: str) -> int:
    """YYYYMMDDHHMMSS → epoch 초 (로컬 naive)."""
    raw = (tick_time or "").strip().replace(":", "").replace("-", "").replace(" ", "")
    if len(raw) < 14:
        if len(raw) >= 12:
            raw = raw[:12] + "00"
        else:
            return 0
    try:
        from datetime import datetime
        dt = datetime.strptime(raw[:14], "%Y%m%d%H%M%S")
        return int(dt.timestamp())
    except Exception:
        return 0


def _aggregate_subbars_columnar(view: Any, subbar_sec: int, lookback_sec: int) -> List[Dict[str, Any]]:
    """aggregate_ticks_subbars 의 컬럼 직접접근 판(백테 틱재생 전용).

    dict 재구성 없이 공유 컬럼 배열(epoch/price/volume)을 직접 읽는다. 로직·산출물은
    dict 판과 100% 동일하다(같은 cutoff·버킷·OHLCV 계산). now_sec 은 dict 판의
    ``_tick_epoch_sec(ticks[-1].tick_time)`` 과 같은 값(사전계산 epoch[마지막 인덱스]).
    """
    if subbar_sec <= 0 or len(view) == 0:
        return []
    owner = view.owner
    epoch = owner._epoch
    price = owner._price
    volume = owner._volume
    last = view.last_idx()
    now_sec = int(epoch[last]) if last >= 0 else 0
    if now_sec <= 0:
        return []
    cutoff = now_sec - max(lookback_sec, subbar_sec)

    buckets: Dict[int, Dict[str, Any]] = {}
    for i in view.iter_idx():
        ts = int(epoch[i])
        if ts <= 0 or ts < cutoff:
            continue
        px = float(price[i])
        if px <= 0:
            continue
        vol = int(volume[i])
        bucket_id = ts // subbar_sec
        b = buckets.get(bucket_id)
        if b is None:
            buckets[bucket_id] = {
                "bucket_id": bucket_id,
                "open": px,
                "high": px,
                "low": px,
                "close": px,
                "volume": vol,
                "start_sec": bucket_id * subbar_sec,
            }
        else:
            b["high"] = max(b["high"], px)
            b["low"] = min(b["low"], px)
            b["close"] = px
            b["volume"] = int(b.get("volume", 0)) + vol

    return [buckets[k] for k in sorted(buckets.keys())]


def aggregate_ticks_subbars(
    ticks: List[Dict[str, Any]],
    subbar_sec: int,
    lookback_sec: int,
) -> List[Dict[str, Any]]:
    """
    RAM 틱 → N초(기본 30초) OHLCV 서브봉 리스트 (오래된→최신).

    tick dict: price, tick_time (14자리), volume(optional)
    """
    # 백테 틱재생: 공유메모리 컬럼 뷰면 dict 재구성 없이 직접집계(실매 dict 경로 무변경).
    try:
        from kis_trader.backtest.shared_ticks import TickColumnView
        if isinstance(ticks, TickColumnView):
            return _aggregate_subbars_columnar(ticks, subbar_sec, lookback_sec)
    except Exception:
        pass
    if not ticks or subbar_sec <= 0:
        return []
    now_sec = _tick_epoch_sec(str(ticks[-1].get("tick_time") or ""))
    if now_sec <= 0:
        return []
    cutoff = now_sec - max(lookback_sec, subbar_sec)

    buckets: Dict[int, Dict[str, Any]] = {}
    for t in ticks:
        ts = _tick_epoch_sec(str(t.get("tick_time") or ""))
        if ts <= 0 or ts < cutoff:
            continue
        px = float(t.get("price") or 0)
        if px <= 0:
            continue
        vol = int(t.get("volume") or 0)
        bucket_id = ts // subbar_sec
        b = buckets.get(bucket_id)
        if b is None:
            buckets[bucket_id] = {
                "bucket_id": bucket_id,
                "open": px,
                "high": px,
                "low": px,
                "close": px,
                "volume": vol,
                "start_sec": bucket_id * subbar_sec,
            }
        else:
            b["high"] = max(b["high"], px)
            b["low"] = min(b["low"], px)
            b["close"] = px
            b["volume"] = int(b.get("volume", 0)) + vol

    return [buckets[k] for k in sorted(buckets.keys())]


def eval_whipsaw_reject(
    ticks: List[Dict[str, Any]],
    *,
    strategy: str,
    anchor_low: float,
    anchor_high: float,
    current_price: float,
    params: Optional[Dict[str, Any]] = None,
) -> Tuple[Optional[str], str]:
    """
    휩쏘 감지 시 (reason, msg), 통과 시 (None, "").

    anchor_low  : 지지선 (모멘텀·꼬리)
    anchor_high : 저항선 (돌파)
    """
    cfg = dict(params or load_whipsaw_params(strategy))
    if not cfg.get("enabled", True):
        return (None, "")

    if current_price <= 0:
        return (None, "")

    subbars = aggregate_ticks_subbars(
        ticks,
        int(cfg.get("subbar_sec", 30)),
        int(cfg.get("lookback_sec", 90)),
    )
    min_bars = max(1, int(cfg.get("min_subbars", 1)))
    if len(subbars) < min_bars:
        return (None, "")

    dip_pct = float(cfg.get("dip_pct", 0.003))
    rec_tol = float(cfg.get("recovery_tol_pct", 0.001))
    mode = str(cfg.get("mode", "support_shake")).strip().lower()

    lows = [float(b["low"]) for b in subbars]
    highs = [float(b["high"]) for b in subbars]
    min_low = min(lows) if lows else 0.0
    max_high = max(highs) if highs else 0.0

    if mode == "resistance_fake":
        ref = float(anchor_high or 0)
        if ref <= 0:
            return (None, "")
        dip_line = ref * (1.0 - dip_pct)
        touched = max_high >= ref * (1.0 - rec_tol)
        dipped = min_low < dip_line
        recovered = current_price >= ref * (1.0 - rec_tol)
        if touched and dipped and recovered:
            return (
                "탈락-휩쏘-가짜돌파",
                "30s봉: 저항%.0f 터치→%.0f 이탈→현재%.0f 회복" % (ref, min_low, current_price),
            )
        return (None, "")

    # support_shake — 모멘텀·꼬리
    ref = float(anchor_low or 0)
    if ref <= 0:
        return (None, "")
    dip_line = ref * (1.0 - dip_pct)
    dipped = min_low < dip_line
    recovered = current_price >= ref * (1.0 - rec_tol)
    if dipped and recovered:
        return (
            "탈락-휩쏘-지지이탈",
            "30s봉: 지지%.0f→%.0f 이탈 후 현재%.0f (세력흔들기)" % (ref, min_low, current_price),
        )
    return (None, "")


def _minute_keys_lookback(bar_key: str, lookback_sec: int, tf_min: int) -> List[str]:
    """bar_key(12자리) 기준 lookback_sec 만큼 1분(또는 tf) 버킷 키."""
    raw = str(bar_key or "")[:12]
    if len(raw) < 12:
        return [raw] if raw else []
    try:
        dt0 = datetime.strptime(raw, "%Y%m%d%H%M")
    except ValueError:
        return [raw]
    n_min = max(1, int(lookback_sec // 60) + 2)
    keys: List[str] = []
    step = max(1, int(tf_min))
    for k in range(n_min):
        t = dt0 - timedelta(minutes=k * step)
        keys.append(t.strftime("%Y%m%d%H%M"))
    return keys


def collect_whipsaw_ticks(
    ticks_by_code: Optional[Dict[str, Dict[str, List[Dict[str, Any]]]]],
    code: str,
    bar_candle_time: str,
    lookback_sec: int,
    tf_min: int = 1,
) -> List[Dict[str, Any]]:
    """백테 ws_ticks — 신호봉 시각 기준 lookback 구간 틱 병합."""
    if not ticks_by_code:
        return []
    bucket = ticks_by_code.get(str(code).strip()) or {}
    # 로더가 `ORDER BY code, tick_time` 로 분(minute) 버킷을 이미 tick_time 오름차순
    # 정렬해 담아둔다. _minute_keys_lookback 는 최신→과거(내림차순) 키를 주므로,
    # 오름차순으로 뒤집어 이어붙이면 전체가 tick_time 오름차순이 된다.
    # → 후보마다 하던 재정렬(merged.sort)을 제거해도 결과가 완전히 동일하다.
    keys = list(reversed(_minute_keys_lookback(bar_candle_time, lookback_sec, tf_min)))
    # 백테 틱재생: 공유메모리 버킷이면 dict 리스트 대신 컬럼 뷰 반환(순서 동일).
    try:
        from kis_trader.backtest.shared_ticks import SharedBucketMapping
        if isinstance(bucket, SharedBucketMapping):
            return bucket.column_view_lookback(keys)
    except Exception:
        pass
    merged: List[Dict[str, Any]] = []
    for mk in keys:
        merged.extend(bucket.get(mk) or [])
    return merged


def eval_whipsaw_ohlc_fallback(
    signal_bar: Dict[str, Any],
    current_price: float,
    *,
    strategy: str,
    anchor_low: float,
    anchor_high: float,
    params: Optional[Dict[str, Any]] = None,
) -> Tuple[Optional[str], str]:
    """틱 없을 때 신호봉 OHLC 1봉으로 휩쏘 근사 (백테·파라서치)."""
    cfg = dict(params or load_whipsaw_params(strategy))
    if not cfg.get("enabled", True):
        return (None, "")

    hi = float(signal_bar.get("high") or current_price or 0)
    lo = float(signal_bar.get("low") or current_price or 0)
    cl = float(current_price or signal_bar.get("close") or 0)
    if cl <= 0:
        return (None, "")

    dip_pct = float(cfg.get("dip_pct", 0.003))
    rec_tol = float(cfg.get("recovery_tol_pct", 0.001))
    mode = str(cfg.get("mode", "support_shake")).strip().lower()

    if mode == "resistance_fake":
        ref = float(anchor_high or 0)
        if ref <= 0:
            return (None, "")
        if (
            hi >= ref * (1.0 - rec_tol)
            and lo < ref * (1.0 - dip_pct)
            and cl >= ref * (1.0 - rec_tol)
        ):
            return (
                "탈락-휩쏘-가짜돌파",
                "OHLC: 저항%.0f 터치→%.0f 이탈→종%.0f" % (ref, lo, cl),
            )
        return (None, "")

    ref = float(anchor_low or lo or 0)
    if ref <= 0:
        return (None, "")
    if lo < ref * (1.0 - dip_pct) and cl >= ref * (1.0 - rec_tol):
        return (
            "탈락-휩쏘-지지이탈",
            "OHLC: 지지%.0f→%.0f 이탈 후 종%.0f" % (ref, lo, cl),
        )
    return (None, "")


def whipsaw_reject_for_signal(
    params: Dict[str, Any],
    strategy: str,
    *,
    signal_bar: Dict[str, Any],
    current_price: float,
    resistance: float = 0.0,
    ws: Any = None,
    ticks: Optional[List[Dict[str, Any]]] = None,
) -> Tuple[Optional[str], str]:
    """
    실매·백테·파라서치 공통 TRIGGER 휩쏘 검사.

    params:
      _whipsaw_ticks — 백테 틱 (포트폴리오 주입)
      _whipsaw_ws    — 실매 WSManager (전략 check_buy 가 주입)
      _whipsaw_code  — 실매 종목코드
    """
    cfg = load_whipsaw_params(strategy)
    if not cfg.get("enabled", True):
        return (None, "")

    cl = float(current_price or signal_bar.get("close") or 0)
    if cl <= 0:
        return (None, "")

    anchor_low = float(signal_bar.get("low") or cl)
    anchor_high = float(resistance or signal_bar.get("high") or cl)

    # 백테 틱재생: _whipsaw_ticks 가 컬럼 뷰(TickColumnView)면 list() 로 재구성하지
    # 않고 그대로 넘긴다(그럼 eval→aggregate 가 컬럼 직접집계). 실매/일반 경로는
    # dict 리스트라 기존과 동일하게 list() 로 복제된다(무변경).
    _src = ticks or params.get("_whipsaw_ticks") or []
    try:
        from kis_trader.backtest.shared_ticks import TickColumnView
        _is_view = isinstance(_src, TickColumnView)
    except Exception:
        _is_view = False
    tick_list = _src if _is_view else list(_src)
    if len(tick_list) < 2:
        live_ws = ws if ws is not None else params.get("_whipsaw_ws")
        code = str(params.get("_whipsaw_code") or "").strip()
        if live_ws is not None and code and getattr(live_ws, "get_recent_ticks", None):
            try:
                limit = int(cfg.get("tick_limit", 300))
                tick_list = list(live_ws.get_recent_ticks(code, limit=limit) or [])
            except Exception:
                tick_list = []

    if len(tick_list) >= 2:
        return eval_whipsaw_reject(
            tick_list,
            strategy=strategy,
            anchor_low=anchor_low,
            anchor_high=anchor_high,
            current_price=cl,
            params=cfg,
        )

    if whipsaw_backtest_ohlc_fallback():
        return eval_whipsaw_ohlc_fallback(
            signal_bar, cl,
            strategy=strategy,
            anchor_low=anchor_low,
            anchor_high=anchor_high,
            params=cfg,
        )
    return (None, "")


def apply_trigger_whipsaw_filter(
    ws: Any,
    code: str,
    strategy: str,
    *,
    anchor_low: float,
    anchor_high: float,
    current_price: float,
) -> Tuple[Optional[str], str]:
    """레거시 래퍼 — whipsaw_reject_for_signal 과 동일."""
    bar = {"low": anchor_low, "high": anchor_high, "close": current_price}
    return whipsaw_reject_for_signal(
        {"_whipsaw_ws": ws, "_whipsaw_code": code},
        strategy,
        signal_bar=bar,
        current_price=current_price,
        resistance=anchor_high,
        ws=ws,
    )


def inject_whipsaw_ticks_into_params(
    params: Dict[str, Any],
    *,
    ticks_by_code: Optional[Dict[str, Dict[str, List[Dict[str, Any]]]]],
    code: str,
    bar_candle_time: str,
    strategy: str,
    tf_min: int = 1,
) -> None:
    """백테 포트폴리오 — eval 직전 params 에 ws_ticks lookback 주입."""
    cfg = load_whipsaw_params(strategy)
    ticks = collect_whipsaw_ticks(
        ticks_by_code, code, bar_candle_time,
        int(cfg.get("lookback_sec", 90)), tf_min,
    )
    if ticks:
        params["_whipsaw_ticks"] = ticks
