"""
8 Named Strategies from the orderflow wiki, implemented as deterministic
setup classifiers. Each returns Setup objects or None.

These run BEFORE any LLM call. The LLM is optional confirmation only.
"""
from __future__ import annotations
import logging
from typing import Optional

from .models import OrderflowSnapshot, MarketRegime, Setup, Side, Candle
from .orderflow import classify_regime

log = logging.getLogger(__name__)

# === Signal Alignment Constants (from wiki) ===
SPREAD_MAX_BPS = 3.0          # max spread for entry
MIN_BOOK_IMBALANCE = 0.15     # minimum book asymmetry
MIN_TAPE_DELTA = 0.15         # minimum tape direction
MIN_OMNI = 0.8                # minimum composite score for entry
RSI_LONG_ZONE = (35, 65)      # RSI not exhausted
RSI_SHORT_ZONE = (35, 65)
MIN_ATR_RISK_MULT = 1.2       # stop at least 1.2x ATR from entry
MIN_RR = 1.5                  # minimum reward:risk ratio


def _base_checks(of: OrderflowSnapshot) -> Optional[str]:
    """Return skip reason if basic conditions fail."""
    if of.spread_bps > SPREAD_MAX_BPS:
        return f"spread too wide ({of.spread_bps:.1f} bps)"
    if abs(of.book_imbalance) < 0.05:
        return "book too balanced"
    return None


def _long_aligned(of: OrderflowSnapshot) -> bool:
    """Check if signals align for a long entry."""
    checks = [
        of.book_imbalance > MIN_BOOK_IMBALANCE,     # bid-heavy book
        of.tape_delta_pct > MIN_TAPE_DELTA,          # buy aggression
        of.price_vs_vwap > -0.005,                   # near/above VWAP
        of.macd_histogram >= 0,                       # momentum ok
        RSI_LONG_ZONE[0] <= of.rsi_14 <= RSI_LONG_ZONE[1],
        of.ask_absorption < 0.6,                      # not being absorbed on ask
        of.funding_rate < 0.001,                      # not crowded long
    ]
    return sum(checks) >= 5  # 5 of 7 must align


def _short_aligned(of: OrderflowSnapshot) -> bool:
    """Check if signals align for a short entry."""
    checks = [
        of.book_imbalance < -MIN_BOOK_IMBALANCE,    # ask-heavy book
        of.tape_delta_pct < -MIN_TAPE_DELTA,         # sell aggression
        of.price_vs_vwap < 0.005,                    # near/below VWAP
        of.macd_histogram <= 0,                       # bearish momentum
        RSI_SHORT_ZONE[0] <= of.rsi_14 <= RSI_SHORT_ZONE[1],
        of.bid_absorption < 0.6,                      # not being absorbed on bid
        of.funding_rate > -0.001,                     # not crowded short
    ]
    return sum(checks) >= 5


def _compute_levels(
    of: OrderflowSnapshot, side: Side, atr: float
) -> tuple[float, float]:
    """Compute stop and target using ATR + structure. Caps R:R at 4:1 max."""
    stop_dist = max(atr * MIN_ATR_RISK_MULT, of.mid * 0.003)  # min 0.3%
    max_reward_dist = stop_dist * 4.0  # cap at 4R to avoid unrealistic targets

    if side == "long":
        stop = of.mid - stop_dist
        # Target: structural level or 2R minimum, capped at 4R
        candidates = [of.mid + stop_dist * MIN_RR]
        if of.vah > of.mid:
            candidates.append(of.vah)
        if of.vpoc > of.mid:
            candidates.append(of.vpoc)
        target = min(max(candidates), of.mid + max_reward_dist)
        if target <= of.mid:
            target = of.mid + stop_dist * 2.0
    else:
        stop = of.mid + stop_dist
        candidates = [of.mid - stop_dist * MIN_RR]
        if of.val < of.mid:
            candidates.append(of.val)
        if of.vpoc < of.mid:
            candidates.append(of.vpoc)
        target = max(min(candidates), of.mid - max_reward_dist)
        if target >= of.mid:
            target = of.mid - stop_dist * 2.0

    return stop, target


