"""
MSA (Market Structure Analysis) - Structure Detector
====================================================
Detection of market structure using ICT/SMC concepts:
- Swing Points (HH, HL, LH, LL)
- Break of Structure (BOS)
- Change of Character (CHoCH)
- Fair Value Gaps (FVG)
- Order Blocks (OB)
- Premium/Discount Zones

Author: ML-Specialist (NEXUS v4.0)
Created: 2026-01-25
Version: 1.0.0
"""

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 SwingType(str, Enum):
    """Swing point type"""
    HIGHER_HIGH = "HH"
    HIGHER_LOW = "HL"
    LOWER_HIGH = "LH"
    LOWER_LOW = "LL"


class StructureType(str, Enum):
    """Market structure event type"""
    BOS_BULLISH = "bos_bullish"
    BOS_BEARISH = "bos_bearish"
    CHOCH_BULLISH = "choch_bullish"
    CHOCH_BEARISH = "choch_bearish"


class POIType(str, Enum):
    """Point of Interest type"""
    ORDER_BLOCK = "order_block"
    FAIR_VALUE_GAP = "fair_value_gap"
    SWING_HIGH = "swing_high"
    SWING_LOW = "swing_low"
    LIQUIDITY_LEVEL = "liquidity_level"
    NONE = "none"


@dataclass
class SwingPoint:
    """Swing point data structure"""
    index: int
    price: float
    swing_type: SwingType
    timestamp: Optional[datetime] = None
    confirmed: bool = True

    def to_dict(self) -> Dict[str, Any]:
        return {
            'index': self.index,
            'price': self.price,
            'swing_type': self.swing_type.value,
            'timestamp': self.timestamp.isoformat() if self.timestamp else None,
            'confirmed': self.confirmed
        }


@dataclass
class StructureEvent:
    """Break of Structure or Change of Character event"""
    event_type: StructureType
    index: int
    break_price: float
    previous_swing_price: float
    timestamp: Optional[datetime] = None
    confirmed: bool = True

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


@dataclass
class FairValueGap:
    """Fair Value Gap (3-candle imbalance)"""
    gap_type: str  # 'bullish' or 'bearish'
    index: int
    high: float
    low: float
    size: float
    size_pct: float
    timestamp: Optional[datetime] = None
    filled: bool = False
    fill_pct: float = 0.0

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

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


@dataclass
class OrderBlock:
    """Order Block (last opposite candle before BOS)"""
    ob_type: str  # 'bullish' or 'bearish'
    index: int
    high: float
    low: float
    open_price: float
    close_price: float
    volume: float
    strength: float
    timestamp: Optional[datetime] = None
    valid: bool = True
    touched: bool = False
    broken: bool = False

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

    def to_dict(self) -> Dict[str, Any]:
        return {
            'ob_type': self.ob_type,
            'index': self.index,
            'high': self.high,
            'low': self.low,
            'midpoint': self.midpoint,
            'open': self.open_price,
            'close': self.close_price,
            'volume': self.volume,
            'strength': self.strength,
            'timestamp': self.timestamp.isoformat() if self.timestamp else None,
            'valid': self.valid,
            'touched': self.touched,
            'broken': self.broken
        }


@dataclass
class LiquidityLevel:
    """Liquidity level (equal highs/lows)"""
    level_type: str  # 'equal_highs' or 'equal_lows'
    price: float
    indices: List[int] = field(default_factory=list)
    strength: int = 0  # Number of touches
    swept: bool = False

    def to_dict(self) -> Dict[str, Any]:
        return {
            'level_type': self.level_type,
            'price': self.price,
            'indices': self.indices,
            'strength': self.strength,
            'swept': self.swept
        }


@dataclass
class PremiumDiscountZone:
    """Premium/Discount zone based on recent swing range"""
    premium_low: float
    premium_high: float
    discount_low: float
    discount_high: float
    equilibrium: float
    current_zone: str  # 'premium', 'discount', 'equilibrium'

    def to_dict(self) -> Dict[str, Any]:
        return {
            'premium': {'low': self.premium_low, 'high': self.premium_high},
            'discount': {'low': self.discount_low, 'high': self.discount_high},
            'equilibrium': self.equilibrium,
            'current_zone': self.current_zone
        }


