import pandas as pd
import numpy as np
import logging
import traceback
import math
from constants import CANDLE_COUNTS

logger = logging.getLogger("TradingBot.EntrySignals")

class EntrySignals:
    def __init__(self, config):
        """Initialize entry signals with configuration."""
        self.config = config
        logger.info("Entry Signals initialized with aggressive settings")
        self.entry_confirmation = self.config.get("entry_confirmation", "touch") # or "candle_close"
        # Parameters for pattern detection (can be made configurable)
        self.fvg_threshold_factor = 0.5 # How much of the FVG needs to be filled for iFVG
        self.displacement_candles = 5 # Increased from 3 to 5 candles to look back
        self.displacement_threshold_atr = 1.0 # Reduced from 1.5 to 1.0 to be more sensitive
        self.atr_period = 14 # ATR period for displacement check
        self.debug_mode = self.config.get("debug_mode", False) # Control detailed logging

    def _calculate_atr(self, df, period):
        """Calculates Average True Range (ATR)."""
        if df.empty or len(df) < period + 1:
            logger.warning(f"ATR calculation: insufficient data (len={len(df)}) for period={period}")
            return pd.Series(index=df.index, dtype=float)
        if df[['high', 'low', 'close']].isnull().any().any():
            logger.warning(f"ATR calculation: NaN detected in OHLCV columns. Data sample:\n{df[['high','low','close']].tail()}")
        high_low = df["high"] - df["low"]
        high_close = np.abs(df["high"] - df["close"].shift())
        low_close = np.abs(df["low"] - df["close"].shift())
        tr = pd.concat([high_low, high_close, low_close], axis=1).max(axis=1)
        atr = tr.rolling(window=period, min_periods=period).mean()
        # Ensure atr is a Series for logging and isnull
        if isinstance(atr, np.ndarray):
            atr = pd.Series(atr, index=tr.index)
        # Fix: Use bool() to avoid invalid conditional operand error
        isnull_any = atr.isnull().any()
        if not isinstance(isnull_any, bool):
            isnull_any = isnull_any.item()
        if bool(isnull_any):
            logger.warning(f"ATR output contains NaN values. ATR sample: {atr.tail()} (period={period})")
        if self.debug_mode:
            logger.info(f"ATR input (close): {df['close'].tail(period+2).tolist()}")
            if hasattr(atr, 'tail'):
                logger.info(f"ATR result: {atr.tail(period+2).tolist()}")
            else:
                logger.info(f"ATR result: {atr[-(period+2):].tolist() if hasattr(atr, 'tolist') else atr}")
        return atr

    def _detect_displacement(self, df):
        """Detects displacement candles based on ATR and body size."""
        if df.empty or len(df) < self.atr_period + 1:
            return df

        # Calculate ATR
        df["atr"] = self._calculate_atr(df, self.atr_period)

        # Calculate body size and direction
        df["body_size"] = abs(df["close"] - df["open"])
        df["body_direction"] = np.where(df["close"] > df["open"], 1, -1)

        # Detect displacement - now also considering wick size for stronger moves
        df["wick_size"] = df["high"] - df["low"]
        df["is_displacement"] = np.where(
            (df["body_size"] > df["atr"] * self.displacement_threshold_atr) |  # Strong body
            (df["wick_size"] > df["atr"] * 2.0),  # Strong wick
            True,
            False
        )
        df["displacement_direction"] = np.where(df["is_displacement"], df["body_direction"], 0)

        # Log displacement detection
        if self.debug_mode:
            recent_displacements = df[df["is_displacement"]].iloc[-5:]  # Look at last 5 displacement candles
            if not recent_displacements.empty:
                logger.info(f"Recent displacement candles detected:")
                for idx, row in recent_displacements.iterrows():
                    logger.info(f"- {idx}: Direction={row['body_direction']}, Body={row['body_size']:.2f}, Wick={row['wick_size']:.2f}, ATR={row['atr']:.2f}")
            else:
                logger.info("No recent displacement candles detected")

        return df

    def _detect_fvg(self, df):
        """Detects Fair Value Gaps (FVGs)."""
        fvgs = []
        if len(df) < 3:  # Need at least 3 candles to detect FVG
            return fvgs

        for i in range(1, len(df) - 1):
            # Bullish FVG: Current low > Previous high
            if df["low"].iloc[i] > df["high"].iloc[i-1]:
                fvg = {
                    "type": "Bullish FVG",
                    "top": df["low"].iloc[i],
                    "bottom": df["high"].iloc[i-1],
                    "time": df.index[i]
                }
                fvgs.append(fvg)
                if self.debug_mode:
                    logger.info(f"Bullish FVG detected at {df.index[i]}: {fvg['bottom']:.2f} - {fvg['top']:.2f}")

            # Bearish FVG: Current high < Previous low
            if df["high"].iloc[i] < df["low"].iloc[i-1]:
                fvg = {
                    "type": "Bearish FVG",
                    "top": df["low"].iloc[i-1],
                    "bottom": df["high"].iloc[i],
                    "time": df.index[i]
                }
                fvgs.append(fvg)
                if self.debug_mode:
                    logger.info(f"Bearish FVG detected at {df.index[i]}: {fvg['bottom']:.2f} - {fvg['top']:.2f}")

        return fvgs

    def _detect_order_block(self, df):
        """Detects Order Blocks (OBs)."""
        obs = []
        if len(df) < 3:  # Need at least 3 candles to detect OB
            return obs

        for i in range(1, len(df) - 1):
            # Bullish OB: Current candle is bearish, next candle breaks its low
            if (df["close"].iloc[i] < df["open"].iloc[i] and  # Bearish candle
                df["low"].iloc[i+1] < df["low"].iloc[i]):     # Next candle breaks low
                ob = {
                    "type": "Bullish OB",
                    "top": df["open"].iloc[i],
                    "bottom": df["close"].iloc[i],
                    "time": df.index[i]
                }
                obs.append(ob)
                if self.debug_mode:
                    logger.info(f"Bullish OB detected at {df.index[i]}: {ob['bottom']:.2f} - {ob['top']:.2f}")

            # Bearish OB: Current candle is bullish, next candle breaks its high
            if (df["close"].iloc[i] > df["open"].iloc[i] and  # Bullish candle
                df["high"].iloc[i+1] > df["high"].iloc[i]):   # Next candle breaks high
                ob = {
                    "type": "Bearish OB",
                    "top": df["close"].iloc[i],
                    "bottom": df["open"].iloc[i],
                    "time": df.index[i]
                }
                obs.append(ob)
                if self.debug_mode:
                    logger.info(f"Bearish OB detected at {df.index[i]}: {ob['bottom']:.2f} - {ob['top']:.2f}")

        return obs

    def _detect_bos_choch(self, df, swing_n=3):
        """Detects Break of Structure (BoS) and Change of Character (CHoCH) on M15."""
        if df is None or len(df) < swing_n * 2 + 2:
            return []
        bos_choch_signals = []
        # Find local swing highs/lows
        swing_highs = []
        swing_lows = []
        highs = df["high"].values
        lows = df["low"].values
        for i in range(swing_n, len(df) - swing_n):
            if all(highs[i] > highs[i - j] for j in range(1, swing_n + 1)) and all(highs[i] > highs[i + j] for j in range(1, swing_n + 1)):
                swing_highs.append(i)
            if all(lows[i] < lows[i - j] for j in range(1, swing_n + 1)) and all(lows[i] < lows[i + j] for j in range(1, swing_n + 1)):
                swing_lows.append(i)
        # Detect BoS and CHoCH
        for idx in range(max(swing_n, 1), len(df) - 1):
            # Bullish BoS: price closes above previous swing high
            if idx in swing_highs and df["close"].iloc[idx + 1] > highs[idx]:
                bos_choch_signals.append({
                    "type": "BoS_bullish",
                    "index": idx + 1,
                    "price": df["close"].iloc[idx + 1],
                    "swing_high_idx": idx
                })
            # Bearish BoS: price closes below previous swing low
            if idx in swing_lows and df["close"].iloc[idx + 1] < lows[idx]:
                bos_choch_signals.append({
                    "type": "BoS_bearish",
                    "index": idx + 1,
                    "price": df["close"].iloc[idx + 1],
                    "swing_low_idx": idx
                })
        # CHoCH: Change of Character (trend reversal)
        # Look for a bullish BoS after a bearish BoS, or vice versa
        for i in range(1, len(bos_choch_signals)):
            prev = bos_choch_signals[i - 1]
            curr = bos_choch_signals[i]
            if prev["type"] != curr["type"]:
                bos_choch_signals[i]["type"] = "CHoCH"
        return bos_choch_signals

    def _detect_breaker_block(self, df, swing_n=3):
        """Detects Breaker Blocks: failed swing point, then break in opposite direction."""
        if df is None or len(df) < swing_n * 2 + 2:
            return []
        breakers = []
        highs = df["high"].values
        lows = df["low"].values
        closes = df["close"].values
        # Find swing highs/lows
        swing_highs = []
        swing_lows = []
        for i in range(swing_n, len(df) - swing_n):
            if all(highs[i] > highs[i - j] for j in range(1, swing_n + 1)) and all(highs[i] > highs[i + j] for j in range(1, swing_n + 1)):
                swing_highs.append(i)
            if all(lows[i] < lows[i - j] for j in range(1, swing_n + 1)) and all(lows[i] < lows[i + j] for j in range(1, swing_n + 1)):
                swing_lows.append(i)
        # Detect breaker blocks
        for idx in swing_highs:
            # Bullish breaker: price takes out swing high, then closes below the low that formed the high
            if idx + 1 < len(df) and closes[idx + 1] < lows[idx]:
                breakers.append({
                    "type": "bullish_breaker",
                    "breaker_idx": idx,
                    "break_price": closes[idx + 1],
                    "swing_high": highs[idx],
                    "breaker_low": lows[idx]
                })
        for idx in swing_lows:
            # Bearish breaker: price takes out swing low, then closes above the high that formed the low
            if idx + 1 < len(df) and closes[idx + 1] > highs[idx]:
                breakers.append({
                    "type": "bearish_breaker",
                    "breaker_idx": idx,
                    "break_price": closes[idx + 1],
                    "swing_low": lows[idx],
                    "breaker_high": highs[idx]
                })
        return breakers

    def _get_best_swing_sl(self, data_cache, direction, current_price):
        """Find the best swing low/high for SL from M15, H1, H4 (in that order)."""
        # (BUY: swing low, SELL: swing high)
        swing_sources = [
            ("M15", 20),
            ("H1", 24),
            ("H4", 6)
        ]
        for tf, lookback in swing_sources:
            df = data_cache.get(tf, None)
            if df is None or len(df) < lookback:
                continue
            if direction == 1:
                swing = self._get_recent_swing_low(df, lookback=lookback)
                if swing < current_price:  # Valid only if below entry
                    return swing, tf
            elif direction == -1:
                swing = self._get_recent_swing_high(df, lookback=lookback)
                if swing > current_price:  # Valid only if above entry
                    return swing, tf
        return None, None

    def _confirm_on_lower_timeframes(self, mt5_interface, direction, key_level, current_price, symbol="USTECH100M"):
        """Check 5m and 1m for confirmation: BoS, IFVG fill, bullish/bearish signs."""
        df_5m = mt5_interface.get_rates(symbol, "M5", count=50)
        df_1m = mt5_interface.get_rates(symbol, "M1", count=50)
        # Use 5m for BoS
        if self._detect_bos(df_5m, direction):
            return True
        # Use both 1m and 5m for IFVG
        if self._detect_ifvg_fill(df_5m, direction, key_level) or self._detect_ifvg_fill(df_1m, direction, key_level):
            return True
        # Simple bullish/bearish pattern (engulfing) as placeholder
        if self._detect_bullish_bearish(df_5m, direction) or self._detect_bullish_bearish(df_1m, direction):
            return True
        return False

    def _detect_bos(self, df, direction):
        # Detect break of structure: recent swing high/low break in direction
        if df is None or len(df) < 10:
            return False
        highs = df["high"]
        lows = df["low"]
        if direction == 1:
            # Bullish: price breaks above recent swing high
            swing_high = highs.iloc[-10:-2].max()
            if highs.iloc[-1] > swing_high:
                return True
        elif direction == -1:
            # Bearish: price breaks below recent swing low
            swing_low = lows.iloc[-10:-2].min()
            if lows.iloc[-1] < swing_low:
                return True
        return False

    def _detect_ifvg_fill(self, df, direction, key_level):
        # Detect if fair value gap (gap between candles) is filled near key_level in last 10 candles
        if df is None or len(df) < 10:
            return False
        for i in range(-10, -1):
            prev_high = df["high"].iloc[i-1]
            prev_low = df["low"].iloc[i-1]
            curr_open = df["open"].iloc[i]
            curr_close = df["close"].iloc[i]
            # IFVG: gap between prev candle and current
            if direction == 1 and curr_open > prev_high and abs(curr_open - prev_high) > 0.1:
                # Bullish gap, check if price has come back to fill
                if df["low"].iloc[-1] <= prev_high + 0.1:
                    return True
            elif direction == -1 and curr_open < prev_low and abs(prev_low - curr_open) > 0.1:
                # Bearish gap, check if price has come back to fill
                if df["high"].iloc[-1] >= prev_low - 0.1:
                    return True
        return False

    def _detect_bullish_bearish(self, df, direction):
        # Simple bullish/bearish engulfing pattern as placeholder
        if df is None or len(df) < 3:
            return False
        prev_open = df["open"].iloc[-2]
        prev_close = df["close"].iloc[-2]
        curr_open = df["open"].iloc[-1]
        curr_close = df["close"].iloc[-1]
        if direction == 1:
            # Bullish engulfing
            if curr_close > curr_open and prev_close < prev_open and curr_close > prev_open and curr_open < prev_close:
                return True
        elif direction == -1:
            # Bearish engulfing
            if curr_close < curr_open and prev_close > prev_open and curr_close < prev_open and curr_open > prev_close:
                return True
        return False

    def _get_recent_swing(self, df, direction, lookback=20):
        if df is None or len(df) < lookback:
            return None
        if direction == 1:
            # For buys, swing low
            return df["low"].iloc[-lookback:].min()
        elif direction == -1:
            # For sells, swing high
            return df["high"].iloc[-lookback:].max()
        return None

    def _get_next_key_level(self, key_levels, entry, direction):
        # Find the next closest key level in the direction of the trade
        levels = [lvl for lvl in key_levels.values() if lvl is not None]
        if direction == 1:
            # For buys, next highest above entry
            above = [lvl for lvl in levels if lvl > entry]
            return min(above) if above else None
        elif direction == -1:
            # For sells, next lowest below entry
            below = [lvl for lvl in levels if lvl < entry]
            return max(below) if below else None
        return None

    def _get_most_recent_local_swing_high(self, df, left=3, right=3):
        """
        Find the most recent local swing high in the DataFrame.
        A swing high is a high that is higher than 'left' bars before and 'right' bars after.
        """
        highs = df["high"].values
        for i in range(len(highs) - right - 1, left - 1, -1):
            if all(highs[i] > highs[i - j] for j in range(1, left + 1)) and \
               all(highs[i] > highs[i + j] for j in range(1, right + 1)):
                return highs[i]
        return highs[-1]  # fallback to last high if no swing found

    def _get_most_recent_local_swing_low(self, df, left=3, right=3):
        """
        Find the most recent local swing low in the DataFrame.
        A swing low is a low that is lower than 'left' bars before and 'right' bars after.
        """
        lows = df["low"].values
        for i in range(len(lows) - right - 1, left - 1, -1):
            if all(lows[i] < lows[i - j] for j in range(1, left + 1)) and \
               all(lows[i] < lows[i + j] for j in range(1, right + 1)):
                return lows[i]
        return lows[-1]  # fallback to last low if no swing found

    def find_signal(self, data_cache, structure_analysis, mt5_interface=None, timeframe="M15", data_m1=None):
        try:
            # Allow M5, M15, and M30 for entry signals
            if timeframe not in ["M5", "M15", "M30"]:
                logger.info(f"Signal generation blocked for timeframe {timeframe}. Only M5, M15, and M30 are allowed.")
                return None
            # Use MarketStructure ICT-style logic for M5, M15, and M30
            from strategy.market_structure import MarketStructure
            ms = MarketStructure(self.config)
            trend_result = ms.get_trend(data_cache)
            signals = trend_result.get('signals', [])
            if not signals:
                logger.info(f"No signals found for {timeframe}.")
                return None
            # Select the highest-confidence signal for the requested timeframe
            tf_signals = [s for s in signals if s.get('timeframe') == timeframe]
            if tf_signals:
                best_signal = max(tf_signals, key=lambda s: s.get('confidence', 0))
            else:
                best_signal = max(signals, key=lambda s: s.get('confidence', 0))
            # Return full context
            return {
                'signal': best_signal,
                'trend_phase': trend_result.get('phase'),
                'key_levels': trend_result.get('key_levels', {})
            }
        except Exception as e:
            logger.error(f"Error in find_signal: {str(e)}")
            return None

    def _get_recent_swing_low(self, df, lookback=20):
        """Get the lowest low over the last N candles (default 20)."""
        if len(df) < lookback:
            return df["low"].min()
        return df["low"].iloc[-lookback:].min()

    def _get_recent_swing_high(self, df, lookback=20):
        """Get the highest high over the last N candles (default 20)."""
        if len(df) < lookback:
            return df["high"].max()
        return df["high"].iloc[-lookback:].max()

    def _find_most_recent_bos(self, df, direction, swing_n=3):
        """
        Find the most recent Break of Structure (BoS) in the given direction.
        Confirmation only if the candle CLOSES above (bullish) or below (bearish) the previous swing high/low.
        """
        if df is None or len(df) < 10:
            return False
        highs = df["high"]
        lows = df["low"]
        closes = df["close"]
        if direction == 1:
            # Bullish: candle CLOSES above previous swing high
            swing_high = highs.iloc[-swing_n-1:-1].max()
            return closes.iloc[-1] > swing_high
        elif direction == -1:
            # Bearish: candle CLOSES below previous swing low
            swing_low = lows.iloc[-swing_n-1:-1].min()
            return closes.iloc[-1] < swing_low
        return False

    def _find_most_recent_ifvg(self, df_m5, direction, left_candles=5, df_m1=None):
        """
        For each of the last 5 M5 candles, check for an iFVG (gap) at that candle and also check for a corresponding iFVG on all lower timeframes (M1) at the same time index.
        Only return True if an iFVG is found on M5 and confirmed by an iFVG on M1 at the same candle (or within the corresponding time window).
        """
        if df_m5 is None or len(df_m5) < left_candles + 2:
            return False
        if df_m1 is None or len(df_m1) < 10:
            return False
        # Loop over the last 5 M5 candles
        for i in range(len(df_m5) - 1, len(df_m5) - 1 - left_candles, -1):
            prev_high = df_m5["high"].iloc[i - 1]
            prev_low = df_m5["low"].iloc[i - 1]
            curr_open = df_m5["open"].iloc[i]
            curr_time = df_m5.index[i]
            # Check for iFVG on M5
            if direction == 1 and curr_open > prev_high and all(df_m5["low"].iloc[i - j] < curr_open for j in range(1, left_candles + 1)):
                # Find corresponding M1 candles within this M5 candle
                m1_candles = df_m1[(df_m1.index >= curr_time) & (df_m1.index < curr_time + pd.Timedelta(minutes=5))]
                # Check for iFVG on M1 within this window
                for k in range(1, len(m1_candles)):
                    prev_high_m1 = m1_candles["high"].iloc[k - 1]
                    curr_open_m1 = m1_candles["open"].iloc[k]
                    if curr_open_m1 > prev_high_m1:
                        return True
            elif direction == -1 and curr_open < prev_low and all(df_m5["high"].iloc[i - j] > curr_open for j in range(1, left_candles + 1)):
                m1_candles = df_m1[(df_m1.index >= curr_time) & (df_m1.index < curr_time + pd.Timedelta(minutes=5))]
                for k in range(1, len(m1_candles)):
                    prev_low_m1 = m1_candles["low"].iloc[k - 1]
                    curr_open_m1 = m1_candles["open"].iloc[k]
                    if curr_open_m1 < prev_low_m1:
                        return True
        return False

    def get_optimal_stop(self, ifvg, candles, direction):
        """
        Use the *tighter* of the two SL methods:
        - IFVG boundary stop or liquidity sweep stop.
        """
        ifvg_stop = self.calculate_stop_loss(ifvg, direction)
        liquidity_stop = self.adjust_stop_for_liquidity(ifvg, candles, direction)
        if direction in [1, "LONG", "long"]:
            return max(ifvg_stop, liquidity_stop)  # Choose higher SL (less aggressive for longs)
        else:
            return min(ifvg_stop, liquidity_stop)  # Choose lower SL (less aggressive for shorts)

    def calculate_stop_loss(self, ifvg, direction):
        """
        Place SL just beyond the opposite boundary of the IFVG zone:
        - Bullish IFVG: SL below the IFVG low.
        - Bearish IFVG: SL above the IFVG high.
        """
        buffer = 0.0010  # 10 pips buffer to avoid slippage
        if direction in [1, "LONG", "long"]:
            return ifvg["low"] - buffer
        else:
            return ifvg["high"] + buffer

    def validate_rr_ratio(self, entry, stop, target, direction, min_rr=2.0):
        """
        Cancel trade if RR < min_rr (default 2:1).
        Returns True if RR is valid, False otherwise.
        """
        if entry == stop:
            return False
        if direction in [1, "LONG", "long"]:
            risk = abs(entry - stop)
            reward = abs(target - entry)
        else:  # SHORT
            risk = abs(stop - entry)
            reward = abs(entry - target)
        if risk == 0:
            return False
        rr = reward / risk
        return rr >= min_rr

    def _calculate_signals(self, data, timeframe, data_m1=None):
        try:
            logger.debug(f"[DEBUG] _calculate_signals: data shape={data.shape if hasattr(data, 'shape') else 'N/A'}, timeframe={timeframe}, data_m1 shape={data_m1.shape if hasattr(data_m1, 'shape') else 'N/A'}")
            current_price = data["close"].iloc[-1]
            atr = self._calculate_atr(data, period=14)
            logger.debug(f"[DEBUG] ATR raw={atr}")
            if atr is None or (isinstance(atr, np.ndarray) and np.isnan(atr).all()):
                logger.warning(f"[SKIP] _calculate_signals: ATR is None or all NaN. data shape={data.shape if hasattr(data, 'shape') else 'N/A'}")
                return None
            atr = atr[-1] if isinstance(atr, np.ndarray) else (atr.iloc[-1] if hasattr(atr, 'iloc') else atr)
            logger.debug(f"[DEBUG] ATR final={atr}")

            direction = 0
            confidence = 0

            # Use BoS, iFVG always, but OB/OB retest only for M5 and M1
            bos_bull = self._find_most_recent_bos(data, 1, swing_n=5)
            bos_bear = self._find_most_recent_bos(data, -1, swing_n=5)
            ifvg_bull = self._find_most_recent_ifvg(data, 1, left_candles=5, df_m1=data_m1)
            ifvg_bear = self._find_most_recent_ifvg(data, -1, left_candles=5, df_m1=data_m1)
            logger.debug(f"[DEBUG] Pattern results: bos_bull={bos_bull}, bos_bear={bos_bear}, ifvg_bull={ifvg_bull}, ifvg_bear={ifvg_bear}")

            ob_retest_bull = ob_retest_bear = False
            if timeframe in ["M5", "M1"]:
                ob_retest_bull = self._detect_order_block_retest(data, 1)
                ob_retest_bear = self._detect_order_block_retest(data, -1)
            ifvg_retest_bull = self._detect_ifvg_retest(data, 1)
            ifvg_retest_bear = self._detect_ifvg_retest(data, -1)

            if bos_bull or ifvg_bull or ob_retest_bull or ifvg_retest_bull:
                direction = 1
                confidence = 70
                logger.info(f"[SIGNAL] Bullish detected: bos_bull={bos_bull}, ifvg_bull={ifvg_bull}, ob_retest_bull={ob_retest_bull}, ifvg_retest_bull={ifvg_retest_bull}")
            elif bos_bear or ifvg_bear or ob_retest_bear or ifvg_retest_bear:
                direction = -1
                confidence = 70
                logger.info(f"[SIGNAL] Bearish detected: bos_bear={bos_bear}, ifvg_bear={ifvg_bear}, ob_retest_bear={ob_retest_bear}, ifvg_retest_bear={ifvg_retest_bear}")

            # After confirming direction and before R:R check, use optimal SL
            if direction != 0:
                try:
                    # For demonstration, pass a dummy IFVG if not available
                    ifvg = {"low": min(c.low for c in data[-3:]), "high": max(c.high for c in data[-3:])}
                    stop_loss = self.get_optimal_stop(ifvg, data, direction)
                    logger.info(f"SL set using optimal stop logic: {stop_loss}")
                except Exception as e:
                    logger.warning(f"Could not set SL using optimal stop logic: {e}")

            # SL/TP logic (unchanged)
            if direction == 0:
                logger.info("No valid BoS or iFVG found, skipping signal.")
                return None
            if timeframe == "M15":
                if direction == 1:
                    stop_loss = self._get_recent_swing_low(data, lookback=20)
                    take_profit = self._get_recent_swing_high(data, lookback=20)
                else:
                    stop_loss = self._get_recent_swing_high(data, lookback=20)
                    take_profit = self._get_recent_swing_low(data, lookback=20)
            else:
                stop_loss = current_price - (atr * 1.0) if direction == 1 else current_price + (atr * 1.0)
                take_profit = current_price + (atr * 1.5) if direction == 1 else current_price - (atr * 1.5)
            # If SL or TP is still None or equal to entry, skip
            if stop_loss is None or take_profit is None or stop_loss == current_price or take_profit == current_price:
                logger.info(f"No valid swing high/low found for SL/TP, skipping signal. entry={current_price}, SL={stop_loss}, TP={take_profit}")
                return None

            # --- ENFORCE XAUUSD STOP LOSS DISTANCE BOUNDS AT SIGNAL GENERATION ---
            # (Removed: no static SL distance enforcement)

            # Enforce minimum RR of min_risk_reward_ratio from config
            min_rr = self.config.get('min_risk_reward_ratio', 2.0)
            reward = abs(current_price - take_profit)
            risk = abs(current_price - stop_loss)
            logger.info(f"DEBUG: entry={current_price}, stop_loss={stop_loss}, take_profit={take_profit}, reward={reward}, risk={risk}, min_rr={min_rr}")
            if risk == 0 or reward == 0:
                logger.info(f"Trade skipped due to zero risk or reward: entry={current_price}, SL={stop_loss}, TP={take_profit}")
                return None
            rr = reward / risk if risk != 0 else 0
            if rr <= 0:
                logger.info(f"Trade blocked due to negative or zero RR: RR={rr:.2f}")
                return None
            if rr < min_rr:
                logger.info(f"Trade skipped due to RR < {min_rr}: RR={rr:.2f}")
                return None

            # After SL/TP are set and before returning the signal
            if not self.validate_rr_ratio(current_price, stop_loss, take_profit, direction, min_rr=min_rr):
                logger.info(f"Trade skipped due to insufficient RR ratio: entry={current_price}, SL={stop_loss}, TP={take_profit}, min_rr={min_rr}")
                return None

            # Set type and pattern based on direction, and ensure confidence is always present
            if direction == -1:
                signal_type = 'SELL'
                direction_str = 'sell'
            else:
                signal_type = 'BUY'
                direction_str = 'buy'
            pattern = signal_type  # Always match pattern to type
            confidence = confidence if confidence is not None else 1.0
            # Build trade reason string
            market_structure_str = f"{'Bullish' if direction == 1 else 'Bearish'} BoS on {timeframe}"
            entry_confluence_str = f"Price returned to {timeframe} iFVG"
            higher_tf_bias_str = "H1 bullish structure" if direction == 1 else "H1 bearish structure"
            liquidity_event_str = "Sweep of prior low before reversal" if direction == 1 else "Sweep of prior high before reversal"
            risk_confirmation_str = f"RR = {round(rr, 2)}, ATR within acceptable volatility"
            session_filter_str = "Session filter passed"  # You can make this dynamic if needed
            reason = (
                f"{'Long' if direction == 1 else 'Short'} USTECH100M because price broke previous {timeframe} structure {'high' if direction == 1 else 'low'} (BoS), "
                f"retraced into an internal fair value gap (iFVG), and tapped a valid order block within a higher timeframe "
                f"{'bullish' if direction == 1 else 'bearish'} bias (H1). Price also swept liquidity under the previous {timeframe} {'low' if direction == 1 else 'high'}. "
                f"Risk/reward is {round(rr, 2)}, and ATR supports manageable volatility."
            )
            signal = {
                "direction": direction_str,  # Always string: 'buy' or 'sell'
                "type": signal_type,
                "confidence": confidence,
                "stop_loss": stop_loss,
                "take_profit": take_profit,
                "timeframe": timeframe if timeframe else 'M15',  # Always include timeframe
                "entry": current_price,
                "swing_high": self._get_recent_swing_high(data, lookback=20),
                "swing_low": self._get_recent_swing_low(data, lookback=20),
                "pattern": pattern,
                "volume": self.config.get('default_volume', 1),
                "symbol": "USTECH100M",
                "reason": reason
            }
            # Example for ltf_bullish signal
            if signal_type == 'ltf_bullish':
                signal['direction'] = 'BUY'
            elif signal_type == 'ltf_bearish':
                signal['direction'] = 'SELL'
            # For all other signals, set direction based on type if not already set
            if 'direction' not in signal:
                if 'buy' in signal.get('type', '').lower() or 'bullish' in signal.get('type', '').lower():
                    signal['direction'] = 'BUY'
                elif 'sell' in signal.get('type', '').lower() or 'bearish' in signal.get('type', '').lower():
                    signal['direction'] = 'SELL'
            # Log the full signal dict before returning
            logger.debug(f"[SIGNAL-DICT] direction={direction}, confidence={confidence}, current_price={current_price}, atr={atr}")
            return self._finalize_signal(signal)
        except Exception as e:
            import traceback
            logger.error(f"[ERROR] _calculate_signals: {e}\n{traceback.format_exc()}")
            return None

    def _finalize_signal(self, signal):
        # Convert all numeric fields to native Python types
        for key in ['entry', 'stop_loss', 'take_profit', 'confidence']:
            if key in signal:
                try:
                    signal[key] = float(signal[key])
                except Exception:
                    pass
        if 'direction' in signal:
            try:
                signal['direction'] = str(signal['direction'])
            except Exception:
                pass
        # Ensure required fields are present
        if 'timeframe' not in signal or not signal['timeframe']:
            signal['timeframe'] = 'M15'
        if 'symbol' not in signal or not signal['symbol']:
            signal['symbol'] = 'USTECH100M'
        return signal

    def _detect_order_block_retest(self, df, direction, lookback=20):
        """
        Detects a retest of the most recent order block (OB) in the given direction within the lookback window.
        For bullish: price returns to a bullish OB after breaking above it.
        For bearish: price returns to a bearish OB after breaking below it.
        """
        obs = self._detect_order_block(df[-lookback:])
        if not obs:
            return False
        last_ob = obs[-1]
        if direction == 1 and last_ob["type"] == "Bullish OB":
            # Retest: price returns to OB zone after break
            recent_lows = df["low"].iloc[-3:]
            if (recent_lows < last_ob["top"]).any() and (recent_lows > last_ob["bottom"]).any():
                return True
        elif direction == -1 and last_ob["type"] == "Bearish OB":
            recent_highs = df["high"].iloc[-3:]
            if (recent_highs > last_ob["bottom"]).any() and (recent_highs < last_ob["top"]).any():
                return True
        return False

    def _detect_ifvg_retest(self, df, direction, left_candles=5):
        """
        Detects a retest of the most recent iFVG in the given direction.
        For bullish: price returns to fill a bullish iFVG.
        For bearish: price returns to fill a bearish iFVG.
        """
        fvgs = self._detect_fvg(df[-left_candles-5:])
        if not fvgs:
            return False
        last_fvg = fvgs[-1]
        if direction == 1 and last_fvg["type"] == "Bullish FVG":
            # Retest: price returns to FVG zone
            recent_lows = df["low"].iloc[-3:]
            if (recent_lows < last_fvg["top"]).any() and (recent_lows > last_fvg["bottom"]).any():
                return True
        elif direction == -1 and last_fvg["type"] == "Bearish FVG":
            recent_highs = df["high"].iloc[-3:]
            if (recent_highs > last_fvg["bottom"]).any() and (recent_highs < last_fvg["top"]).any():
                return True
        return False

    def is_london_or_ny_session(self):
        # For testing, always return True
        return True

    def is_in_ote_zone(self, entry, swing_high, swing_low):
        # OTE: 61.8%–79% retracement from swing high/low
        if swing_high == swing_low:
            return False
        fib_618 = swing_low + 0.618 * (swing_high - swing_low)
        fib_79 = swing_low + 0.79 * (swing_high - swing_low)
        return fib_618 <= entry <= fib_79

    def is_near_liquidity_pool(self, entry, liquidity_pools, threshold=2.0):
        # Placeholder: liquidity_pools is a list of price levels
        return any(abs(entry - lp) < threshold for lp in liquidity_pools)

    def adjust_stop_for_liquidity(self, ifvg, candles, direction):
        """
        Adjust SL to the *other side* of the liquidity sweep:
        - If price swept a low (SSL), SL goes below that low.
        - If price swept a high (BSL), SL goes above that high.
        """
        if len(candles) < 1:
            raise ValueError("Not enough candles to determine liquidity sweep stop loss.")
        recent_low = min(c.low for c in candles[-3:])
        recent_high = max(c.high for c in candles[-3:])
        buffer = 0.0010  # 10 pips
        # Normalize direction
        if direction in [1, "LONG", "long"]:
            sl = recent_low - buffer
        else:
            sl = recent_high + buffer
        return sl

    def get_intraday_bias(self, structure_analysis):
        """Determine overall bias from H4, H1, M30, M15 structure directions."""
        bullish_count = 0
        bearish_count = 0
        for tf in ["H4", "H1", "M30", "M15"]:
            direction = None
            if isinstance(structure_analysis, dict):
                tf_struct = structure_analysis.get(tf)
                if tf_struct and isinstance(tf_struct, dict):
                    direction = tf_struct.get("direction")
                elif tf == structure_analysis.get("timeframe"):
                    direction = structure_analysis.get("direction")
            if direction == 1:
                bullish_count += 1
            elif direction == -1:
                bearish_count += 1
        if bullish_count > bearish_count:
            return 1  # Bullish bias
        elif bearish_count > bullish_count:
            return -1  # Bearish bias
        else:
            return 0  # Neutral

# Example Usage (called from main.py)
if __name__ == "__main__":
    # This module is complex and requires realistic data and structure analysis
    # Integration testing within main.py is more practical
    print("EntrySignals module. Run integration test via main.py with MT5 connection.")
    # Add basic standalone tests for individual pattern detectors if needed
    # Example:
    # test_df = pd.DataFrame({...})
    # entry_signals = EntrySignals(config) # Load dummy config
    # fvgs = entry_signals._detect_fvg(test_df)
    # print(f"Detected FVGs: {fvgs}")

