"""
Opportunity Detector for Markov Chain Trading Strategy

Identifies when markets are "not watching" - the key insight from 0xRicker's
$1.3M profit article. When human attention drops, prices become lazy, stale,
and exploitable through Markov chain arbitrage.

Key Detection Criteria:
1. Low volume periods (below 50% of average)
2. Wide spreads (above normal threshold)
3. Arbitrage gaps (model probability - market price > eps threshold)
4. High state persistence (above tau threshold)
"""

import asyncio
import logging
from typing import Optional, List, Dict, Any
from dataclasses import dataclass
from datetime import datetime

logger = logging.getLogger(__name__)


@dataclass
class Opportunity:
    """Represents a trading opportunity."""
    symbol: str
    timestamp: float
    opportunity_type: str  # 'low_volume', 'wide_spread', 'arbitrage_gap', 'persistence'
    confidence: float  # 0-1 confidence score
    expected_return: float  # Expected return percentage
    entry_price: float
    target_price: float
    stop_loss_price: float
    recommended_stake: float  # Based on Kelly criterion
    metadata: Dict[str, Any]
    
    def __str__(self) -> str:
        return (f"Opportunity({self.symbol}, type={self.opportunity_type}, "
                f"confidence={self.confidence:.2f}, expected_return={self.expected_return:.2f}%)")