class StructureDetector:
    """
    Market Structure Analysis Detector

    Detects and analyzes market structure using ICT/SMC concepts:
    - Swing points detection with configurable order
    - Higher Highs (HH), Higher Lows (HL), Lower Highs (LH), Lower Lows (LL)
    - Break of Structure (BOS) - continuation pattern
    - Change of Character (CHoCH) - reversal signal
    - Fair Value Gaps (FVG) - 3-candle imbalances
    - Order Blocks (OB) - institutional accumulation/distribution zones
    - Premium/Discount zones for optimal entry identification
    """

    def __init__(
        self,
        swing_order: int = 5,
        fvg_min_size_pct: float = 0.001,
        ob_min_size_pct: float = 0.001,
        liquidity_tolerance_pct: float = 0.0005,
        max_ob_lookback: int = 10
    ):
        """
        Initialize Structure Detector.

        Args:
            swing_order: Number of bars on each side for swing detection
            fvg_min_size_pct: Minimum FVG size as percentage of price
            ob_min_size_pct: Minimum Order Block size as percentage of price
            liquidity_tolerance_pct: Tolerance for equal highs/lows detection
            max_ob_lookback: Maximum lookback for Order Block detection before BOS
        """
        self.swing_order = swing_order
        self.fvg_min_size_pct = fvg_min_size_pct
        self.ob_min_size_pct = ob_min_size_pct
        self.liquidity_tolerance_pct = liquidity_tolerance_pct
        self.max_ob_lookback = max_ob_lookback

        logger.info(
            f"StructureDetector initialized: order={swing_order}, "
            f"fvg_min={fvg_min_size_pct:.4f}, ob_min={ob_min_size_pct:.4f}"
        )

    def detect_swing_points(
        self,
        df: pd.DataFrame,
        order: Optional[int] = None
    ) -> Tuple[List[SwingPoint], List[SwingPoint]]:
        """
        Detect swing highs and swing lows.

        Uses a rolling window approach where a swing high is the highest point
        within 'order' bars on each side, and swing low is the lowest point.

        Args:
            df: OHLCV DataFrame
            order: Number of bars on each side (uses default if None)

        Returns:
            Tuple of (swing_highs, swing_lows) as lists of SwingPoint
        """
        order = order or self.swing_order
        swing_highs = []
        swing_lows = []

        if len(df) < order * 2 + 1:
            logger.warning(f"Insufficient data for swing detection: {len(df)} < {order * 2 + 1}")
            return swing_highs, swing_lows

        high = df['high'].values
        low = df['low'].values

        # Detect swing points
        for i in range(order, len(df) - order):
            # Swing High: highest point in the window
            window_high = high[i - order:i + order + 1]
            if high[i] == window_high.max() and high[i] == window_high[order]:
                ts = df.index[i] if isinstance(df.index, pd.DatetimeIndex) else None
                swing_highs.append(SwingPoint(
                    index=i,
                    price=float(high[i]),
                    swing_type=SwingType.HIGHER_HIGH,  # Will be updated later
                    timestamp=ts,
                    confirmed=True
                ))

            # Swing Low: lowest point in the window
            window_low = low[i - order:i + order + 1]
            if low[i] == window_low.min() and low[i] == window_low[order]:
                ts = df.index[i] if isinstance(df.index, pd.DatetimeIndex) else None
                swing_lows.append(SwingPoint(
                    index=i,
                    price=float(low[i]),
                    swing_type=SwingType.HIGHER_LOW,  # Will be updated later
                    timestamp=ts,
                    confirmed=True
                ))

        logger.debug(f"Detected {len(swing_highs)} swing highs and {len(swing_lows)} swing lows")
        return swing_highs, swing_lows

    def detect_higher_highs_lower_lows(
        self,
        swing_highs: List[SwingPoint],
        swing_lows: List[SwingPoint]
    ) -> Tuple[List[SwingPoint], List[SwingPoint]]:
        """
        Classify swing points as HH, HL, LH, LL.

        - HH (Higher High): Current swing high > previous swing high
        - HL (Higher Low): Current swing low > previous swing low
        - LH (Lower High): Current swing high < previous swing high
        - LL (Lower Low): Current swing low < previous swing low

        Args:
            swing_highs: List of swing high points
            swing_lows: List of swing low points

        Returns:
            Tuple of (classified_highs, classified_lows)
        """
        classified_highs = []
        classified_lows = []

        # Classify swing highs
        for i, sh in enumerate(swing_highs):
            if i == 0:
                # First swing cannot be classified relative to previous
                sh.swing_type = SwingType.HIGHER_HIGH
            else:
                prev_high = swing_highs[i - 1].price
                if sh.price > prev_high:
                    sh.swing_type = SwingType.HIGHER_HIGH
                else:
                    sh.swing_type = SwingType.LOWER_HIGH
            classified_highs.append(sh)

        # Classify swing lows
        for i, sl in enumerate(swing_lows):
            if i == 0:
                sl.swing_type = SwingType.HIGHER_LOW
            else:
                prev_low = swing_lows[i - 1].price
                if sl.price > prev_low:
                    sl.swing_type = SwingType.HIGHER_LOW
                else:
                    sl.swing_type = SwingType.LOWER_LOW
            classified_lows.append(sl)

        return classified_highs, classified_lows

    def detect_break_of_structure(
        self,
        df: pd.DataFrame,
        swing_highs: List[SwingPoint],
        swing_lows: List[SwingPoint]
    ) -> List[StructureEvent]:
        """
        Detect Break of Structure (BOS) events.

        BOS occurs when:
        - Bullish BOS: Price breaks above a previous swing high in an uptrend
        - Bearish BOS: Price breaks below a previous swing low in a downtrend

        BOS confirms trend continuation.

        Args:
            df: OHLCV DataFrame
            swing_highs: List of swing high points
            swing_lows: List of swing low points

        Returns:
            List of BOS StructureEvent
        """
        bos_events = []
        close = df['close'].values
        high = df['high'].values
        low = df['low'].values

        # Track the most recent significant swings
        last_swing_high = None
        last_swing_low = None
        trend = 'neutral'  # 'bullish', 'bearish', 'neutral'

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

        for i in range(1, len(all_swings)):
            idx, price, swing_kind, swing_obj = all_swings[i]

            # Look for breaks after the swing is formed
            for j in range(idx + 1, min(idx + 50, len(df))):
                if swing_kind == 'high' and last_swing_high is not None:
                    # Check for bullish BOS (close above previous swing high)
                    if close[j] > last_swing_high.price and trend in ['bullish', 'neutral']:
                        ts = df.index[j] if isinstance(df.index, pd.DatetimeIndex) else None
                        bos_events.append(StructureEvent(
                            event_type=StructureType.BOS_BULLISH,
                            index=j,
                            break_price=float(close[j]),
                            previous_swing_price=last_swing_high.price,
                            timestamp=ts,
                            confirmed=True
                        ))
                        trend = 'bullish'
                        break

                if swing_kind == 'low' and last_swing_low is not None:
                    # Check for bearish BOS (close below previous swing low)
                    if close[j] < last_swing_low.price and trend in ['bearish', 'neutral']:
                        ts = df.index[j] if isinstance(df.index, pd.DatetimeIndex) else None
                        bos_events.append(StructureEvent(
                            event_type=StructureType.BOS_BEARISH,
                            index=j,
                            break_price=float(close[j]),
                            previous_swing_price=last_swing_low.price,
                            timestamp=ts,
                            confirmed=True
                        ))
                        trend = 'bearish'
                        break

            # Update last swings
            if swing_kind == 'high':
                last_swing_high = swing_obj
            else:
                last_swing_low = swing_obj

        logger.debug(f"Detected {len(bos_events)} BOS events")
        return bos_events

    def detect_change_of_character(
        self,
        df: pd.DataFrame,
        swing_highs: List[SwingPoint],
        swing_lows: List[SwingPoint]
    ) -> List[StructureEvent]:
        """
        Detect Change of Character (CHoCH) events.

        CHoCH occurs when:
        - Bullish CHoCH: Price breaks above swing high after a series of LH/LL
        - Bearish CHoCH: Price breaks below swing low after a series of HH/HL

        CHoCH signals potential trend reversal.

        Args:
            df: OHLCV DataFrame
            swing_highs: List of swing high points
            swing_lows: List of swing low points

        Returns:
            List of CHoCH StructureEvent
        """
        choch_events = []
        close = df['close'].values

        # Classify swings first
        classified_highs, classified_lows = self.detect_higher_highs_lower_lows(
            swing_highs.copy(), swing_lows.copy()
        )

        # Detect bearish to bullish CHoCH
        # Look for sequence of LH followed by break above recent LH
        for i in range(2, len(classified_highs)):
            curr = classified_highs[i]
            prev = classified_highs[i - 1]
            prev2 = classified_highs[i - 2]

            # Sequence of Lower Highs (bearish structure)
            if (prev.swing_type == SwingType.LOWER_HIGH and
                prev2.swing_type == SwingType.LOWER_HIGH):
                # Look for break above the most recent LH
                for j in range(curr.index + 1, min(curr.index + 30, len(df))):
                    if close[j] > prev.price:
                        ts = df.index[j] if isinstance(df.index, pd.DatetimeIndex) else None
                        choch_events.append(StructureEvent(
                            event_type=StructureType.CHOCH_BULLISH,
                            index=j,
                            break_price=float(close[j]),
                            previous_swing_price=prev.price,
                            timestamp=ts,
                            confirmed=True
                        ))
                        break

        # Detect bullish to bearish CHoCH
        # Look for sequence of HL followed by break below recent HL
        for i in range(2, len(classified_lows)):
            curr = classified_lows[i]
            prev = classified_lows[i - 1]
            prev2 = classified_lows[i - 2]

            # Sequence of Higher Lows (bullish structure)
            if (prev.swing_type == SwingType.HIGHER_LOW and
                prev2.swing_type == SwingType.HIGHER_LOW):
                # Look for break below the most recent HL
                for j in range(curr.index + 1, min(curr.index + 30, len(df))):
                    if close[j] < prev.price:
                        ts = df.index[j] if isinstance(df.index, pd.DatetimeIndex) else None
                        choch_events.append(StructureEvent(
                            event_type=StructureType.CHOCH_BEARISH,
                            index=j,
                            break_price=float(close[j]),
                            previous_swing_price=prev.price,
                            timestamp=ts,
                            confirmed=True
                        ))
                        break

        logger.debug(f"Detected {len(choch_events)} CHoCH events")
        return choch_events

    def detect_fair_value_gaps(
        self,
        df: pd.DataFrame
    ) -> List[FairValueGap]:
        """
        Detect Fair Value Gaps (FVG) - 3-candle imbalances.

        Bullish FVG: Gap between candle 1 high and candle 3 low (price skipped up)
        Bearish FVG: Gap between candle 3 high and candle 1 low (price skipped down)

        Args:
            df: OHLCV DataFrame

        Returns:
            List of FairValueGap objects
        """
        fvgs = []
        high = df['high'].values
        low = df['low'].values
        close = df['close'].values

        for i in range(2, len(df)):
            # Bullish FVG: Low of candle 3 > High of candle 1
            if low[i] > high[i - 2]:
                gap_size = low[i] - high[i - 2]
                gap_pct = gap_size / close[i]

                if gap_pct >= self.fvg_min_size_pct:
                    ts = df.index[i] if isinstance(df.index, pd.DatetimeIndex) else None

                    # Check if gap was filled
                    filled = False
                    fill_pct = 0.0
                    for j in range(i + 1, len(df)):
                        if low[j] <= high[i - 2]:
                            filled = True
                            fill_pct = 1.0
                            break
                        elif low[j] < low[i]:
                            fill_pct = max(fill_pct, (low[i] - low[j]) / gap_size)

                    fvgs.append(FairValueGap(
                        gap_type='bullish',
                        index=i,
                        high=float(low[i]),
                        low=float(high[i - 2]),
                        size=float(gap_size),
                        size_pct=float(gap_pct * 100),
                        timestamp=ts,
                        filled=filled,
                        fill_pct=float(fill_pct)
                    ))

            # Bearish FVG: High of candle 3 < Low of candle 1
            if high[i] < low[i - 2]:
                gap_size = low[i - 2] - high[i]
                gap_pct = gap_size / close[i]

                if gap_pct >= self.fvg_min_size_pct:
                    ts = df.index[i] if isinstance(df.index, pd.DatetimeIndex) else None

                    # Check if gap was filled
                    filled = False
                    fill_pct = 0.0
                    for j in range(i + 1, len(df)):
                        if high[j] >= low[i - 2]:
                            filled = True
                            fill_pct = 1.0
                            break
                        elif high[j] > high[i]:
                            fill_pct = max(fill_pct, (high[j] - high[i]) / gap_size)

                    fvgs.append(FairValueGap(
                        gap_type='bearish',
                        index=i,
                        high=float(low[i - 2]),
                        low=float(high[i]),
                        size=float(gap_size),
                        size_pct=float(gap_pct * 100),
                        timestamp=ts,
                        filled=filled,
                        fill_pct=float(fill_pct)
                    ))

        logger.debug(f"Detected {len(fvgs)} FVGs (Bullish: {sum(1 for f in fvgs if f.gap_type == 'bullish')}, Bearish: {sum(1 for f in fvgs if f.gap_type == 'bearish')})")
        return fvgs

    def detect_order_blocks(
        self,
        df: pd.DataFrame,
        bos_events: List[StructureEvent]
    ) -> List[OrderBlock]:
        """
        Detect Order Blocks - last opposite candle before BOS.

        Bullish OB: Last bearish candle before a bullish BOS
        Bearish OB: Last bullish candle before a bearish BOS

        Args:
            df: OHLCV DataFrame
            bos_events: List of BOS events from detect_break_of_structure

        Returns:
            List of OrderBlock objects
        """
        order_blocks = []
        open_prices = df['open'].values
        high = df['high'].values
        low = df['low'].values
        close = df['close'].values
        volume = df['volume'].values if 'volume' in df.columns else np.ones(len(df))

        volume_ma = pd.Series(volume).rolling(20).mean().values

        for bos in bos_events:
            bos_idx = bos.index

            if bos.event_type == StructureType.BOS_BULLISH:
                # Look for last bearish candle before BOS
                for j in range(bos_idx - 1, max(0, bos_idx - self.max_ob_lookback), -1):
                    if close[j] < open_prices[j]:  # Bearish candle
                        ob_size_pct = (high[j] - low[j]) / close[j]
                        if ob_size_pct >= self.ob_min_size_pct:
                            ts = df.index[j] if isinstance(df.index, pd.DatetimeIndex) else None

                            # Calculate strength
                            vol_ratio = volume[j] / volume_ma[j] if volume_ma[j] > 0 else 1.0
                            move_after = (bos.break_price - low[j]) / close[j]
                            strength = min(1.0, (move_after * 5 + vol_ratio * 0.3) / 2)

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

                            order_blocks.append(OrderBlock(
                                ob_type='bullish',
                                index=j,
                                high=float(high[j]),
                                low=float(low[j]),
                                open_price=float(open_prices[j]),
                                close_price=float(close[j]),
                                volume=float(volume[j]),
                                strength=float(strength),
                                timestamp=ts,
                                valid=valid,
                                touched=touched,
                                broken=broken
                            ))
                        break

            elif bos.event_type == StructureType.BOS_BEARISH:
                # Look for last bullish candle before BOS
                for j in range(bos_idx - 1, max(0, bos_idx - self.max_ob_lookback), -1):
                    if close[j] > open_prices[j]:  # Bullish candle
                        ob_size_pct = (high[j] - low[j]) / close[j]
                        if ob_size_pct >= self.ob_min_size_pct:
                            ts = df.index[j] if isinstance(df.index, pd.DatetimeIndex) else None

                            # Calculate strength
                            vol_ratio = volume[j] / volume_ma[j] if volume_ma[j] > 0 else 1.0
                            move_after = (high[j] - bos.break_price) / close[j]
                            strength = min(1.0, (move_after * 5 + vol_ratio * 0.3) / 2)

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

                            order_blocks.append(OrderBlock(
                                ob_type='bearish',
                                index=j,
                                high=float(high[j]),
                                low=float(low[j]),
                                open_price=float(open_prices[j]),
                                close_price=float(close[j]),
                                volume=float(volume[j]),
                                strength=float(strength),
                                timestamp=ts,
                                valid=valid,
                                touched=touched,
                                broken=broken
                            ))
                        break

        logger.debug(f"Detected {len(order_blocks)} Order Blocks")
        return order_blocks

    def compute_premium_discount_zone(
        self,
        df: pd.DataFrame,
        swing_highs: List[SwingPoint],
        swing_lows: List[SwingPoint],
        lookback: int = 5
    ) -> PremiumDiscountZone:
        """
        Compute Premium/Discount zones based on recent swing range.

        - Premium Zone: Upper 38.2% of the range (0.618 - 1.0 Fibonacci)
        - Discount Zone: Lower 38.2% of the range (0.0 - 0.382 Fibonacci)
        - Equilibrium: 50% level of the range

        Args:
            df: OHLCV DataFrame
            swing_highs: List of swing highs
            swing_lows: List of swing lows
            lookback: Number of recent swings to consider

        Returns:
            PremiumDiscountZone object
        """
        current_price = df['close'].iloc[-1]

        if not swing_highs or not swing_lows:
            # Use recent range if no swings
            recent_high = df['high'].iloc[-20:].max()
            recent_low = df['low'].iloc[-20:].min()
        else:
            # Use recent swings
            recent_high = max(sh.price for sh in swing_highs[-lookback:])
            recent_low = min(sl.price for sl in swing_lows[-lookback:])

        range_size = recent_high - recent_low
        if range_size <= 0:
            range_size = recent_high * 0.01  # Minimum 1% range

        equilibrium = recent_low + range_size * 0.5

        # Premium zone: 61.8% - 100%
        premium_low = recent_low + range_size * 0.618
        premium_high = recent_high

        # Discount zone: 0% - 38.2%
        discount_low = recent_low
        discount_high = recent_low + range_size * 0.382

        # Determine current zone
        if current_price >= premium_low:
            current_zone = 'premium'
        elif current_price <= discount_high:
            current_zone = 'discount'
        else:
            current_zone = 'equilibrium'

        return PremiumDiscountZone(
            premium_low=float(premium_low),
            premium_high=float(premium_high),
            discount_low=float(discount_low),
            discount_high=float(discount_high),
            equilibrium=float(equilibrium),
            current_zone=current_zone
        )

    def detect_liquidity_levels(
        self,
        swing_highs: List[SwingPoint],
        swing_lows: List[SwingPoint],
        df: pd.DataFrame
    ) -> List[LiquidityLevel]:
        """
        Detect liquidity levels (equal highs/lows).

        Equal highs/lows are areas where stop losses cluster,
        making them targets for liquidity sweeps.

        Args:
            swing_highs: List of swing highs
            swing_lows: List of swing lows
            df: OHLCV DataFrame

        Returns:
            List of LiquidityLevel objects
        """
        liquidity_levels = []
        tolerance = df['close'].iloc[-1] * self.liquidity_tolerance_pct

        # Detect equal highs
        high_prices = [(sh.index, sh.price) for sh in swing_highs]
        for i in range(len(high_prices)):
            matching_indices = [high_prices[i][0]]
            base_price = high_prices[i][1]

            for j in range(i + 1, len(high_prices)):
                if abs(high_prices[j][1] - base_price) <= tolerance:
                    matching_indices.append(high_prices[j][0])

            if len(matching_indices) >= 2:
                # Check if already added
                existing = [ll for ll in liquidity_levels if ll.level_type == 'equal_highs' and abs(ll.price - base_price) <= tolerance]
                if not existing:
                    swept = df['high'].iloc[-1] > base_price
                    liquidity_levels.append(LiquidityLevel(
                        level_type='equal_highs',
                        price=float(base_price),
                        indices=matching_indices,
                        strength=len(matching_indices),
                        swept=swept
                    ))

        # Detect equal lows
        low_prices = [(sl.index, sl.price) for sl in swing_lows]
        for i in range(len(low_prices)):
            matching_indices = [low_prices[i][0]]
            base_price = low_prices[i][1]

            for j in range(i + 1, len(low_prices)):
                if abs(low_prices[j][1] - base_price) <= tolerance:
                    matching_indices.append(low_prices[j][0])

            if len(matching_indices) >= 2:
                existing = [ll for ll in liquidity_levels if ll.level_type == 'equal_lows' and abs(ll.price - base_price) <= tolerance]
                if not existing:
                    swept = df['low'].iloc[-1] < base_price
                    liquidity_levels.append(LiquidityLevel(
                        level_type='equal_lows',
                        price=float(base_price),
                        indices=matching_indices,
                        strength=len(matching_indices),
                        swept=swept
                    ))

        logger.debug(f"Detected {len(liquidity_levels)} liquidity levels")
        return liquidity_levels

    def analyze(
        self,
        df: pd.DataFrame
    ) -> Dict[str, Any]:
        """
        Perform complete market structure analysis.

        Args:
            df: OHLCV DataFrame

        Returns:
            Dictionary with all structure analysis results
        """
        # Detect swing points
        swing_highs, swing_lows = self.detect_swing_points(df)

        # Classify swings
        classified_highs, classified_lows = self.detect_higher_highs_lower_lows(
            swing_highs, swing_lows
        )

        # Detect structure events
        bos_events = self.detect_break_of_structure(df, classified_highs, classified_lows)
        choch_events = self.detect_change_of_character(df, classified_highs, classified_lows)

        # Detect POIs
        fvgs = self.detect_fair_value_gaps(df)
        order_blocks = self.detect_order_blocks(df, bos_events)
        liquidity_levels = self.detect_liquidity_levels(classified_highs, classified_lows, df)

        # Compute zones
        zones = self.compute_premium_discount_zone(df, classified_highs, classified_lows)

        return {
            'swing_highs': [sh.to_dict() for sh in classified_highs],
            'swing_lows': [sl.to_dict() for sl in classified_lows],
            'bos_events': [bos.to_dict() for bos in bos_events],
            'choch_events': [choch.to_dict() for choch in choch_events],
            'fair_value_gaps': [fvg.to_dict() for fvg in fvgs],
            'order_blocks': [ob.to_dict() for ob in order_blocks],
            'liquidity_levels': [ll.to_dict() for ll in liquidity_levels],
            'premium_discount_zone': zones.to_dict(),
            'summary': {
                'total_swing_highs': len(classified_highs),
                'total_swing_lows': len(classified_lows),
                'total_bos': len(bos_events),
                'total_choch': len(choch_events),
                'total_fvgs': len(fvgs),
                'unfilled_fvgs': sum(1 for f in fvgs if not f.filled),
                'total_order_blocks': len(order_blocks),
                'valid_order_blocks': sum(1 for ob in order_blocks if ob.valid),
                'total_liquidity_levels': len(liquidity_levels),
                'current_zone': zones.current_zone
            }
        }


