"""Volatility-adaptive barrier methods for stop-loss and take-profit placement.

Implements multiple researched methodologies for dynamic SL/TP placement:

1. **Volatility Regime-Adjusted ATR Multipliers** — adjust SL/TP based on ATR
   percentile (low/med/high vol). Research-backed: adaptive multipliers outperform
   fixed ones across asset classes (MDPI 2018, StratBase 2024).

2. **Volatility Ratio Method** — ratio of short-term ATR(14) to long-term ATR(100).
   >1.5 = vol expansion → widen stops; <0.5 = vol contraction → tighten stops.

3. **Minimum Absolute/Percentage Distances** — prevent degenerate SL=TP levels
   when ATR is near zero (fixes EURUSD bug where SL=TP=1.16).

4. **Structure-Based Barrier Anchoring** — place stops at nearest swing high/low
   with ATR buffer, not just ATR distance from entry.

All methods produce output in the same format: (sl_price, tp_price, metadata)
for easy integration with existing backtest/inference/scanner code.
"""

from __future__ import annotations

from dataclasses import dataclass
from enum import Enum
from typing import Dict, Optional, Tuple

import numpy as np
import pandas as pd


class VolatilityRegime(str, Enum):
    """Volatility regime classification based on ATR percentile."""
    LOW = "LOW"        # ATR < 33rd percentile
    NORMAL = "NORMAL"  # ATR between 33rd and 67th percentile
    HIGH = "HIGH"      # ATR > 67th percentile


@dataclass
class DynamicBarrierResult:
    """Result of dynamic barrier computation.
    
    Contains the computed SL/TP levels plus metadata about how they were derived.
    """
    sl_price: float
    tp_price: float
    sl_distance_price: float
    tp_distance_price: float
    sl_multiplier: float
    tp_multiplier: float
    volatility_regime: VolatilityRegime
    volatility_ratio: float
    atr_at_entry: float
    min_distance_applied: bool = False
    structure_anchored: bool = False
    min_gap_applied: bool = False


# ---------------------------------------------------------------
# Default configuration for dynamic barriers
# ---------------------------------------------------------------

# Base ATR multipliers by volatility regime
REGIME_MULTIPLIERS: Dict[VolatilityRegime, Tuple[float, float]] = {
    VolatilityRegime.LOW:    (0.8,  1.2),   # Tight in low vol
    VolatilityRegime.NORMAL: (1.0,  1.5),   # Default balanced
    VolatilityRegime.HIGH:   (1.5,  2.5),   # Wide in high vol
}

# Volatility ratio thresholds for adjustment
VOL_RATIO_LOW = 0.5     # Below this → vol contraction → tighten
VOL_RATIO_HIGH = 1.5    # Above this → vol expansion → widen
VOL_RATIO_ADJ_SL = 1.3  # Multiply SL by this when vol ratio > HIGH
VOL_RATIO_ADJ_TP = 1.3  # Multiply TP by this when vol ratio > HIGH

# ATR percentile thresholds for regime classification
REGIME_LOW_PCT = 0.33    # Below this → LOW vol
REGIME_HIGH_PCT = 0.67   # Above this → HIGH vol

# Minimum distance safeguards (percentage of entry price)
MIN_SL_PCT = 0.001    # 0.1% of entry — absolute minimum SL distance
MIN_TP_PCT = 0.0015   # 0.15% of entry — absolute minimum TP distance
MIN_GAP_PCT = 0.003   # 0.3% of entry — minimum total SL+TP gap

# Minimum Risk-Reward ratio (TP / SL)
# Applied to ALL signals — overrides NN predictions if they give poor RR
MIN_RR = 1.5  # Minimum RR for every trade (was ~0.8-1.0 for many NN signals)

# Structure-based barrier config
STRUCTURE_LOOKBACK = 20      # Bars to look back for swing points
STRUCTURE_ATR_BUFFER = 0.3   # ATR multiplier for buffer around swing point
STRUCTURE_TP_MULT = 2.0      # TP multiplier when using structure-based placement


# ---------------------------------------------------------------
# Volatility regime detection
# ---------------------------------------------------------------


def compute_volatility_regime(
    atr_series: pd.Series,
    lookback: int = 100,
) -> VolatilityRegime:
    """Classify current volatility into LOW/NORMAL/HIGH regime.
    
    Uses percentile rank of current ATR within its recent history.
    
    Args:
        atr_series: Full ATR Series (must have at least ``lookback`` values)
        lookback: Number of historical periods to compare against
        
    Returns:
        VolatilityRegime enum value
    """
    current_atr = atr_series.iloc[-1]
    history = atr_series.iloc[-min(lookback, len(atr_series)):]
    
    if len(history) < 10:
        return VolatilityRegime.NORMAL
    
    percentile = (history < current_atr).mean()
    
    if percentile < REGIME_LOW_PCT:
        return VolatilityRegime.LOW
    elif percentile > REGIME_HIGH_PCT:
        return VolatilityRegime.HIGH
    else:
        return VolatilityRegime.NORMAL


