"""
Grid bot runtime: adaptive price bounds + waterfall (cascade) protection.

Called from TradingExecutor before each grid bot on_bar so existing strategies
benefit without re-saving script code. Scripts should read upperPrice/lowerPrice
via ctx.param() each bar (not only in on_init).
"""
from __future__ import annotations

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

import numpy as np
import pandas as pd

from app.utils.logger import get_logger

logger = get_logger(__name__)

_DEFAULTS: Dict[str, Any] = {
    "adaptiveBounds": True,
    "adaptiveLookback": 48,
    "adaptiveAtrPeriod": 14,
    "adaptiveAtrMult": 2.0,
    "adaptiveMinWidthPct": 0.02,
    "adaptiveMaxShiftPct": 0.08,
    "adaptiveEdgePct": 0.12,
    "waterfallProtection": True,
    "waterfallDropPct": 0.03,
    "waterfallWindowBars": 6,
    "waterfallWindowSec": 300,
    "waterfallCooldownBars": 12,
    "waterfallCooldownSec": 900,
    "waterfallCloseOnTrigger": False,
}


def _truthy(v: Any, default: bool = False) -> bool:
    if v is None:
        return default
    if isinstance(v, bool):
        return v
    if isinstance(v, (int, float)):
        return v != 0
    s = str(v).strip().lower()
    if s in ("1", "true", "yes", "on"):
        return True
    if s in ("0", "false", "no", "off", ""):
        return False
    return default


def _float(v: Any, default: float = 0.0) -> float:
    try:
        return float(v)
    except Exception:
        return float(default)


def _int(v: Any, default: int = 0) -> int:
    try:
        return int(v)
    except Exception:
        return int(default)


def _apply_defaults(params: Dict[str, Any]) -> None:
    for k, dv in _DEFAULTS.items():
        if k not in params or params[k] is None or params[k] == "":
            params[k] = dv


def _compute_atr(bars_df: Optional[pd.DataFrame], period: int) -> float:
    if bars_df is None or len(bars_df) < 2:
        return 0.0
    try:
        df = bars_df.tail(max(period + 2, 20))
        high = pd.to_numeric(df["high"], errors="coerce")
        low = pd.to_numeric(df["low"], errors="coerce")
        close = pd.to_numeric(df["close"], errors="coerce")
        prev_close = close.shift(1)
        tr = pd.concat(
            [(high - low).abs(), (high - prev_close).abs(), (low - prev_close).abs()],
            axis=1,
        ).max(axis=1)
        atr = tr.rolling(period, min_periods=1).mean().iloc[-1]
        return float(atr) if atr == atr else 0.0
    except Exception:
        return 0.0


def _blend_bounds(
    old_lo: float,
    old_hi: float,
    new_lo: float,
    new_hi: float,
    max_shift_pct: float,
) -> tuple[float, float]:
    if old_lo <= 0 or old_hi <= 0 or old_hi <= old_lo:
        return new_lo, new_hi
    mid_old = (old_lo + old_hi) * 0.5
    mid_new = (new_lo + new_hi) * 0.5
    half_old = (old_hi - old_lo) * 0.5
    half_new = (new_hi - new_lo) * 0.5
    if mid_old > 0:
        shift = abs(mid_new - mid_old) / mid_old
        if shift > max_shift_pct:
            mid_new = mid_old + (mid_new - mid_old) * (max_shift_pct / max(shift, 1e-9))
    half = half_new * 0.5 + half_old * 0.5
    return mid_new - half, mid_new + half


