"""
ICT/SMC (Inner Circle Trader / Smart Money Concepts) Detector
Advanced market structure analysis for institutional trading patterns

Key Concepts:
- Order Blocks (OB): Institutional buying/selling zones
- Fair Value Gaps (FVG): Price inefficiencies that tend to get filled
- Liquidity Sweeps: Stop hunts above/below key levels
- Break of Structure (BOS): Market structure changes
- Change of Character (CHoCH): Trend reversal signals
- Premium/Discount Zones: Fibonacci-based optimal entry areas
"""

import pandas as pd
import numpy as np
from typing import Dict, List, Optional, Tuple, Any
from dataclasses import dataclass, field
from datetime import datetime
from enum import Enum
from loguru import logger


class MarketBias(str, Enum):
    """Market directional bias"""
    BULLISH = "bullish"
    BEARISH = "bearish"
    NEUTRAL = "neutral"


class StructureType(str, Enum):
    """Market structure types"""
    BOS = "break_of_structure"
    CHOCH = "change_of_character"
    SWEEP = "liquidity_sweep"
    INDUCEMENT = "inducement"


@dataclass
class OrderBlock:
    """Institutional Order Block"""
    type: str  # 'bullish' or 'bearish'
    high: float
    low: float
    open_price: float
    close_price: float
    volume: float
    timestamp: datetime
    strength: float  # 0-1 strength score
    valid: bool = True
    touched: bool = False
    broken: bool = False
    mitigation_price: Optional[float] = None

    @property
    def midpoint(self) -> float:
        return (self.high + self.low) / 2

    @property
    def size_percent(self) -> float:
        """Size as percentage of price"""
        return ((self.high - self.low) / self.close_price) * 100

    def to_dict(self) -> Dict[str, Any]:
        return {
            'type': self.type,
            'high': self.high,
            'low': self.low,
            'midpoint': self.midpoint,
            'strength': self.strength,
            'valid': self.valid,
            'touched': self.touched,
            'broken': self.broken,
            'timestamp': self.timestamp.isoformat() if self.timestamp else None
        }


@dataclass
class FairValueGap:
    """Fair Value Gap (Imbalance)"""
    type: str  # 'bullish' or 'bearish'
    high: float  # Upper bound of gap
    low: float   # Lower bound of gap
    size: float  # Gap size in price
    size_percent: float  # Gap size as percentage
    timestamp: datetime
    filled: bool = False
    fill_percent: float = 0.0

    @property
    def midpoint(self) -> float:
        return (self.high + self.low) / 2

    def to_dict(self) -> Dict[str, Any]:
        return {
            'type': self.type,
            'high': self.high,
            'low': self.low,
            'midpoint': self.midpoint,
            'size': self.size,
            'size_percent': self.size_percent,
            'filled': self.filled,
            'fill_percent': self.fill_percent,
            'timestamp': self.timestamp.isoformat() if self.timestamp else None
        }


@dataclass
class LiquiditySweep:
    """Liquidity Sweep / Stop Hunt"""
    type: str  # 'high_sweep' or 'low_sweep'
    sweep_price: float  # Price that was swept
    reaction_price: float  # Where price reversed
    previous_level: float  # The level that was swept
    volume_spike: float  # Volume relative to average
    timestamp: datetime
    confirmed: bool = False

    def to_dict(self) -> Dict[str, Any]:
        return {
            'type': self.type,
            'sweep_price': self.sweep_price,
            'reaction_price': self.reaction_price,
            'previous_level': self.previous_level,
            'volume_spike': self.volume_spike,
            'confirmed': self.confirmed,
            'timestamp': self.timestamp.isoformat() if self.timestamp else None
        }


@dataclass
class StructureBreak:
    """Break of Structure or Change of Character"""
    type: StructureType
    direction: str  # 'bullish' or 'bearish'
    break_price: float
    previous_swing: float
    timestamp: datetime
    confirmed: bool = False

    def to_dict(self) -> Dict[str, Any]:
        return {
            'type': self.type.value,
            'direction': self.direction,
            'break_price': self.break_price,
            'previous_swing': self.previous_swing,
            'confirmed': self.confirmed,
            'timestamp': self.timestamp.isoformat() if self.timestamp else None
        }