def compute_volatility_ratio(
    atr_series: pd.Series,
    short_period: int = 14,
    long_period: int = 100,
) -> float:
    """Ratio of current ATR to long-term average ATR.
    
    >1.5 = volatility expansion (market heating up)
    <0.5 = volatility contraction (market quieting down)
    
    Args:
        atr_series: Full ATR Series
        short_period: Not used directly — current ATR is the last value
        long_period: Lookback for long-term average
        
    Returns:
        Volatility ratio (current / long-term average)
    """
    current_atr = atr_series.iloc[-1]
    long_history = atr_series.iloc[-min(long_period, len(atr_series)):]
    
    if len(long_history) < 5:
        return 1.0
    
    long_avg = long_history.mean()
    if long_avg <= 0:
        return 1.0
    
    return current_atr / long_avg


# ---------------------------------------------------------------
# Swing point detection for structure-based stops
# ---------------------------------------------------------------


def _find_swing_low(
    df: pd.DataFrame,
    idx: int,
    lookback: int = STRUCTURE_LOOKBACK,
) -> Optional[float]:
    """Find the lowest low in a window ending at ``idx``.
    
    Args:
        df: OHLCV DataFrame
        idx: Current bar index
        lookback: Number of bars to look back
        
    Returns:
        Lowest low value, or None if insufficient data
    """
    if idx < lookback:
        return None
    window = df.iloc[idx - lookback: idx + 1]
    return window["Low"].min()


def _find_swing_high(
    df: pd.DataFrame,
    idx: int,
    lookback: int = STRUCTURE_LOOKBACK,
) -> Optional[float]:
    """Find the highest high in a window ending at ``idx``.
    
    Args:
        df: OHLCV DataFrame
        idx: Current bar index
        lookback: Number of bars to look back
        
    Returns:
        Highest high value, or None if insufficient data
    """
    if idx < lookback:
        return None
    window = df.iloc[idx - lookback: idx + 1]
    return window["High"].max()


# ---------------------------------------------------------------
# Main barrier computation functions
# ---------------------------------------------------------------