def scan_setups(
    coin: str,
    of: OrderflowSnapshot,
    regime: MarketRegime,
    candles_15m: list[Candle],
) -> list[Setup]:
    """
    Run all 8 strategy scanners. Returns list of detected setups.
    """
    skip = _base_checks(of)
    if skip:
        log.debug(f"{coin}: skip ({skip})")
        return []

    setups: list[Setup] = []

    # 1. Absorption + Reversal
    s = _check_absorption_reversal(coin, of, regime)
    if s:
        setups.append(s)

    # 2. Initiation + Breakout
    s = _check_initiation_breakout(coin, of, regime)
    if s:
        setups.append(s)

    # 3. Delta Divergence
    s = _check_delta_divergence(coin, of)
    if s:
        setups.append(s)

    # 4. Liquidity Grab
    s = _check_liquidity_grab(coin, of, candles_15m)
    if s:
        setups.append(s)

    # 5. Trend Pullback
    s = _check_trend_pullback(coin, of, regime)
    if s:
        setups.append(s)

    # 6. Iceberg Follow
    s = _check_iceberg_follow(coin, of)
    if s:
        setups.append(s)

    # 7. Liquidity Vacuum
    s = _check_liquidity_vacuum(coin, of)
    if s:
        setups.append(s)

    # 8. Exhaustion Fade
    s = _check_exhaustion_fade(coin, of)
    if s:
        setups.append(s)

    return setups


# === Strategy 1: Absorption + Reversal ===
def _check_absorption_reversal(
    coin: str, of: OrderflowSnapshot, regime: MarketRegime
) -> Optional[Setup]:
    """
    High aggressive volume but price fails to move.
    Bid absorption + sell tape + price holds = long (buyers absorbing sellers)
    Ask absorption + buy tape + price stalls = short (sellers absorbing buyers)
    """
    evidence = []

    # Long: bid absorption detected (sellers exhausted)
    if of.bid_absorption > 0.5 and of.tape_delta_pct < -0.1:
        if regime.trend_15m != "down" or regime.overall == "ranging":
            stop, target = _compute_levels(of, "long", of.atr_14)
            evidence = [
                f"bid_absorption={of.bid_absorption:.2f}",
                f"tape_delta={of.tape_delta_pct:+.2f}",
                "selling absorbed at support",
            ]
            if of.stacked_bid_count >= 3:
                evidence.append(f"stacked_bids={of.stacked_bid_count}")
            return Setup(
                setup_type="absorption_reversal", side="long", coin=coin,
                confidence=min(0.9, 0.5 + of.bid_absorption * 0.3),
                edge=1.5, evidence=evidence,
                entry_ref=of.mid, invalidation=stop, target=target, atr=of.atr_14,
            )

    # Short: ask absorption detected (buyers exhausted)
    if of.ask_absorption > 0.5 and of.tape_delta_pct > 0.1:
        if regime.trend_15m != "up" or regime.overall == "ranging":
            stop, target = _compute_levels(of, "short", of.atr_14)
            evidence = [
                f"ask_absorption={of.ask_absorption:.2f}",
                f"tape_delta={of.tape_delta_pct:+.2f}",
                "buying absorbed at resistance",
            ]
            if of.stacked_ask_count >= 3:
                evidence.append(f"stacked_asks={of.stacked_ask_count}")
            return Setup(
                setup_type="absorption_reversal", side="short", coin=coin,
                confidence=min(0.9, 0.5 + of.ask_absorption * 0.3),
                edge=1.5, evidence=evidence,
                entry_ref=of.mid, invalidation=stop, target=target, atr=of.atr_14,
            )

    return None