class OpportunityDetector:
    """Detects trading opportunities based on market inattention.
    
    From 0xRicker's article:
    "When human attention drops, the market still sets prices - but those
    prices are lazy, stale, and exploitable. This is where the edge is."
    
    Detection Strategy:
    1. Volume Analysis: Find periods with <50% average volume
    2. Spread Analysis: Identify abnormally wide bid-ask spreads
    3. Arbitrage Gap: Compare Markov model probabilities to market prices
    4. Persistence Check: Verify state persistence > tau threshold (0.87)
    """
    
    # Thresholds from 0xRicker's strategy
    EPS_THRESHOLD = 0.05  # Minimum arbitrage gap (5%)
    TAU_THRESHOLD = 0.87  # Minimum state persistence
    VOLUME_LOW_THRESHOLD = 0.5  # Below 50% of average
    SPREAD_WIDE_THRESHOLD = 0.02  # Above 2% spread
    
    def __init__(
        self,
        adapter,
        markov_engine,
        db,
        kelly_calculator=None
    ):
        """Initialize opportunity detector.
        
        Args:
            adapter: Exchange adapter (KrakenAdapter)
            markov_engine: MarkovChainEngine instance
            db: Database instance for persistence
            kelly_calculator: Optional Kelly criterion calculator
        """
        self.adapter = adapter
        self.markov_engine = markov_engine
        self.db = db
        self.kelly_calculator = kelly_calculator
        
        # Cache for recent data
        self._volume_cache: Dict[str, Dict[str, Any]] = {}
        self._orderbook_cache: Dict[str, Dict[str, Any]] = {}
        self._cache_timestamp: Dict[str, float] = {}
        self._cache_ttl = 30  # 30 second cache
    
    async def detect_opportunities(
        self,
        symbols: List[str],
        min_confidence: float = 0.7
    ) -> List[Opportunity]:
        """Detect all opportunities across multiple symbols.
        
        Args:
            symbols: List of trading symbols to analyze
            min_confidence: Minimum confidence threshold
            
        Returns:
            List of detected opportunities
        """
        logger.info(f"Scanning for opportunities across {len(symbols)} symbols")
        
        opportunities = []
        
        # Run all detections concurrently
        detection_tasks = [
            self._analyze_symbol(symbol)
            for symbol in symbols
        ]
        results = await asyncio.gather(*detection_tasks, return_exceptions=True)
        
        for symbol, result in zip(symbols, results):
            if isinstance(result, Exception):
                logger.error(f"Error analyzing {symbol}: {result}")
                continue
            
            symbol_opportunities = [
                opp for opp in result
                if opp.confidence >= min_confidence
            ]
            opportunities.extend(symbol_opportunities)
            
            logger.info(
                f"{symbol}: Found {len(symbol_opportunities)} opportunities "
                f"(confidence >= {min_confidence})"
            )
        
        # Sort by expected return
        opportunities.sort(key=lambda x: x.expected_return, reverse=True)
        
        logger.info(f"Total opportunities detected: {len(opportunities)}")
        return opportunities
    
    async def _analyze_symbol(self, symbol: str) -> List[Opportunity]:
        """Analyze a single symbol for opportunities.
        
        Args:
            symbol: Trading symbol
            
        Returns:
            List of opportunities for this symbol
        """
        opportunities = []
        
        try:
            # Get current market data
            volume_data = await self._get_volume_data(symbol)
            orderbook_data = await self._get_orderbook_data(symbol)
            
            # Get Markov chain analysis
            markov_analysis = await self._get_markov_analysis(symbol)
            
            if not markov_analysis:
                logger.warning(f"No Markov analysis available for {symbol}")
                return opportunities
            
            # Check for different opportunity types
            opp_volume = self._check_low_volume_opportunity(
                symbol, volume_data, markov_analysis
            )
            if opp_volume:
                opportunities.append(opp_volume)
            
            opp_spread = self._check_wide_spread_opportunity(
                symbol, orderbook_data, markov_analysis
            )
            if opp_spread:
                opportunities.append(opp_spread)
            
            opp_arbitrage = self._check_arbitrage_gap_opportunity(
                symbol, markov_analysis
            )
            if opp_arbitrage:
                opportunities.append(opp_arbitrage)
            
            opp_persistence = self._check_persistence_opportunity(
                symbol, markov_analysis
            )
            if opp_persistence:
                opportunities.append(opp_persistence)
                
        except Exception as e:
            logger.error(f"Error analyzing {symbol}: {e}")
        
        return opportunities
    
    async def _get_volume_data(self, symbol: str) -> Dict[str, Any]:
        """Get or fetch volume data with caching.
        
        Args:
            symbol: Trading symbol
            
        Returns:
            Volume data dictionary
        """
        current_time = datetime.now().timestamp()
        
        # Check cache
        if symbol in self._cache_timestamp and \
           current_time - self._cache_timestamp[symbol] < self._cache_ttl:
            return self._volume_cache.get(symbol, {})
        
        # Fetch fresh data
        volume_data = await self.adapter.get_recent_volume(symbol, window_minutes=60)
        
        # Update cache
        self._volume_cache[symbol] = volume_data
        self._cache_timestamp[symbol] = current_time
        
        return volume_data
    
    async def _get_orderbook_data(self, symbol: str) -> Dict[str, Any]:
        """Get or fetch order book data with caching.
        
        Args:
            symbol: Trading symbol
            
        Returns:
            Order book data dictionary
        """
        current_time = datetime.now().timestamp()
        
        # Check cache
        if symbol in self._cache_timestamp and \
           current_time - self._cache_timestamp[symbol] < self._cache_ttl:
            return self._orderbook_cache.get(symbol, {})
        
        # Fetch fresh data
        orderbook_data = await self.adapter.get_orderbook(symbol, limit=10)
        
        # Update cache
        self._orderbook_cache[symbol] = orderbook_data
        self._cache_timestamp[symbol] = current_time
        
        return orderbook_data
    
    async def _get_markov_analysis(self, symbol: str) -> Optional[Dict[str, Any]]:
        """Get Markov chain analysis for symbol.
        
        Args:
            symbol: Trading symbol
            
        Returns:
            Markov analysis dictionary or None
        """
        try:
            # Get current price
            ticker = await self.adapter.fetch_ticker(symbol)
            current_price = ticker.last
            
            # Analyze with Markov engine
            analysis = self.markov_engine.analyze_state(current_price)
            
            return analysis
            
        except Exception as e:
            logger.error(f"Markov analysis failed for {symbol}: {e}")
            return None
    
    def _check_low_volume_opportunity(
        self,
        symbol: str,
        volume_data: Dict[str, Any],
        markov_analysis: Dict[str, Any]
    ) -> Optional[Opportunity]:
        """Check for low volume opportunity.
        
        Low volume indicates reduced human attention - prices may be stale.
        
        Args:
            symbol: Trading symbol
            volume_data: Volume statistics
            markov_analysis: Markov chain analysis
            
        Returns:
            Opportunity if detected, None otherwise
        """
        if not volume_data.get('is_low_volume'):
            return None
        
        # Calculate confidence based on how low volume is
        avg_volume = volume_data.get('avg_volume', 0)
        recent_volume = volume_data.get('recent_avg', 0)
        
        if avg_volume > 0:
            volume_ratio = recent_volume / avg_volume
            confidence = min(1.0, (1 - volume_ratio) * 1.5)  # Scale to 0-1
        else:
            confidence = 0.5
        
        # Check if Markov model suggests entry
        if not markov_analysis.get('should_enter'):
            return None
        
        # Calculate expected return from Markov analysis
        expected_return = markov_analysis.get('expected_return_pct', 0)
        
        # Get entry/exit prices
        current_price = markov_analysis.get('current_price', 0)
        state_range = markov_analysis.get('state_range', (0, 0))
        
        # Target is next state range
        optimal_next = markov_analysis.get('optimal_next_state', 0)
        target_price = current_price * (1 + expected_return / 100)
        stop_loss = current_price * 0.97  # 3% stop loss
        
        # Calculate stake using Kelly criterion
        stake = self._calculate_stake(symbol, expected_return, markov_analysis)
        
        return Opportunity(
            symbol=symbol,
            timestamp=datetime.now().timestamp(),
            opportunity_type='low_volume',
            confidence=confidence,
            expected_return=expected_return,
            entry_price=current_price,
            target_price=target_price,
            stop_loss_price=stop_loss,
            recommended_stake=stake,
            metadata={
                'volume_ratio': volume_ratio,
                'avg_volume': avg_volume,
                'recent_volume': recent_volume,
                'persistence': markov_analysis.get('persistence', 0),
            }
        )
    
    def _check_wide_spread_opportunity(
        self,
        symbol: str,
        orderbook_data: Dict[str, Any],
        markov_analysis: Dict[str, Any]
    ) -> Optional[Opportunity]:
        """Check for wide spread opportunity.
        
        Wide spreads indicate low liquidity and potential for stale prices.
        
        Args:
            symbol: Trading symbol
            orderbook_data: Order book data
            markov_analysis: Markov chain analysis
            
        Returns:
            Opportunity if detected, None otherwise
        """
        spread_pct = orderbook_data.get('spread_pct', 0)
        
        if spread_pct < self.SPREAD_WIDE_THRESHOLD * 100:
            return None
        
        # Calculate confidence based on spread width
        confidence = min(1.0, spread_pct / 5.0)  # 5% spread = max confidence
        
        # Check if Markov model suggests entry
        if not markov_analysis.get('should_enter'):
            return None
        
        expected_return = markov_analysis.get('expected_return_pct', 0)
        current_price = markov_analysis.get('current_price', 0)
        
        # Use mid price for entry
        best_bid = orderbook_data.get('best_bid', current_price)
        best_ask = orderbook_data.get('best_ask', current_price)
        mid_price = (best_bid + best_ask) / 2
        
        target_price = mid_price * (1 + expected_return / 100)
        stop_loss = mid_price * 0.97
        
        stake = self._calculate_stake(symbol, expected_return, markov_analysis)
        
        return Opportunity(
            symbol=symbol,
            timestamp=datetime.now().timestamp(),
            opportunity_type='wide_spread',
            confidence=confidence,
            expected_return=expected_return,
            entry_price=mid_price,
            target_price=target_price,
            stop_loss_price=stop_loss,
            recommended_stake=stake,
            metadata={
                'spread_pct': spread_pct,
                'best_bid': best_bid,
                'best_ask': best_ask,
                'bid_depth': orderbook_data.get('bid_depth', 0),
                'ask_depth': orderbook_data.get('ask_depth', 0),
            }
        )
    
    def _check_arbitrage_gap_opportunity(
        self,
        symbol: str,
        markov_analysis: Dict[str, Any]
    ) -> Optional[Opportunity]:
        """Check for arbitrage gap opportunity.
        
        When model probability exceeds market price by eps threshold,
        an arbitrage opportunity exists.
        
        From 0xRicker: "The arbitrage gap is the edge - when the market
        prices a transition at 10% but the model says 15%, that 5% gap
        is profit."
        
        Args:
            symbol: Trading symbol
            markov_analysis: Markov chain analysis
            
        Returns:
            Opportunity if detected, None otherwise
        """
        arbitrage_gap = markov_analysis.get('arbitrage_gap', 0)
        
        if arbitrage_gap < self.EPS_THRESHOLD:
            return None
        
        # Confidence based on gap size
        confidence = min(1.0, arbitrage_gap / 0.1)  # 10% gap = max confidence
        
        expected_return = markov_analysis.get('expected_return_pct', 0)
        current_price = markov_analysis.get('current_price', 0)
        
        target_price = current_price * (1 + expected_return / 100)
        stop_loss = current_price * 0.97
        
        stake = self._calculate_stake(symbol, expected_return, markov_analysis)
        
        return Opportunity(
            symbol=symbol,
            timestamp=datetime.now().timestamp(),
            opportunity_type='arbitrage_gap',
            confidence=confidence,
            expected_return=expected_return,
            entry_price=current_price,
            target_price=target_price,
            stop_loss_price=stop_loss,
            recommended_stake=stake,
            metadata={
                'arbitrage_gap': arbitrage_gap,
                'model_probability': markov_analysis.get('transition_probability', 0),
                'market_probability': markov_analysis.get('market_probability', 0),
                'eps_threshold': self.EPS_THRESHOLD,
            }
        )
    
    def _check_persistence_opportunity(
        self,
        symbol: str,
        markov_analysis: Dict[str, Any]
    ) -> Optional[Opportunity]:
        """Check for high persistence opportunity.
        
        When state persistence exceeds tau threshold, the current state
        is likely to continue - good for trend-following entries.
        
        Args:
            symbol: Trading symbol
            markov_analysis: Markov chain analysis
            
        Returns:
            Opportunity if detected, None otherwise
        """
        persistence = markov_analysis.get('persistence', 0)
        
        if persistence < self.TAU_THRESHOLD:
            return None
        
        # Confidence based on persistence level
        confidence = min(1.0, (persistence - self.TAU_THRESHOLD) / 0.13 + 0.5)
        
        expected_return = markov_analysis.get('expected_return_pct', 0)
        current_price = markov_analysis.get('current_price', 0)
        
        target_price = current_price * (1 + expected_return / 100)
        stop_loss = current_price * 0.97
        
        stake = self._calculate_stake(symbol, expected_return, markov_analysis)
        
        return Opportunity(
            symbol=symbol,
            timestamp=datetime.now().timestamp(),
            opportunity_type='persistence',
            confidence=confidence,
            expected_return=expected_return,
            entry_price=current_price,
            target_price=target_price,
            stop_loss_price=stop_loss,
            recommended_stake=stake,
            metadata={
                'persistence': persistence,
                'tau_threshold': self.TAU_THRESHOLD,
                'state_id': markov_analysis.get('current_state', 0),
                'optimal_next_state': markov_analysis.get('optimal_next_state', 0),
            }
        )
    
    def _calculate_stake(
        self,
        symbol: str,
        expected_return: float,
        markov_analysis: Dict[str, Any]
    ) -> float:
        """Calculate recommended stake using Kelly criterion.
        
        Args:
            symbol: Trading symbol
            expected_return: Expected return percentage
            markov_analysis: Markov analysis data
            
        Returns:
            Recommended stake as fraction of portfolio (0-1)
        """
        if self.kelly_calculator:
            # Use dedicated Kelly calculator if available
            return self.kelly_calculator.calculate_fraction(symbol)
        
        # Fallback: simplified Kelly calculation
        # f* = (p * (b + 1) - 1) / b
        # where p = win probability, b = odds (expected_return / loss)
        
        win_rate = markov_analysis.get('transition_probability', 0.5)
        loss = 0.03  # Assume 3% loss on stop loss
        
        if loss > 0:
            b = expected_return / loss
            if b > 0:
                f_star = (win_rate * (b + 1) - 1) / b
                # Cap at 71% (optimal Kelly cap from 0xRicker)
                f_star = min(f_star, 0.71)
                # Use half Kelly for safety
                return max(0, f_star * 0.5)
        
        return 0.1  # Default 10% stake
    
    async def record_opportunity(self, opportunity: Opportunity) -> int:
        """Record opportunity to database.
        
        Args:
            opportunity: Opportunity to record
            
        Returns:
            Record ID
        """
        return await self.db.record_signal(
            strategy='opportunity_detector',
            signal='entry',
            confidence=opportunity.confidence,
            reason=f"{opportunity.opportunity_type} opportunity detected",
            metadata={
                'symbol': opportunity.symbol,
                'type': opportunity.opportunity_type,
                'expected_return': opportunity.expected_return,
                'entry_price': opportunity.entry_price,
                'target_price': opportunity.target_price,
                'stop_loss': opportunity.stop_loss_price,
                'stake': opportunity.recommended_stake,
                'opportunity_metadata': opportunity.metadata,
            },
            executed=False
        )
    
    def clear_cache(self) -> None:
        """Clear all cached data."""
        self._volume_cache.clear()
        self._orderbook_cache.clear()
        self._cache_timestamp.clear()
        logger.debug("Opportunity detector cache cleared")