def suggest_dynamic_barriers(
    entry_price: float,
    side: str,
    atr_val: float,
    atr_series: pd.Series,
    df: Optional[pd.DataFrame] = None,
    idx: Optional[int] = None,
    use_structure: bool = True,
) -> DynamicBarrierResult:
    """Suggest SL and TP levels using volatility-adaptive methodology.
    
    Four-layer approach:
    1. **Regime-based multipliers**: Adjust base ATR multipliers by volatility regime
    2. **Volatility ratio adjustment**: Fine-tune multipliers based on short/long ATR
    3. **Minimum distance enforcement**: Ensure SL/TP never collapse to near-zero
    4. **Structure anchoring** (optional): Snap SL to nearest swing high/low
    
    Args:
        entry_price: Entry price
        side: "LONG" or "SHORT"
        atr_val: Current ATR value
        atr_series: Full ATR Series for regime detection
        df: Full OHLCV DataFrame (required for structure anchoring)
        idx: Current bar index in ``df`` (required for structure anchoring)
        use_structure: Whether to attempt structure-based anchoring
        
    Returns:
        DynamicBarrierResult with computed levels and metadata
    """
    # ---- Layer 1: Volatility regime detection ----
    regime = compute_volatility_regime(atr_series)
    base_sl_mult, base_tp_mult = REGIME_MULTIPLIERS[regime]
    
    # ---- Layer 2: Volatility ratio adjustment ----
    vol_ratio = compute_volatility_ratio(atr_series)
    sl_mult = base_sl_mult
    tp_mult = base_tp_mult
    
    if vol_ratio > VOL_RATIO_HIGH:
        # Volatility expansion → widen barriers
        sl_mult *= VOL_RATIO_ADJ_SL
        tp_mult *= VOL_RATIO_ADJ_TP
    elif vol_ratio < VOL_RATIO_LOW:
        # Volatility contraction → tighten barriers
        sl_mult *= 0.8
        tp_mult *= 0.8
    
    # Compute raw ATR-based distances
    sl_distance_price = atr_val * sl_mult
    tp_distance_price = atr_val * tp_mult
    
    min_distance_applied = False
    structure_anchored = False
    min_gap_applied = False
    
    # ---- Layer 3: Minimum distance enforcement ----
    min_sl_price = entry_price * MIN_SL_PCT
    min_tp_price = entry_price * MIN_TP_PCT
    
    if sl_distance_price < min_sl_price:
        sl_distance_price = min_sl_price
        min_distance_applied = True
    
    if tp_distance_price < min_tp_price:
        tp_distance_price = min_tp_price
        min_distance_applied = True
    
    # ---- Layer 4: Structure-based anchoring (optional) ----
    if use_structure and df is not None and idx is not None:
        sl_price, tp_price = _suggest_structure_barriers(
            df=df,
            idx=idx,
            side=side,
            entry_price=entry_price,
            atr_val=atr_val,
            default_sl_dist=sl_distance_price,
            default_tp_dist=tp_distance_price,
        )
        structure_anchored = True
    else:
        # Pure ATR-based placement
        if side == "LONG":
            sl_price = entry_price - sl_distance_price
            tp_price = entry_price + tp_distance_price
        else:
            sl_price = entry_price + sl_distance_price
            tp_price = entry_price - tp_distance_price
    
    # ---- Layer 5: Minimum Risk-Reward enforcement ----
    if side == "LONG":
        sl_dist = entry_price - sl_price
        tp_dist = tp_price - entry_price
    else:
        sl_dist = sl_price - entry_price
        tp_dist = entry_price - tp_price
    
    if sl_dist > 0 and tp_dist / sl_dist < MIN_RR:
        # Scale TP up to meet minimum RR, keep SL unchanged
        tp_dist_needed = sl_dist * MIN_RR
        if side == "LONG":
            tp_price = entry_price + tp_dist_needed
        else:
            tp_price = entry_price - tp_dist_needed
        tp_distance_price = tp_dist_needed
    
    # ---- Layer 6: Minimum gap enforcement (only if not already covered by RR) ----
    if side == "LONG":
        total_distance = (tp_price - entry_price) + (entry_price - sl_price)
    else:
        total_distance = (entry_price - tp_price) + (sl_price - entry_price)
    
    min_gap = entry_price * MIN_GAP_PCT
    if total_distance < min_gap:
        scale = min_gap / (total_distance + 1e-10)
        sl_distance_scaled = sl_distance_price * scale
        tp_distance_scaled = tp_distance_price * scale
        
        if side == "LONG":
            sl_price = entry_price - sl_distance_scaled
            tp_price = entry_price + tp_distance_scaled
        else:
            sl_price = entry_price + sl_distance_scaled
            tp_price = entry_price - tp_distance_scaled
        
        sl_distance_price = sl_distance_scaled
        tp_distance_price = tp_distance_scaled
        min_gap_applied = True
    
    return DynamicBarrierResult(
        sl_price=sl_price,
        tp_price=tp_price,
        sl_distance_price=sl_distance_price,
        tp_distance_price=tp_distance_price,
        sl_multiplier=sl_mult,
        tp_multiplier=tp_mult,
        volatility_regime=regime,
        volatility_ratio=vol_ratio,
        atr_at_entry=atr_val,
        min_distance_applied=min_distance_applied,
        structure_anchored=structure_anchored,
        min_gap_applied=min_gap_applied,
    )


def _suggest_structure_barriers(
    df: pd.DataFrame,
    idx: int,
    side: str,
    entry_price: float,
    atr_val: float,
    default_sl_dist: float,
    default_tp_dist: float,
) -> Tuple[float, float]:
    """Place SL at nearest swing high/low with ATR buffer.
    
    For LONG: SL at nearest swing low minus ATR buffer (or default if no swing found)
    For SHORT: SL at nearest swing high plus ATR buffer (or default if no swing found)
    
    TP is always ATR-based (TP research shows ATR targets > structure targets).
    
    Args:
        df: Full OHLCV DataFrame
        idx: Current bar index
        side: "LONG" or "SHORT"
        entry_price: Entry price
        atr_val: Current ATR value
        default_sl_dist: Fallback SL distance if structure anchoring fails
        default_tp_dist: Fallback TP distance
        
    Returns:
        Tuple of (sl_price, tp_price)
    """
    if side == "LONG":
        swing_low = _find_swing_low(df, idx)
        if swing_low is not None and swing_low < entry_price:
            # Place SL slightly below swing low with ATR buffer
            structure_sl = swing_low - atr_val * STRUCTURE_ATR_BUFFER
            # Use the wider of structure vs ATR-based stop
            default_sl = entry_price - default_sl_dist
            sl_price = min(structure_sl, default_sl)  # further away = safer
        else:
            sl_price = entry_price - default_sl_dist
        
        # TP based on volatility target (research-supported)
        tp_price = entry_price + max(default_tp_dist, atr_val * STRUCTURE_TP_MULT)
        
    else:  # SHORT
        swing_high = _find_swing_high(df, idx)
        if swing_high is not None and swing_high > entry_price:
            structure_sl = swing_high + atr_val * STRUCTURE_ATR_BUFFER
            default_sl = entry_price + default_sl_dist
            sl_price = max(structure_sl, default_sl)  # further away = safer
        else:
            sl_price = entry_price + default_sl_dist
        
        tp_price = entry_price - max(default_tp_dist, atr_val * STRUCTURE_TP_MULT)
    
    return sl_price, tp_price