# === Strategy 2: Initiation + Breakout ===
def _check_initiation_breakout(
    coin: str, of: OrderflowSnapshot, regime: MarketRegime
) -> Optional[Setup]:
    """
    Stacked imbalances + strong tape + trend confirmation.
    """
    # Long breakout
    if (of.stacked_bid_count >= 3
        and of.tape_delta_pct > 0.3
        and of.omni_score > MIN_OMNI
        and regime.trend_15m in ("up", "chop")
        and _long_aligned(of)):

        stop, target = _compute_levels(of, "long", of.atr_14)
        evidence = [
            f"stacked_bids={of.stacked_bid_count}",
            f"tape_delta={of.tape_delta_pct:+.2f}",
            f"omni={of.omni_score:+.2f}",
            "breakout initiation",
        ]
        return Setup(
            setup_type="initiation_breakout", side="long", coin=coin,
            confidence=min(0.95, 0.6 + of.stacked_bid_count * 0.05),
            edge=2.0, evidence=evidence,
            entry_ref=of.mid, invalidation=stop, target=target, atr=of.atr_14,
        )

    # Short breakout
    if (of.stacked_ask_count >= 3
        and of.tape_delta_pct < -0.3
        and of.omni_score < -MIN_OMNI
        and regime.trend_15m in ("down", "chop")
        and _short_aligned(of)):

        stop, target = _compute_levels(of, "short", of.atr_14)
        evidence = [
            f"stacked_asks={of.stacked_ask_count}",
            f"tape_delta={of.tape_delta_pct:+.2f}",
            f"omni={of.omni_score:+.2f}",
            "breakdown initiation",
        ]
        return Setup(
            setup_type="initiation_breakout", side="short", coin=coin,
            confidence=min(0.95, 0.6 + of.stacked_ask_count * 0.05),
            edge=2.0, evidence=evidence,
            entry_ref=of.mid, invalidation=stop, target=target, atr=of.atr_14,
        )

    return None


# === Strategy 3: Delta Divergence ===
def _check_delta_divergence(coin: str, of: OrderflowSnapshot) -> Optional[Setup]:
    """
    CVD diverging from price — buying/selling exhaustion.
    """
    if not of.cvd_divergence:
        return None

    # Bearish divergence: price up, CVD down → short
    if (of.price_vs_vwap > 0.005
        and of.cvd < 0
        and of.book_imbalance < -0.1
        and of.rsi_14 > 60):
        stop, target = _compute_levels(of, "short", of.atr_14)
        return Setup(
            setup_type="delta_divergence", side="short", coin=coin,
            confidence=0.7, edge=1.8,
            evidence=[
                "CVD bearish divergence",
                f"price_vs_vwap={of.price_vs_vwap:+.3f}",
                f"cvd={of.cvd:.0f}",
                f"rsi={of.rsi_14:.0f}",
            ],
            entry_ref=of.mid, invalidation=stop, target=target, atr=of.atr_14,
        )

    # Bullish divergence: price down, CVD up → long
    if (of.price_vs_vwap < -0.005
        and of.cvd > 0
        and of.book_imbalance > 0.1
        and of.rsi_14 < 40):
        stop, target = _compute_levels(of, "long", of.atr_14)
        return Setup(
            setup_type="delta_divergence", side="long", coin=coin,
            confidence=0.7, edge=1.8,
            evidence=[
                "CVD bullish divergence",
                f"price_vs_vwap={of.price_vs_vwap:+.3f}",
                f"cvd={of.cvd:.0f}",
                f"rsi={of.rsi_14:.0f}",
            ],
            entry_ref=of.mid, invalidation=stop, target=target, atr=of.atr_14,
        )

    return None