@dataclass
class ICTAnalysis:
    """Complete ICT/SMC Analysis Result"""
    timestamp: datetime
    symbol: str
    timeframe: str

    # Market Structure
    market_bias: MarketBias
    bias_confidence: float
    current_trend: str  # 'uptrend', 'downtrend', 'ranging'

    # Key Levels
    order_blocks: List[OrderBlock] = field(default_factory=list)
    fair_value_gaps: List[FairValueGap] = field(default_factory=list)
    liquidity_sweeps: List[LiquiditySweep] = field(default_factory=list)
    structure_breaks: List[StructureBreak] = field(default_factory=list)

    # Trading Zones
    premium_zone: Tuple[float, float] = (0, 0)  # (low, high)
    discount_zone: Tuple[float, float] = (0, 0)  # (low, high)
    equilibrium: float = 0

    # Key Levels
    swing_highs: List[float] = field(default_factory=list)
    swing_lows: List[float] = field(default_factory=list)
    liquidity_pools: Dict[str, List[float]] = field(default_factory=dict)

    # Trade Setup
    entry_zone: Optional[Tuple[float, float]] = None
    stop_loss: Optional[float] = None
    take_profit_1: Optional[float] = None
    take_profit_2: Optional[float] = None
    take_profit_3: Optional[float] = None
    risk_reward: Optional[float] = None

    # Signals
    signals: List[str] = field(default_factory=list)
    score: float = 0  # Overall setup score 0-100

    def to_dict(self) -> Dict[str, Any]:
        return {
            'timestamp': self.timestamp.isoformat() if self.timestamp else None,
            'symbol': self.symbol,
            'timeframe': self.timeframe,
            'market_bias': self.market_bias.value,
            'bias_confidence': self.bias_confidence,
            'current_trend': self.current_trend,
            'order_blocks': [ob.to_dict() for ob in self.order_blocks],
            'fair_value_gaps': [fvg.to_dict() for fvg in self.fair_value_gaps],
            'liquidity_sweeps': [ls.to_dict() for ls in self.liquidity_sweeps],
            'structure_breaks': [sb.to_dict() for sb in self.structure_breaks],
            'premium_zone': {'low': self.premium_zone[0], 'high': self.premium_zone[1]},
            'discount_zone': {'low': self.discount_zone[0], 'high': self.discount_zone[1]},
            'equilibrium': self.equilibrium,
            'swing_highs': self.swing_highs[-5:] if self.swing_highs else [],
            'swing_lows': self.swing_lows[-5:] if self.swing_lows else [],
            'liquidity_pools': self.liquidity_pools,
            'entry_zone': {'low': self.entry_zone[0], 'high': self.entry_zone[1]} if self.entry_zone else None,
            'stop_loss': self.stop_loss,
            'take_profits': {
                'tp1': self.take_profit_1,
                'tp2': self.take_profit_2,
                'tp3': self.take_profit_3
            },
            'risk_reward': self.risk_reward,
            'signals': self.signals,
            'score': self.score
        }