def update_adaptive_bounds(
    params: Dict[str, Any],
    price: float,
    bars_df: Optional[pd.DataFrame],
    *,
    force: bool = False,
) -> bool:
    """Adjust upperPrice/lowerPrice around price using ATR/range. Returns True if changed."""
    if not _truthy(params.get("adaptiveBounds"), True):
        return False
    if price <= 0:
        return False

    upper = _float(params.get("upperPrice") or params.get("upper_price"), 0.0)
    lower = _float(params.get("lowerPrice") or params.get("lower_price"), 0.0)
    lookback = max(10, _int(params.get("adaptiveLookback"), 48))
    atr_period = max(2, _int(params.get("adaptiveAtrPeriod"), 14))
    atr_mult = max(0.5, _float(params.get("adaptiveAtrMult"), 2.0))
    min_width_pct = max(0.005, _float(params.get("adaptiveMinWidthPct"), 0.02))
    max_shift_pct = max(0.01, _float(params.get("adaptiveMaxShiftPct"), 0.08))
    edge_pct = max(0.05, min(0.45, _float(params.get("adaptiveEdgePct"), 0.12)))

    atr = _compute_atr(bars_df, atr_period)
    if atr <= 0 and bars_df is not None and len(bars_df) > 0:
        try:
            tail = bars_df.tail(lookback)
            hi = float(pd.to_numeric(tail["high"], errors="coerce").max())
            lo = float(pd.to_numeric(tail["low"], errors="coerce").min())
            atr = max((hi - lo) / max(lookback, 1), price * min_width_pct * 0.5)
        except Exception:
            atr = price * min_width_pct

    half = max(price * min_width_pct * 0.5, atr * atr_mult)
    target_lo = price - half
    target_hi = price + half

    need_recenter = force
    if not need_recenter and upper > lower > 0:
        width = upper - lower
        pos = (price - lower) / width if width > 0 else 0.5
        if pos < edge_pct or pos > (1.0 - edge_pct):
            need_recenter = True
        if price < lower or price > upper:
            need_recenter = True
    else:
        need_recenter = True

    if not need_recenter:
        return False

    if upper > lower > 0:
        new_lo, new_hi = _blend_bounds(lower, upper, target_lo, target_hi, max_shift_pct)
    else:
        new_lo, new_hi = target_lo, target_hi

    if new_hi <= new_lo:
        return False

    params["lowerPrice"] = round(new_lo, 8)
    params["upperPrice"] = round(new_hi, 8)
    params["adaptive_last_center"] = round(price, 8)
    return True


def update_waterfall_state(
    params: Dict[str, Any],
    price: float,
    high: float,
    *,
    is_closed_bar: bool,
    now_ts: Optional[int] = None,
) -> bool:
    """
    Track cascade moves; set waterfall_pause until cooldown elapses.
    Returns True if waterfall just triggered this call.
    """
    if not _truthy(params.get("waterfallProtection"), True):
        params["waterfall_pause"] = False
        return False
    if price <= 0:
        return False

    now = int(now_ts or time.time())
    until_ts = _int(params.get("waterfall_until_ts"), 0)
    if until_ts > now:
        params["waterfall_pause"] = True
        return False
    if until_ts > 0 and until_ts <= now:
        params["waterfall_pause"] = False
        params["waterfall_until_ts"] = 0
        params["waterfall_peak_price"] = 0.0
        params.pop("waterfall_triggered_ts", None)

    drop_pct = max(0.005, _float(params.get("waterfallDropPct"), 0.03))
    peak = _float(params.get("waterfall_peak_price"), 0.0)
    ref_high = max(high, price, peak)

    if is_closed_bar:
        window = max(2, _int(params.get("waterfallWindowBars"), 6))
        params["waterfall_peak_price"] = max(peak, ref_high)
        peak = _float(params.get("waterfall_peak_price"), ref_high)
        params["waterfall_bar_counter"] = _int(params.get("waterfall_bar_counter"), 0) + 1
        if _int(params.get("waterfall_bar_counter"), 0) >= window:
            params["waterfall_bar_counter"] = 0
            params["waterfall_peak_price"] = ref_high
            peak = ref_high
    else:
        window_sec = max(30, _int(params.get("waterfallWindowSec"), 300))
        last_reset = _int(params.get("waterfall_peak_reset_ts"), 0)
        if last_reset <= 0 or (now - last_reset) >= window_sec:
            params["waterfall_peak_reset_ts"] = now
            params["waterfall_peak_price"] = ref_high
            peak = ref_high
        else:
            params["waterfall_peak_price"] = max(peak, ref_high)
            peak = _float(params.get("waterfall_peak_price"), ref_high)

    bar_index = _int(params.get("waterfall_bar_index"), 0)
    triggered = False
    if peak > 0 and price <= peak * (1.0 - drop_pct):
        triggered = True
        if is_closed_bar:
            cooldown = max(1, _int(params.get("waterfallCooldownBars"), 12))
            params["waterfall_until_bar"] = bar_index + cooldown
            params["waterfall_pause"] = True
        else:
            cooldown_sec = max(60, _int(params.get("waterfallCooldownSec"), 900))
            params["waterfall_until_ts"] = now + cooldown_sec
            params["waterfall_pause"] = True
        params["waterfall_triggered_ts"] = now
        logger.info(
            "Grid waterfall triggered: drop>=%.2f%% peak=%.6f price=%.6f pause_until=%s",
            drop_pct * 100,
            peak,
            price,
            params.get("waterfall_until_ts") or params.get("waterfall_until_bar"),
        )

    if is_closed_bar:
        bar_index += 1
        params["waterfall_bar_index"] = bar_index
        until_bar = _int(params.get("waterfall_until_bar"), 0)
        if until_bar > 0 and bar_index >= until_bar:
            params["waterfall_pause"] = False
            params["waterfall_until_bar"] = 0
            params["waterfall_peak_price"] = 0.0

    return triggered