# === Strategy 4: Liquidity Grab ===
def _check_liquidity_grab(
    coin: str, of: OrderflowSnapshot, candles: list[Candle]
) -> Optional[Setup]:
    """
    Price sweeps previous high/low (stop hunt) then reverses.
    """
    if len(candles) < 20:
        return None

    recent_high = max(c.high for c in candles[-20:])
    recent_low = min(c.low for c in candles[-20:])

    # Sweep high + reversal → short
    if (of.mid > recent_high * 0.998
        and of.tape_delta_pct < -0.2
        and of.ask_absorption > 0.3
        and of.rsi_14 > 65):
        stop = recent_high * 1.003  # above sweep
        target = of.vpoc if of.vpoc < of.mid else of.vwap
        if target >= of.mid:
            target = of.mid - (stop - of.mid) * 2
        return Setup(
            setup_type="liquidity_grab", side="short", coin=coin,
            confidence=0.65, edge=1.5,
            evidence=[
                "high sweep + tape reversal",
                f"rsi={of.rsi_14:.0f}",
                f"ask_absorption={of.ask_absorption:.2f}",
            ],
            entry_ref=of.mid, invalidation=stop, target=target, atr=of.atr_14,
        )

    # Sweep low + reversal → long
    if (of.mid < recent_low * 1.002
        and of.tape_delta_pct > 0.2
        and of.bid_absorption > 0.3
        and of.rsi_14 < 35):
        stop = recent_low * 0.997
        target = of.vpoc if of.vpoc > of.mid else of.vwap
        if target <= of.mid:
            target = of.mid + (of.mid - stop) * 2
        return Setup(
            setup_type="liquidity_grab", side="long", coin=coin,
            confidence=0.65, edge=1.5,
            evidence=[
                "low sweep + tape reversal",
                f"rsi={of.rsi_14:.0f}",
                f"bid_absorption={of.bid_absorption:.2f}",
            ],
            entry_ref=of.mid, invalidation=stop, target=target, atr=of.atr_14,
        )

    return None


# === Strategy 5: Trend Pullback ===
def _check_trend_pullback(
    coin: str, of: OrderflowSnapshot, regime: MarketRegime
) -> Optional[Setup]:
    """
    Trend continuation on pullback to VWAP/support.
    """
    # Long pullback in uptrend
    if (regime.trend_1h == "up"
        and -0.005 < of.price_vs_vwap < 0.005  # near VWAP
        and of.book_imbalance > 0.1              # bids building
        and of.tape_delta_pct > 0                 # tape turning positive
        and of.macd_histogram > 0):
        stop, target = _compute_levels(of, "long", of.atr_14)
        return Setup(
            setup_type="trend_pullback", side="long", coin=coin,
            confidence=0.65, edge=1.5,
            evidence=[
                "uptrend pullback to VWAP",
                f"book_imbalance={of.book_imbalance:+.2f}",
                f"regime={regime.trend_1h}",
            ],
            entry_ref=of.mid, invalidation=stop, target=target, atr=of.atr_14,
        )

    # Short pullback in downtrend
    if (regime.trend_1h == "down"
        and -0.005 < of.price_vs_vwap < 0.005
        and of.book_imbalance < -0.1
        and of.tape_delta_pct < 0
        and of.macd_histogram < 0):
        stop, target = _compute_levels(of, "short", of.atr_14)
        return Setup(
            setup_type="trend_pullback", side="short", coin=coin,
            confidence=0.65, edge=1.5,
            evidence=[
                "downtrend pullback to VWAP",
                f"book_imbalance={of.book_imbalance:+.2f}",
                f"regime={regime.trend_1h}",
            ],
            entry_ref=of.mid, invalidation=stop, target=target, atr=of.atr_14,
        )

    return None


# === Strategy 6: Iceberg Follow ===
def _check_iceberg_follow(coin: str, of: OrderflowSnapshot) -> Optional[Setup]:
    """
    Large prints at a single price = institutional iceberg.
    Follow the hidden order.
    """
    if of.large_prints < 3:
        return None

    # Large buys (bid-side iceberg absorbing sells) → long
    if (of.large_prints >= 3
        and of.tape_delta_pct > 0.2
        and of.bid_absorption > 0.4):
        stop = of.mid - of.atr_14 * 1.5
        target = of.mid + of.atr_14 * 3
        return Setup(
            setup_type="iceberg_follow", side="long", coin=coin,
            confidence=0.6, edge=1.5,
            evidence=[
                f"large_prints={of.large_prints}",
                "bid iceberg detected",
            ],
            entry_ref=of.mid, invalidation=stop, target=target, atr=of.atr_14,
        )

    # Large sells (ask-side iceberg) → short
    if (of.large_prints >= 3
        and of.tape_delta_pct < -0.2
        and of.ask_absorption > 0.4):
        stop = of.mid + of.atr_14 * 1.5
        target = of.mid - of.atr_14 * 3
        return Setup(
            setup_type="iceberg_follow", side="short", coin=coin,
            confidence=0.6, edge=1.5,
            evidence=[
                f"large_prints={of.large_prints}",
                "ask iceberg detected",
            ],
            entry_ref=of.mid, invalidation=stop, target=target, atr=of.atr_14,
        )

    return None