if __name__ == "__main__":
    # Test StructureDetector
    import numpy as np

    np.random.seed(42)

    # Create sample OHLCV data with some structure
    n_samples = 500
    dates = pd.date_range('2024-01-01', periods=n_samples, freq='5min')

    # Generate price with trends and reversals
    price = 2000.0
    prices = [price]
    for i in range(1, n_samples):
        # Add some trending behavior
        trend = 0.5 * np.sin(i / 50) + np.random.randn() * 0.3
        price = price + trend
        prices.append(price)

    prices = np.array(prices)

    df = pd.DataFrame({
        'open': prices + np.random.randn(n_samples) * 0.5,
        'high': prices + abs(np.random.randn(n_samples)) * 1.5,
        'low': prices - abs(np.random.randn(n_samples)) * 1.5,
        'close': prices + np.random.randn(n_samples) * 0.5,
        'volume': np.random.randint(100, 10000, n_samples)
    }, index=dates)

    # Ensure high > low and proper OHLC relationships
    df['high'] = df[['open', 'high', 'close']].max(axis=1)
    df['low'] = df[['open', 'low', 'close']].min(axis=1)

    # Create detector and analyze
    detector = StructureDetector(swing_order=5)
    analysis = detector.analyze(df)

    print("=== Market Structure Analysis ===")
    print(f"Summary: {analysis['summary']}")
    print(f"\nRecent BOS Events: {analysis['bos_events'][-3:] if analysis['bos_events'] else 'None'}")
    print(f"Recent CHoCH Events: {analysis['choch_events'][-3:] if analysis['choch_events'] else 'None'}")
    print(f"Valid Order Blocks: {[ob for ob in analysis['order_blocks'] if ob['valid']][-3:]}")
    print(f"Unfilled FVGs: {[fvg for fvg in analysis['fair_value_gaps'] if not fvg['filled']][-3:]}")
    print(f"Premium/Discount Zone: {analysis['premium_discount_zone']}")