def prepare_grid_runtime(
    params: Dict[str, Any],
    *,
    price: float,
    high: float,
    low: float,
    bars_df: Optional[pd.DataFrame] = None,
    is_closed_bar: bool = False,
    now_ts: Optional[int] = None,
) -> Dict[str, Any]:
    """Merge defaults and update adaptive bounds + waterfall state in-place."""
    return prepare_bot_market_guards(
        "grid",
        params,
        price=price,
        high=high,
        low=low,
        bars_df=bars_df,
        is_closed_bar=is_closed_bar,
        now_ts=now_ts,
    )


def prepare_bot_market_guards(
    bot_type: str,
    params: Dict[str, Any],
    *,
    price: float,
    high: float,
    low: float,
    bars_df: Optional[pd.DataFrame] = None,
    is_closed_bar: bool = False,
    now_ts: Optional[int] = None,
) -> Dict[str, Any]:
    """Grid: adaptive bounds + waterfall; Martingale: waterfall only."""
    if not isinstance(params, dict):
        return {}
    bt = (bot_type or "").strip().lower()
    if bt not in ("grid", "martingale"):
        return params

    _apply_defaults(params)
    px = float(price or 0.0)
    hi = float(high or px)
    lo = float(low or px)
    if px <= 0:
        return params

    if bt == "grid":
        changed = update_adaptive_bounds(
            params, px, bars_df, force=(is_closed_bar and _float(params.get("upperPrice"), 0) <= 0)
        )
        if changed:
            logger.debug(
                "Grid adaptive bounds: lower=%s upper=%s price=%s",
                params.get("lowerPrice"),
                params.get("upperPrice"),
                px,
            )

    wf = update_waterfall_state(params, px, hi, is_closed_bar=is_closed_bar, now_ts=now_ts)
    if wf and _truthy(params.get("waterfallCloseOnTrigger"), False):
        params["waterfall_request_close"] = True

    return params


_ENTRY_SIGNALS = frozenset({"open_long", "open_short", "add_long", "add_short"})


def filter_grid_signals_under_waterfall(
    signals: List[Dict[str, Any]],
    params: Dict[str, Any],
) -> List[Dict[str, Any]]:
    """Drop new grid entries while waterfall_pause; allow close/reduce."""
    if not signals:
        return signals
    if not _truthy((params or {}).get("waterfall_pause"), False):
        return signals
    out = []
    for s in signals:
        st = str((s or {}).get("type") or "").strip().lower()
        if st in _ENTRY_SIGNALS:
            continue
        out.append(s)
    return out


def inject_waterfall_close_signal(
    signals: List[Dict[str, Any]],
    params: Dict[str, Any],
    *,
    has_long: bool,
    has_short: bool,
    price: float,
    timestamp: int,
) -> List[Dict[str, Any]]:
    if not _truthy((params or {}).get("waterfall_request_close"), False):
        return signals
    params["waterfall_request_close"] = False
    out = list(signals or [])
    if has_long and not any((s or {}).get("type") == "close_long" for s in out):
        out.insert(
            0,
            {
                "type": "close_long",
                "trigger_price": float(price or 0),
                "position_size": 0,
                "timestamp": int(timestamp or 0),
                "reason": "grid_waterfall_close",
            },
        )
    if has_short and not any((s or {}).get("type") == "close_short" for s in out):
        out.insert(
            0,
            {
                "type": "close_short",
                "trigger_price": float(price or 0),
                "position_size": 0,
                "timestamp": int(timestamp or 0),
                "reason": "grid_waterfall_close",
            },
        )
    return out