# === Strategy 7: Liquidity Vacuum ===
def _check_liquidity_vacuum(coin: str, of: OrderflowSnapshot) -> Optional[Setup]:
    """
    One side of the book vanishes — liquidity vacuum.
    Bid depth collapsing + tape against = short
    Ask depth collapsing + tape for = long
    """
    depth_ratio = of.bid_depth_usd / of.ask_depth_usd if of.ask_depth_usd > 0 else 1.0

    # Bids vanishing → short
    if (depth_ratio < 0.4  # bids < 40% of asks
        and of.tape_delta_pct < -0.1
        and of.book_imbalance < -0.3):
        stop, target = _compute_levels(of, "short", of.atr_14)
        return Setup(
            setup_type="liquidity_vacuum", side="short", coin=coin,
            confidence=0.7, edge=1.8,
            evidence=[
                f"bid/ask_depth ratio={depth_ratio:.2f}",
                "liquidity vacuum — bids vanishing",
            ],
            entry_ref=of.mid, invalidation=stop, target=target, atr=of.atr_14,
        )

    # Asks vanishing → long
    inv_ratio = of.ask_depth_usd / of.bid_depth_usd if of.bid_depth_usd > 0 else 1.0
    if (inv_ratio < 0.4
        and of.tape_delta_pct > 0.1
        and of.book_imbalance > 0.3):
        stop, target = _compute_levels(of, "long", of.atr_14)
        return Setup(
            setup_type="liquidity_vacuum", side="long", coin=coin,
            confidence=0.7, edge=1.8,
            evidence=[
                f"ask/bid depth ratio={inv_ratio:.2f}",
                "liquidity vacuum — asks vanishing",
            ],
            entry_ref=of.mid, invalidation=stop, target=target, atr=of.atr_14,
        )

    return None


# === Strategy 8: Exhaustion Fade ===
def _check_exhaustion_fade(coin: str, of: OrderflowSnapshot) -> Optional[Setup]:
    """
    Sustained high pressure (high OFI) that stalls → fade.
    OMPI peaks and reverses.
    """
    # Rally exhaustion → short
    if (of.rsi_14 > 70
        and of.price_vs_vwap > 0.01  # extended above VWAP
        and of.ask_absorption > 0.5   # buyers being absorbed
        and of.tape_delta_pct < 0):   # tape flipping
        stop, target = _compute_levels(of, "short", of.atr_14)
        return Setup(
            setup_type="exhaustion_fade", side="short", coin=coin,
            confidence=0.65, edge=1.5,
            evidence=[
                f"rsi={of.rsi_14:.0f} overbought",
                f"price_vs_vwap={of.price_vs_vwap:+.3f}",
                "rally exhaustion + ask absorption",
            ],
            entry_ref=of.mid, invalidation=stop, target=target, atr=of.atr_14,
        )

    # Sell exhaustion → long
    if (of.rsi_14 < 30
        and of.price_vs_vwap < -0.01
        and of.bid_absorption > 0.5
        and of.tape_delta_pct > 0):
        stop, target = _compute_levels(of, "long", of.atr_14)
        return Setup(
            setup_type="exhaustion_fade", side="long", coin=coin,
            confidence=0.65, edge=1.5,
            evidence=[
                f"rsi={of.rsi_14:.0f} oversold",
                f"price_vs_vwap={of.price_vs_vwap:+.3f}",
                "sell exhaustion + bid absorption",
            ],
            entry_ref=of.mid, invalidation=stop, target=target, atr=of.atr_14,
        )

    return None