class ICTSMCDetector:
    """
    ICT/SMC Pattern Detector

    Identifies institutional trading patterns based on Smart Money Concepts:
    - Order Blocks: Where institutions placed large orders
    - Fair Value Gaps: Price imbalances that tend to get filled
    - Liquidity Sweeps: Stop hunts before reversals
    - Market Structure: BOS and CHoCH for trend analysis
    """

    def __init__(
        self,
        swing_lookback: int = 10,
        ob_min_size: float = 0.001,  # Minimum OB size as fraction of price
        fvg_min_size: float = 0.0005,  # Minimum FVG size
        volume_spike_threshold: float = 1.5,  # Volume spike multiplier
        max_order_blocks: int = 5,  # Max OBs to track
        max_fvgs: int = 10  # Max FVGs to track
    ):
        self.swing_lookback = swing_lookback
        self.ob_min_size = ob_min_size
        self.fvg_min_size = fvg_min_size
        self.volume_spike_threshold = volume_spike_threshold
        self.max_order_blocks = max_order_blocks
        self.max_fvgs = max_fvgs

        logger.info("ICTSMCDetector initialized")

    def analyze(
        self,
        df: pd.DataFrame,
        symbol: str = "UNKNOWN",
        timeframe: str = "1H"
    ) -> ICTAnalysis:
        """
        Perform complete ICT/SMC analysis

        Args:
            df: OHLCV DataFrame with columns: open, high, low, close, volume
            symbol: Trading symbol
            timeframe: Timeframe string

        Returns:
            ICTAnalysis with complete market structure analysis
        """
        if len(df) < self.swing_lookback * 3:
            return self._empty_analysis(symbol, timeframe)

        # Ensure DataFrame has datetime index or timestamp column
        if not isinstance(df.index, pd.DatetimeIndex):
            if 'timestamp' in df.columns:
                df = df.set_index('timestamp')
            else:
                df.index = pd.to_datetime(df.index)

        # 1. Identify swing points
        swing_highs, swing_lows = self._find_swing_points(df)

        # 2. Detect market structure
        structure_breaks = self._detect_structure_breaks(df, swing_highs, swing_lows)
        current_trend, market_bias, bias_confidence = self._determine_trend(df, structure_breaks)

        # 3. Find Order Blocks
        order_blocks = self._find_order_blocks(df, swing_highs, swing_lows)

        # 4. Find Fair Value Gaps
        fair_value_gaps = self._find_fair_value_gaps(df)

        # 5. Detect Liquidity Sweeps
        liquidity_sweeps = self._detect_liquidity_sweeps(df, swing_highs, swing_lows)

        # 6. Calculate Premium/Discount zones
        premium_zone, discount_zone, equilibrium = self._calculate_zones(df, swing_highs, swing_lows)

        # 7. Identify Liquidity Pools
        liquidity_pools = self._find_liquidity_pools(swing_highs, swing_lows)

        # 8. Generate trade setup
        entry_zone, stop_loss, tp1, tp2, tp3, rr = self._generate_trade_setup(
            df, market_bias, order_blocks, fair_value_gaps,
            premium_zone, discount_zone, equilibrium
        )

        # 9. Generate signals
        signals = self._generate_signals(
            market_bias, order_blocks, fair_value_gaps,
            liquidity_sweeps, structure_breaks, df
        )

        # 10. Calculate overall score
        score = self._calculate_setup_score(
            market_bias, bias_confidence, order_blocks, fair_value_gaps,
            liquidity_sweeps, structure_breaks, rr
        )

        return ICTAnalysis(
            timestamp=df.index[-1] if isinstance(df.index[-1], datetime) else datetime.now(),
            symbol=symbol,
            timeframe=timeframe,
            market_bias=market_bias,
            bias_confidence=bias_confidence,
            current_trend=current_trend,
            order_blocks=order_blocks[:self.max_order_blocks],
            fair_value_gaps=fair_value_gaps[:self.max_fvgs],
            liquidity_sweeps=liquidity_sweeps[-5:],
            structure_breaks=structure_breaks[-5:],
            premium_zone=premium_zone,
            discount_zone=discount_zone,
            equilibrium=equilibrium,
            swing_highs=[h for _, h in swing_highs[-10:]],
            swing_lows=[l for _, l in swing_lows[-10:]],
            liquidity_pools=liquidity_pools,
            entry_zone=entry_zone,
            stop_loss=stop_loss,
            take_profit_1=tp1,
            take_profit_2=tp2,
            take_profit_3=tp3,
            risk_reward=rr,
            signals=signals,
            score=score
        )

    def _find_swing_points(
        self,
        df: pd.DataFrame
    ) -> Tuple[List[Tuple[int, float]], List[Tuple[int, float]]]:
        """Find swing highs and lows"""
        swing_highs = []
        swing_lows = []
        lookback = self.swing_lookback

        for i in range(lookback, len(df) - lookback):
            # Swing High: Higher than surrounding bars
            if df['high'].iloc[i] == df['high'].iloc[i-lookback:i+lookback+1].max():
                swing_highs.append((i, df['high'].iloc[i]))

            # Swing Low: Lower than surrounding bars
            if df['low'].iloc[i] == df['low'].iloc[i-lookback:i+lookback+1].min():
                swing_lows.append((i, df['low'].iloc[i]))

        return swing_highs, swing_lows

    def _detect_structure_breaks(
        self,
        df: pd.DataFrame,
        swing_highs: List[Tuple[int, float]],
        swing_lows: List[Tuple[int, float]]
    ) -> List[StructureBreak]:
        """Detect Break of Structure (BOS) and Change of Character (CHoCH)"""
        breaks = []

        if len(swing_highs) < 2 or len(swing_lows) < 2:
            return breaks

        # Track the trend
        last_hh = None  # Last Higher High
        last_ll = None  # Last Lower Low
        trend = 'neutral'

        # Combine and sort swings by index
        all_swings = [(i, h, 'high') for i, h in swing_highs] + [(i, l, 'low') for i, l in swing_lows]
        all_swings.sort(key=lambda x: x[0])

        for i in range(1, len(all_swings)):
            idx, price, swing_type = all_swings[i]
            prev_idx, prev_price, prev_type = all_swings[i-1]

            if swing_type == 'high':
                if last_hh is not None:
                    # Check for Higher High (bullish continuation)
                    if price > last_hh:
                        if trend == 'down':
                            # CHoCH - Change of Character (bearish to bullish)
                            breaks.append(StructureBreak(
                                type=StructureType.CHOCH,
                                direction='bullish',
                                break_price=price,
                                previous_swing=last_hh,
                                timestamp=df.index[idx] if idx < len(df) else datetime.now(),
                                confirmed=True
                            ))
                        else:
                            # BOS - Break of Structure (bullish)
                            breaks.append(StructureBreak(
                                type=StructureType.BOS,
                                direction='bullish',
                                break_price=price,
                                previous_swing=last_hh,
                                timestamp=df.index[idx] if idx < len(df) else datetime.now(),
                                confirmed=True
                            ))
                        trend = 'up'
                last_hh = price

            elif swing_type == 'low':
                if last_ll is not None:
                    # Check for Lower Low (bearish continuation)
                    if price < last_ll:
                        if trend == 'up':
                            # CHoCH - Change of Character (bullish to bearish)
                            breaks.append(StructureBreak(
                                type=StructureType.CHOCH,
                                direction='bearish',
                                break_price=price,
                                previous_swing=last_ll,
                                timestamp=df.index[idx] if idx < len(df) else datetime.now(),
                                confirmed=True
                            ))
                        else:
                            # BOS - Break of Structure (bearish)
                            breaks.append(StructureBreak(
                                type=StructureType.BOS,
                                direction='bearish',
                                break_price=price,
                                previous_swing=last_ll,
                                timestamp=df.index[idx] if idx < len(df) else datetime.now(),
                                confirmed=True
                            ))
                        trend = 'down'
                last_ll = price

        return breaks

    def _determine_trend(
        self,
        df: pd.DataFrame,
        structure_breaks: List[StructureBreak]
    ) -> Tuple[str, MarketBias, float]:
        """Determine current trend and market bias"""
        if not structure_breaks:
            # Use simple moving average for basic trend
            sma_20 = df['close'].rolling(20).mean().iloc[-1]
            sma_50 = df['close'].rolling(50).mean().iloc[-1]
            current_price = df['close'].iloc[-1]

            if current_price > sma_20 > sma_50:
                return 'uptrend', MarketBias.BULLISH, 0.6
            elif current_price < sma_20 < sma_50:
                return 'downtrend', MarketBias.BEARISH, 0.6
            else:
                return 'ranging', MarketBias.NEUTRAL, 0.5

        # Count recent structure breaks
        recent_breaks = structure_breaks[-5:]
        bullish_count = sum(1 for b in recent_breaks if b.direction == 'bullish')
        bearish_count = sum(1 for b in recent_breaks if b.direction == 'bearish')

        # Check last break
        last_break = structure_breaks[-1]

        # Determine trend
        if bullish_count > bearish_count:
            trend = 'uptrend'
            bias = MarketBias.BULLISH
            confidence = min(0.9, 0.5 + (bullish_count - bearish_count) * 0.1)
        elif bearish_count > bullish_count:
            trend = 'downtrend'
            bias = MarketBias.BEARISH
            confidence = min(0.9, 0.5 + (bearish_count - bullish_count) * 0.1)
        else:
            trend = 'ranging'
            bias = MarketBias.NEUTRAL
            confidence = 0.5

        # Boost confidence if last break is CHoCH
        if last_break.type == StructureType.CHOCH:
            confidence = min(0.95, confidence + 0.15)

        return trend, bias, confidence

    def _find_order_blocks(
        self,
        df: pd.DataFrame,
        swing_highs: List[Tuple[int, float]],
        swing_lows: List[Tuple[int, float]]
    ) -> List[OrderBlock]:
        """Find Order Blocks (institutional accumulation/distribution zones)"""
        order_blocks = []
        volume_ma = df['volume'].rolling(20).mean()

        # Find bullish Order Blocks (before up moves)
        for i, low_price in swing_lows:
            if i >= len(df) - 1:
                continue

            # Look for the last bearish candle before the swing low
            for j in range(i, max(0, i - 5), -1):
                if df['close'].iloc[j] < df['open'].iloc[j]:  # Bearish candle
                    # Check if followed by bullish move
                    if i + 3 < len(df):
                        future_high = df['high'].iloc[i:i+5].max()
                        move_size = (future_high - df['low'].iloc[j]) / df['close'].iloc[j]

                        if move_size > self.ob_min_size * 2:  # Significant move
                            ob_size = (df['high'].iloc[j] - df['low'].iloc[j]) / df['close'].iloc[j]

                            if ob_size >= self.ob_min_size:
                                # Check if OB was touched/broken
                                valid = True
                                touched = False
                                broken = False

                                for k in range(j + 1, len(df)):
                                    if df['low'].iloc[k] <= df['high'].iloc[j]:
                                        touched = True
                                    if df['close'].iloc[k] < df['low'].iloc[j]:
                                        broken = True
                                        valid = False
                                        break

                                # Calculate strength based on volume and move size
                                vol_ratio = df['volume'].iloc[j] / volume_ma.iloc[j] if volume_ma.iloc[j] > 0 else 1
                                strength = min(1.0, (move_size * 10 + vol_ratio * 0.3) / 2)

                                order_blocks.append(OrderBlock(
                                    type='bullish',
                                    high=df['high'].iloc[j],
                                    low=df['low'].iloc[j],
                                    open_price=df['open'].iloc[j],
                                    close_price=df['close'].iloc[j],
                                    volume=df['volume'].iloc[j],
                                    timestamp=df.index[j],
                                    strength=strength,
                                    valid=valid,
                                    touched=touched,
                                    broken=broken
                                ))
                    break

        # Find bearish Order Blocks (before down moves)
        for i, high_price in swing_highs:
            if i >= len(df) - 1:
                continue

            # Look for the last bullish candle before the swing high
            for j in range(i, max(0, i - 5), -1):
                if df['close'].iloc[j] > df['open'].iloc[j]:  # Bullish candle
                    # Check if followed by bearish move
                    if i + 3 < len(df):
                        future_low = df['low'].iloc[i:i+5].min()
                        move_size = (df['high'].iloc[j] - future_low) / df['close'].iloc[j]

                        if move_size > self.ob_min_size * 2:  # Significant move
                            ob_size = (df['high'].iloc[j] - df['low'].iloc[j]) / df['close'].iloc[j]

                            if ob_size >= self.ob_min_size:
                                # Check if OB was touched/broken
                                valid = True
                                touched = False
                                broken = False

                                for k in range(j + 1, len(df)):
                                    if df['high'].iloc[k] >= df['low'].iloc[j]:
                                        touched = True
                                    if df['close'].iloc[k] > df['high'].iloc[j]:
                                        broken = True
                                        valid = False
                                        break

                                # Calculate strength
                                vol_ratio = df['volume'].iloc[j] / volume_ma.iloc[j] if volume_ma.iloc[j] > 0 else 1
                                strength = min(1.0, (move_size * 10 + vol_ratio * 0.3) / 2)

                                order_blocks.append(OrderBlock(
                                    type='bearish',
                                    high=df['high'].iloc[j],
                                    low=df['low'].iloc[j],
                                    open_price=df['open'].iloc[j],
                                    close_price=df['close'].iloc[j],
                                    volume=df['volume'].iloc[j],
                                    timestamp=df.index[j],
                                    strength=strength,
                                    valid=valid,
                                    touched=touched,
                                    broken=broken
                                ))
                    break

        # Sort by strength and recency, prioritize valid blocks
        order_blocks.sort(key=lambda x: (x.valid, x.strength, x.timestamp), reverse=True)

        return order_blocks

    def _find_fair_value_gaps(self, df: pd.DataFrame) -> List[FairValueGap]:
        """Find Fair Value Gaps (price imbalances)"""
        fvgs = []

        for i in range(2, len(df)):
            # Bullish FVG: Gap between candle 1 high and candle 3 low
            if df['low'].iloc[i] > df['high'].iloc[i-2]:
                gap_size = df['low'].iloc[i] - df['high'].iloc[i-2]
                gap_percent = gap_size / df['close'].iloc[i]

                if gap_percent >= self.fvg_min_size:
                    # Check if gap was filled
                    filled = False
                    fill_percent = 0.0

                    for j in range(i + 1, len(df)):
                        if df['low'].iloc[j] <= df['high'].iloc[i-2]:
                            filled = True
                            fill_percent = 1.0
                            break
                        elif df['low'].iloc[j] < df['low'].iloc[i]:
                            # Partial fill
                            fill_percent = max(fill_percent,
                                (df['low'].iloc[i] - df['low'].iloc[j]) / gap_size)

                    fvgs.append(FairValueGap(
                        type='bullish',
                        high=df['low'].iloc[i],
                        low=df['high'].iloc[i-2],
                        size=gap_size,
                        size_percent=gap_percent * 100,
                        timestamp=df.index[i],
                        filled=filled,
                        fill_percent=fill_percent
                    ))

            # Bearish FVG: Gap between candle 3 high and candle 1 low
            if df['high'].iloc[i] < df['low'].iloc[i-2]:
                gap_size = df['low'].iloc[i-2] - df['high'].iloc[i]
                gap_percent = gap_size / df['close'].iloc[i]

                if gap_percent >= self.fvg_min_size:
                    # Check if gap was filled
                    filled = False
                    fill_percent = 0.0

                    for j in range(i + 1, len(df)):
                        if df['high'].iloc[j] >= df['low'].iloc[i-2]:
                            filled = True
                            fill_percent = 1.0
                            break
                        elif df['high'].iloc[j] > df['high'].iloc[i]:
                            # Partial fill
                            fill_percent = max(fill_percent,
                                (df['high'].iloc[j] - df['high'].iloc[i]) / gap_size)

                    fvgs.append(FairValueGap(
                        type='bearish',
                        high=df['low'].iloc[i-2],
                        low=df['high'].iloc[i],
                        size=gap_size,
                        size_percent=gap_percent * 100,
                        timestamp=df.index[i],
                        filled=filled,
                        fill_percent=fill_percent
                    ))

        # Sort by recency, prioritize unfilled gaps
        fvgs.sort(key=lambda x: (not x.filled, x.timestamp), reverse=True)

        return fvgs

    def _detect_liquidity_sweeps(
        self,
        df: pd.DataFrame,
        swing_highs: List[Tuple[int, float]],
        swing_lows: List[Tuple[int, float]]
    ) -> List[LiquiditySweep]:
        """Detect liquidity sweeps (stop hunts)"""
        sweeps = []
        volume_ma = df['volume'].rolling(20).mean()

        # High sweeps (sweep of highs followed by reversal)
        for i, high_price in swing_highs:
            if i >= len(df) - 3:
                continue

            # Check for sweep above the high
            for j in range(i + 1, min(i + 10, len(df) - 1)):
                if df['high'].iloc[j] > high_price:
                    # Check for reversal (close below the high)
                    if df['close'].iloc[j] < high_price or \
                       (j + 1 < len(df) and df['close'].iloc[j+1] < high_price):

                        vol_spike = df['volume'].iloc[j] / volume_ma.iloc[j] if volume_ma.iloc[j] > 0 else 1

                        sweeps.append(LiquiditySweep(
                            type='high_sweep',
                            sweep_price=df['high'].iloc[j],
                            reaction_price=min(df['close'].iloc[j], df['low'].iloc[j]),
                            previous_level=high_price,
                            volume_spike=vol_spike,
                            timestamp=df.index[j],
                            confirmed=vol_spike > self.volume_spike_threshold
                        ))
                    break

        # Low sweeps (sweep of lows followed by reversal)
        for i, low_price in swing_lows:
            if i >= len(df) - 3:
                continue

            # Check for sweep below the low
            for j in range(i + 1, min(i + 10, len(df) - 1)):
                if df['low'].iloc[j] < low_price:
                    # Check for reversal (close above the low)
                    if df['close'].iloc[j] > low_price or \
                       (j + 1 < len(df) and df['close'].iloc[j+1] > low_price):

                        vol_spike = df['volume'].iloc[j] / volume_ma.iloc[j] if volume_ma.iloc[j] > 0 else 1

                        sweeps.append(LiquiditySweep(
                            type='low_sweep',
                            sweep_price=df['low'].iloc[j],
                            reaction_price=max(df['close'].iloc[j], df['high'].iloc[j]),
                            previous_level=low_price,
                            volume_spike=vol_spike,
                            timestamp=df.index[j],
                            confirmed=vol_spike > self.volume_spike_threshold
                        ))
                    break

        return sweeps

    def _calculate_zones(
        self,
        df: pd.DataFrame,
        swing_highs: List[Tuple[int, float]],
        swing_lows: List[Tuple[int, float]]
    ) -> Tuple[Tuple[float, float], Tuple[float, float], float]:
        """Calculate Premium/Discount zones using Fibonacci"""
        if not swing_highs or not swing_lows:
            current = df['close'].iloc[-1]
            return (current, current), (current, current), current

        # Get recent range
        recent_high = max(h for _, h in swing_highs[-5:]) if swing_highs else df['high'].iloc[-20:].max()
        recent_low = min(l for _, l in swing_lows[-5:]) if swing_lows else df['low'].iloc[-20:].min()

        range_size = recent_high - recent_low
        equilibrium = recent_low + range_size * 0.5

        # Premium zone: 0.618 - 1.0 of range (upper)
        premium_low = recent_low + range_size * 0.618
        premium_high = recent_high

        # Discount zone: 0.0 - 0.382 of range (lower)
        discount_low = recent_low
        discount_high = recent_low + range_size * 0.382

        return (premium_low, premium_high), (discount_low, discount_high), equilibrium

    def _find_liquidity_pools(
        self,
        swing_highs: List[Tuple[int, float]],
        swing_lows: List[Tuple[int, float]]
    ) -> Dict[str, List[float]]:
        """Find clusters of liquidity (stop losses)"""
        return {
            'buy_side': [h for _, h in swing_highs[-10:]],  # Stops above highs
            'sell_side': [l for _, l in swing_lows[-10:]]   # Stops below lows
        }

    def _generate_trade_setup(
        self,
        df: pd.DataFrame,
        market_bias: MarketBias,
        order_blocks: List[OrderBlock],
        fair_value_gaps: List[FairValueGap],
        premium_zone: Tuple[float, float],
        discount_zone: Tuple[float, float],
        equilibrium: float
    ) -> Tuple[Optional[Tuple[float, float]], Optional[float], Optional[float], Optional[float], Optional[float], Optional[float]]:
        """Generate trade setup based on ICT analysis"""
        current_price = df['close'].iloc[-1]

        if market_bias == MarketBias.BULLISH:
            # Look for entries in discount zone or at bullish OBs
            valid_obs = [ob for ob in order_blocks if ob.type == 'bullish' and ob.valid and not ob.broken]
            unfilled_fvgs = [fvg for fvg in fair_value_gaps if fvg.type == 'bullish' and not fvg.filled]

            if valid_obs:
                # Entry at order block
                ob = valid_obs[0]
                entry_zone = (ob.low, ob.midpoint)
                stop_loss = ob.low - (ob.high - ob.low) * 0.5  # Below OB

            elif unfilled_fvgs:
                # Entry at FVG
                fvg = unfilled_fvgs[0]
                entry_zone = (fvg.low, fvg.midpoint)
                stop_loss = fvg.low - fvg.size  # Below FVG

            elif current_price < discount_zone[1]:
                # Entry in discount zone
                entry_zone = discount_zone
                stop_loss = discount_zone[0] - (discount_zone[1] - discount_zone[0]) * 0.5

            else:
                return None, None, None, None, None, None

            # Take profits
            tp1 = equilibrium
            tp2 = premium_zone[0]
            tp3 = premium_zone[1]

        elif market_bias == MarketBias.BEARISH:
            # Look for entries in premium zone or at bearish OBs
            valid_obs = [ob for ob in order_blocks if ob.type == 'bearish' and ob.valid and not ob.broken]
            unfilled_fvgs = [fvg for fvg in fair_value_gaps if fvg.type == 'bearish' and not fvg.filled]

            if valid_obs:
                # Entry at order block
                ob = valid_obs[0]
                entry_zone = (ob.midpoint, ob.high)
                stop_loss = ob.high + (ob.high - ob.low) * 0.5  # Above OB

            elif unfilled_fvgs:
                # Entry at FVG
                fvg = unfilled_fvgs[0]
                entry_zone = (fvg.midpoint, fvg.high)
                stop_loss = fvg.high + fvg.size  # Above FVG

            elif current_price > premium_zone[0]:
                # Entry in premium zone
                entry_zone = premium_zone
                stop_loss = premium_zone[1] + (premium_zone[1] - premium_zone[0]) * 0.5

            else:
                return None, None, None, None, None, None

            # Take profits
            tp1 = equilibrium
            tp2 = discount_zone[1]
            tp3 = discount_zone[0]

        else:
            return None, None, None, None, None, None

        # Calculate risk/reward
        entry_mid = (entry_zone[0] + entry_zone[1]) / 2
        risk = abs(entry_mid - stop_loss)
        reward = abs(tp2 - entry_mid) if tp2 else abs(tp1 - entry_mid)
        rr = reward / risk if risk > 0 else 0

        return entry_zone, stop_loss, tp1, tp2, tp3, round(rr, 2)

    def _generate_signals(
        self,
        market_bias: MarketBias,
        order_blocks: List[OrderBlock],
        fair_value_gaps: List[FairValueGap],
        liquidity_sweeps: List[LiquiditySweep],
        structure_breaks: List[StructureBreak],
        df: pd.DataFrame
    ) -> List[str]:
        """Generate trading signals based on analysis"""
        signals = []
        current_price = df['close'].iloc[-1]

        # Bias signals
        if market_bias == MarketBias.BULLISH:
            signals.append("BULLISH_BIAS")
        elif market_bias == MarketBias.BEARISH:
            signals.append("BEARISH_BIAS")

        # Structure signals
        if structure_breaks:
            last_break = structure_breaks[-1]
            if last_break.type == StructureType.CHOCH:
                signals.append(f"CHOCH_{last_break.direction.upper()}")
            elif last_break.type == StructureType.BOS:
                signals.append(f"BOS_{last_break.direction.upper()}")

        # Order Block signals
        valid_bullish_obs = [ob for ob in order_blocks if ob.type == 'bullish' and ob.valid]
        valid_bearish_obs = [ob for ob in order_blocks if ob.type == 'bearish' and ob.valid]

        for ob in valid_bullish_obs[:2]:
            if ob.low <= current_price <= ob.high:
                signals.append("PRICE_IN_BULLISH_OB")
            elif current_price > ob.high and not ob.touched:
                signals.append("BULLISH_OB_BELOW")

        for ob in valid_bearish_obs[:2]:
            if ob.low <= current_price <= ob.high:
                signals.append("PRICE_IN_BEARISH_OB")
            elif current_price < ob.low and not ob.touched:
                signals.append("BEARISH_OB_ABOVE")

        # FVG signals
        unfilled_fvgs = [fvg for fvg in fair_value_gaps if not fvg.filled]
        for fvg in unfilled_fvgs[:2]:
            if fvg.low <= current_price <= fvg.high:
                signals.append(f"PRICE_IN_{fvg.type.upper()}_FVG")
            elif fvg.type == 'bullish' and current_price > fvg.high:
                signals.append("UNFILLED_BULLISH_FVG_BELOW")
            elif fvg.type == 'bearish' and current_price < fvg.low:
                signals.append("UNFILLED_BEARISH_FVG_ABOVE")

        # Liquidity sweep signals
        recent_sweeps = [s for s in liquidity_sweeps if s.confirmed][-2:]
        for sweep in recent_sweeps:
            if sweep.type == 'low_sweep':
                signals.append("LIQUIDITY_SWEEP_LOWS")
            else:
                signals.append("LIQUIDITY_SWEEP_HIGHS")

        return signals

    def _calculate_setup_score(
        self,
        market_bias: MarketBias,
        bias_confidence: float,
        order_blocks: List[OrderBlock],
        fair_value_gaps: List[FairValueGap],
        liquidity_sweeps: List[LiquiditySweep],
        structure_breaks: List[StructureBreak],
        risk_reward: Optional[float]
    ) -> float:
        """Calculate overall setup quality score (0-100)"""
        score = 0

        # Bias contribution (0-25)
        if market_bias != MarketBias.NEUTRAL:
            score += bias_confidence * 25

        # Structure contribution (0-20)
        if structure_breaks:
            last_break = structure_breaks[-1]
            if last_break.type == StructureType.CHOCH:
                score += 20
            elif last_break.type == StructureType.BOS:
                score += 15

        # Order Blocks contribution (0-20)
        valid_obs = [ob for ob in order_blocks if ob.valid and not ob.broken]
        if valid_obs:
            avg_strength = sum(ob.strength for ob in valid_obs[:3]) / min(3, len(valid_obs))
            score += avg_strength * 20

        # FVG contribution (0-15)
        unfilled_fvgs = [fvg for fvg in fair_value_gaps if not fvg.filled]
        if unfilled_fvgs:
            score += min(15, len(unfilled_fvgs) * 5)

        # Liquidity sweep contribution (0-10)
        confirmed_sweeps = [s for s in liquidity_sweeps if s.confirmed]
        if confirmed_sweeps:
            score += min(10, len(confirmed_sweeps) * 5)

        # Risk/Reward contribution (0-10)
        if risk_reward:
            if risk_reward >= 3:
                score += 10
            elif risk_reward >= 2:
                score += 7
            elif risk_reward >= 1.5:
                score += 5

        return min(100, round(score, 1))

    def _empty_analysis(self, symbol: str, timeframe: str) -> ICTAnalysis:
        """Return empty analysis when not enough data"""
        return ICTAnalysis(
            timestamp=datetime.now(),
            symbol=symbol,
            timeframe=timeframe,
            market_bias=MarketBias.NEUTRAL,
            bias_confidence=0,
            current_trend='unknown',
            score=0
        )

    def get_trade_recommendation(self, analysis: ICTAnalysis) -> Dict[str, Any]:
        """
        Get a simple trade recommendation from ICT analysis

        Returns:
            Dictionary with action, entry, stop_loss, take_profit, confidence
        """
        if analysis.score < 50 or analysis.market_bias == MarketBias.NEUTRAL:
            return {
                'action': 'HOLD',
                'reason': 'No high-probability setup detected',
                'score': analysis.score
            }

        if analysis.market_bias == MarketBias.BULLISH and analysis.entry_zone:
            return {
                'action': 'BUY',
                'entry_zone': {
                    'low': analysis.entry_zone[0],
                    'high': analysis.entry_zone[1]
                },
                'stop_loss': analysis.stop_loss,
                'take_profit_1': analysis.take_profit_1,
                'take_profit_2': analysis.take_profit_2,
                'take_profit_3': analysis.take_profit_3,
                'risk_reward': analysis.risk_reward,
                'confidence': analysis.bias_confidence,
                'score': analysis.score,
                'signals': analysis.signals
            }

        elif analysis.market_bias == MarketBias.BEARISH and analysis.entry_zone:
            return {
                'action': 'SELL',
                'entry_zone': {
                    'low': analysis.entry_zone[0],
                    'high': analysis.entry_zone[1]
                },
                'stop_loss': analysis.stop_loss,
                'take_profit_1': analysis.take_profit_1,
                'take_profit_2': analysis.take_profit_2,
                'take_profit_3': analysis.take_profit_3,
                'risk_reward': analysis.risk_reward,
                'confidence': analysis.bias_confidence,
                'score': analysis.score,
                'signals': analysis.signals
            }

        return {
            'action': 'HOLD',
            'reason': 'Setup conditions not met',
            'score': analysis.score
        }