# ---------------------------------------------------------------
# Validation and post-processing
# ---------------------------------------------------------------


def validate_barrier_levels(
    sl_price: float,
    tp_price: float,
    entry_price: float,
    side: str,
    atr_val: float,
    min_rr: float = MIN_RR,
) -> Tuple[float, float]:
    """Validate and fix degenerate barrier levels, enforce minimum RR.
    
    Ensures:
    - SL != TP (no zero-distance levels)
    - SL and TP are on correct sides of entry
    - Minimum gap between SL and TP (at least MIN_GAP_PCT of entry)
    - Minimum absolute distance from entry (at least MIN_SL_PCT for SL)
    - **Minimum RR (TP/SL >= min_rr)** — scales TP up if ratio is too low
    
    Args:
        sl_price: Proposed SL price
        tp_price: Proposed TP price
        entry_price: Entry price
        side: "LONG" or "SHORT"
        atr_val: Current ATR value
        min_rr: Minimum risk-reward ratio (TP distance / SL distance)
        
    Returns:
        Tuple of (sl_price, tp_price) — validated and possibly corrected
    """
    min_sl_dist = entry_price * MIN_SL_PCT
    min_tp_dist = entry_price * MIN_TP_PCT
    min_gap = entry_price * MIN_GAP_PCT
    
    if side == "LONG":
        # SL must be below entry, TP must be above entry
        if sl_price >= entry_price:
            sl_price = entry_price - max(min_sl_dist, atr_val * 0.5)
        if tp_price <= entry_price:
            tp_price = entry_price + max(min_tp_dist, atr_val * 0.5)
        
        # Ensure minimum gap
        gap = tp_price - sl_price
        if gap < min_gap:
            half_gap = min_gap / 2
            sl_price = entry_price - max(entry_price - sl_price, half_gap)
            tp_price = entry_price + max(tp_price - entry_price, half_gap)
        
        # --- Enforce minimum RR ---
        sl_dist = entry_price - sl_price
        tp_dist = tp_price - entry_price
        if sl_dist > 0 and tp_dist / sl_dist < min_rr:
            # Scale TP up to meet minimum RR
            tp_dist_needed = sl_dist * min_rr
            tp_price = entry_price + tp_dist_needed
    
    else:  # SHORT
        # SL must be above entry, TP must be below entry
        if sl_price <= entry_price:
            sl_price = entry_price + max(min_sl_dist, atr_val * 0.5)
        if tp_price >= entry_price:
            tp_price = entry_price - max(min_tp_dist, atr_val * 0.5)
        
        # Ensure minimum gap
        gap = sl_price - tp_price
        if gap < min_gap:
            half_gap = min_gap / 2
            sl_price = entry_price + max(sl_price - entry_price, half_gap)
            tp_price = entry_price - max(entry_price - tp_price, half_gap)
        
        # --- Enforce minimum RR ---
        sl_dist = sl_price - entry_price
        tp_dist = entry_price - tp_price
        if sl_dist > 0 and tp_dist / sl_dist < min_rr:
            # Scale TP up (move it further from entry) to meet RR
            tp_dist_needed = sl_dist * min_rr
            tp_price = entry_price - tp_dist_needed
    
    return sl_price, tp_price


def barriers_to_dict(result: DynamicBarrierResult) -> Dict:
    """Convert DynamicBarrierResult to a flat dictionary for logging/storage.
    
    Args:
        result: DynamicBarrierResult object
        
    Returns:
        Dictionary with all fields
    """
    return {
        "sl_price": result.sl_price,
        "tp_price": result.tp_price,
        "sl_distance_price": result.sl_distance_price,
        "tp_distance_price": result.tp_distance_price,
        "sl_multiplier": round(result.sl_multiplier, 2),
        "tp_multiplier": round(result.tp_multiplier, 2),
        "volatility_regime": result.volatility_regime.value,
        "volatility_ratio": round(result.volatility_ratio, 2),
        "atr_at_entry": result.atr_at_entry,
        "min_distance_applied": result.min_distance_applied,
        "structure_anchored": result.structure_anchored,
        "min_gap_applied": result.min_gap_applied,
    }
