#!/usr/bin/env python3
"""
Bitcoin Trading Bot - Main Orchestrator

Integrates all components:
- Markov Chain Engine: State analysis and entry signals
- Opportunity Detector: Market inattention detection
- Kelly Criterion: Optimal position sizing
- Exchange Adapter: Trade execution
- Database: Signal and trade tracking

Core loop:
1. Fetch current market data
2. Update Markov model with new price
3. Detect opportunities (low volume, wide spread, arbitrage gap, persistence)
4. Calculate optimal position size
5. Execute trades with risk management
6. Track performance and adjust
"""

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

from exchange.kraken_adapter import KrakenAdapter
from strategy.markov_engine import MarkovEngine
from strategy.opportunity_detector import OpportunityDetector
from strategy.kelly_criterion import KellyCalculator, KellyPosition
from database.signals_db import SignalsDatabase
from config.settings import Settings

logger = logging.getLogger(__name__)


@dataclass
class TradeState:
    """Tracks state of an open trade."""
    trade_id: int
    symbol: str
    side: str  # 'buy' or 'sell'
    entry_price: float
    stop_loss_price: float
    target_price: float
    position_size: float
    avg_entry: float
    status: str  # 'open', 'closed', 'stopped', 'target'
    open_time: datetime
    close_time: Optional[datetime] = None
    exit_price: Optional[float] = None
    pnl: Optional[float] = None
    pnl_pct: Optional[float] = None


class TradingBot:
    """
    Main trading bot orchestrator.
    
    Implements the full 0xRicker strategy:
    1. Monitor market for opportunities (low attention periods)
    2. Use Markov chains to assess state and transition probabilities
    3. Enter only when gap >= eps AND persistence >= tau
    4. Size positions using Kelly criterion (capped at 71%)
    5. Manage trades with stop-loss and take-profit
    
    Configuration:
        - Runs every minute (as per article)
        - Can monitor multiple symbols
        - Paper trading mode for testing
        - Live trading with real money
    """
    
    def __init__(self, settings: Settings):
        """
        Initialize trading bot.
        
        Args:
            settings: Configuration settings
        """
        self.settings = settings
        self.running = False
        self.current_price: Dict[str, float] = {}
        self.open_trades: Dict[str, TradeState] = {}
        
        # Initialize components
        self.db = SignalsDatabase(settings.database_path)
        self.adapter = KrakenAdapter(
            api_key=settings.kraken_api_key,
            api_secret=settings.kraken_api_secret,
            paper_trading=settings.paper_trading
        )
        
        # Initialize strategy components per symbol
        self.markov_engines: Dict[str, MarkovEngine] = {}
        self.opportunity_detectors: Dict[str, OpportunityDetector] = {}
        self.kelly_calculators: Dict[str, KellyCalculator] = {}
        
        # Price history for initial fitting
        self._price_history: Dict[str, List[float]] = {symbol: [] for symbol in settings.trading_symbols}
        
        logger.info(f"TradingBot initialized: paper_trading={settings.paper_trading}")
        logger.info(f"Symbols: {settings.trading_symbols}")
    
    async def initialize(self) -> None:
        """Initialize bot with historical data and component setup."""
        logger.info("Initializing trading bot...")
        
        # Initialize components for each symbol
        for symbol in self.settings.trading_symbols:
            # Markov engine
            self.markov_engines[symbol] = MarkovEngine(
                n_states=self.settings.n_states,
                tau=self.settings.tau_threshold,
                eps=self.settings.eps_threshold,
                lookback_periods=self.settings.lookback_periods
            )
            
            # Kelly calculator (shared portfolio value)
            self.kelly_calculators[symbol] = KellyCalculator(
                portfolio_value=self.settings.initial_portfolio_value
            )
            
            logger.info(f"Initialized components for {symbol}")
        
        # Fetch initial price history
        await self._fetch_initial_data()
        
        # Fit Markov models
        for symbol in self.settings.trading_symbols:
            if len(self._price_history[symbol]) >= self.settings.lookback_periods:
                self.markov_engines[symbol].fit(self._price_history[symbol])
                logger.info(
                    f"{symbol}: Markov model fitted with {len(self._price_history[symbol])} prices"
                )
            else:
                logger.warning(
                    f"{symbol}: Insufficient history ({len(self._price_history[symbol])} < "
                    f"{self.settings.lookback_periods}), waiting for more data"
                )
        
        logger.info("Trading bot initialization complete")
    
    async def _fetch_initial_data(self) -> None:
        """Fetch initial price history for all symbols."""
        logger.info("Fetching initial price history...")
        
        for symbol in self.settings.trading_symbols:
            try:
                # Fetch last N candlesticks
                candles = await self.adapter.get_candles(
                    symbol,
                    timeframe=self.settings.candle_timeframe,
                    count=self.settings.lookback_periods * 2
                )
                
                # Extract close prices
                prices = [candle.close for candle in candles]
                self._price_history[symbol] = prices
                
                logger.info(f"{symbol}: Fetched {len(prices)} price points")
                
            except Exception as e:
                logger.error(f"Failed to fetch history for {symbol}: {e}")
    
    async def run(self) -> None:
        """Main trading loop."""
        self.running = True
        
        logger.info("Starting trading bot main loop...")
        logger.info(f"Paper trading: {self.settings.paper_trading}")
        logger.info(f"Interval: {self.settings.trading_interval_seconds}s")
        
        # Get initial portfolio value
        try:
            balance = await self.adapter.get_balance()
            portfolio_value = balance.get('USD', self.settings.initial_portfolio_value)
            
            # Update Kelly calculators
            for symbol in self.settings.trading_symbols:
                self.kelly_calculators[symbol].update_portfolio_value(portfolio_value)
            
            logger.info(f"Initial portfolio value: ${portfolio_value:.2f}")
        except Exception as e:
            logger.error(f"Failed to get balance: {e}")
            portfolio_value = self.settings.initial_portfolio_value
        
        # Main loop
        iteration = 0
        while self.running:
            iteration += 1
            logger.debug(f"=== Iteration {iteration} ===")
            
            try:
                # Run trading cycle
                await self._trading_cycle()
                
            except Exception as e:
                logger.error(f"Error in trading cycle: {e}")
            
            # Wait for next iteration
            try:
                await asyncio.sleep(self.settings.trading_interval_seconds)
            except asyncio.CancelledError:
                break
        
        logger.info("Trading bot stopped")
    
    async def _trading_cycle(self) -> None:
        """Execute one trading cycle (runs every minute)."""
        timestamp = datetime.now()
        logger.debug(f"Trading cycle started at {timestamp}")
        
        # 1. Update prices and check open trades
        await self._update_prices()
        await self._check_open_trades()
        
        # 2. Detect opportunities for each symbol
        for symbol in self.settings.trading_symbols:
            if symbol in self.open_trades:
                logger.debug(f"{symbol}: Already have open trade, skipping")
                continue
            
            try:
                await self._scan_for_opportunity(symbol)
            except Exception as e:
                logger.error(f"Error scanning {symbol}: {e}")
        
        logger.debug(f"Trading cycle completed at {datetime.now()}")
    
    async def _update_prices(self) -> None:
        """Update current prices for all symbols."""
        for symbol in self.settings.trading_symbols:
            try:
                ticker = await self.adapter.fetch_ticker(symbol)
                current_price = ticker.last
                
                self.current_price[symbol] = current_price
                
                # Update Markov engine
                if symbol in self.markov_engines:
                    self.markov_engines[symbol].update(current_price)
                
                logger.debug(f"{symbol}: ${current_price:.2f}")
                
            except Exception as e:
                logger.error(f"Failed to update price for {symbol}: {e}")
    
    async def _check_open_trades(self) -> None:
        """Check and manage open trades."""
        for symbol, trade in list(self.open_trades.items()):
            if symbol not in self.current_price:
                continue
            
            current_price = self.current_price[symbol]
            
            # Check stop loss
            if trade.side == 'buy' and current_price <= trade.stop_loss_price:
                await self._close_trade(symbol, 'stop_loss', current_price)
                continue
            
            # Check take profit
            if trade.side == 'buy' and current_price >= trade.target_price:
                await self._close_trade(symbol, 'target', current_price)
                continue
    
    async def _scan_for_opportunity(self, symbol: str) -> None:
        """
        Scan for trading opportunities.
        
        Process:
        1. Get Markov analysis
        2. Check if entry conditions met (gap + persistence)
        3. Detect market inattention (low volume, wide spread)
        4. Calculate position size with Kelly
        5. Execute trade if all conditions met
        """
        if symbol not in self.current_price:
            return
        
        current_price = self.current_price[symbol]
        markov_engine = self.markov_engines[symbol]
        
        # Get comprehensive Markov analysis
        analysis = markov_engine.analyze_state(current_price)
        
        logger.debug(
            f"{symbol}: state={analysis['current_state']}, "
            f"persistence={analysis['persistence']:.3f}, "
            f"gap={analysis['arbitrage_gap']:.3f}"
        )
        
        # Check core entry conditions
        should_enter = analysis['should_enter']
        
        if not should_enter:
            reason = []
            if analysis['persistence'] < self.settings.tau_threshold:
                reason.append(f"persistence={analysis['persistence']:.3f} < {self.settings.tau_threshold}")
            if analysis['arbitrage_gap'] < self.settings.eps_threshold:
                reason.append(f"gap={analysis['arbitrage_gap']:.3f} < {self.settings.eps_threshold}")
            
            logger.debug(f"{symbol}: Entry conditions not met ({', '.join(reason)})")
            
            # Still record the signal for tracking
            await self.db.record_signal(
                strategy='markov_engine',
                signal='no_entry',
                confidence=0.0,
                reason=f"Conditions not met: {'; '.join(reason)}",
                metadata={
                    'symbol': symbol,
                    'current_price': current_price,
                    'persistence': analysis['persistence'],
                    'arbitrage_gap': analysis['arbitrage_gap'],
                    'state': analysis['current_state'],
                },
                executed=False
            )
            return
        
        # Entry conditions met! Check for market inattention
        logger.info(f"{symbol}: Entry conditions MET!")
        logger.info(
            f"  Persistence: {analysis['persistence']:.3f} (>= {self.settings.tau_threshold})"
        )
        logger.info(
            f"  Gap: {analysis['arbitrage_gap']:.3f} (>= {self.settings.eps_threshold})"
        )
        
        # Get volume and orderbook data for opportunity detection
        try:
            volume_data = await self.adapter.get_recent_volume(symbol, window_minutes=60)
            orderbook_data = await self.adapter.get_orderbook(symbol, limit=10)
            
            # Check for low volume (market not watching)
            is_low_volume = volume_data.get('is_low_volume', False)
            if is_low_volume:
                logger.info(f"{symbol}: Low volume detected (market not watching)")
            
            # Check for wide spread
            spread_pct = orderbook_data.get('spread_pct', 0)
            if spread_pct > 2.0:
                logger.info(f"{symbol}: Wide spread detected ({spread_pct:.2f}%)")
            
        except Exception as e:
            logger.warning(f"Failed to get market data for {symbol}: {e}")
        
        # Calculate position size
        kelly_calc = self.kelly_calculators[symbol]
        
        # Get dynamic win rate from recent performance
        base_win_rate = analysis['transition_probability']
        dynamic_win_rate = kelly_calc.get_dynamic_win_rate(symbol, base_win_rate)
        
        # Calculate stop loss and target
        stop_loss_pct = 0.03  # 3% stop loss
        stop_loss_price = current_price * (1 - stop_loss_pct)
        
        expected_return_pct = analysis['expected_return_pct']
        target_price = current_price * (1 + expected_return_pct / 100)
        
        # Calculate Kelly position
        position = kelly_calc.calculate_position(
            symbol=symbol,
            entry_price=current_price,
            stop_loss_price=stop_loss_price,
            target_price=target_price,
            win_rate=dynamic_win_rate,
            expected_return=expected_return_pct,
            model_confidence=analysis['transition_probability']
        )
        
        if not position:
            logger.warning(f"{symbol}: Kelly calculation rejected position")
            return
        
        # Log position details
        logger.info(f"{symbol}: Position calculated")
        logger.info(
            f"  Size: {position.absolute_size:.4f} @ ${position.entry_price:.2f}"
        )
        logger.info(
            f"  Fraction: {position.fraction:.1%} (Kelly: {position.kelly_fraction:.1%})"
        )
        logger.info(
            f"  Risk: ${position.risk_amount:.2f} ({stop_loss_pct*100:.1f}%)"
        )
        logger.info(
            f"  Target: ${position.target_price:.2f} ({expected_return_pct:.1f}%)"
        )
        
        # Record signal
        signal_id = await self.db.record_signal(
            strategy='kelly_position',
            signal='entry',
            confidence=dynamic_win_rate,
            reason=f"Markov entry + Kelly sizing",
            metadata={
                'symbol': symbol,
                'entry_price': position.entry_price,
                'position_size': position.absolute_size,
                'stop_loss': position.stop_loss_price,
                'target': position.target_price,
                'fraction': position.fraction,
                'win_rate': position.win_rate,
                'expected_return': position.expected_return,
                'markov_analysis': {
                    'state': analysis['current_state'],
                    'persistence': analysis['persistence'],
                    'gap': analysis['arbitrage_gap'],
                }
            },
            executed=False
        )
        
        # Execute trade
        try:
            trade_id = await self._execute_trade(position)
            
            if trade_id:
                # Track open trade
                self.open_trades[symbol] = TradeState(
                    trade_id=trade_id,
                    symbol=symbol,
                    side='buy',
                    entry_price=position.entry_price,
                    stop_loss_price=position.stop_loss_price,
                    target_price=position.target_price,
                    position_size=position.absolute_size,
                    avg_entry=position.entry_price,
                    status='open',
                    open_time=datetime.now()
                )
                
                # Update signal as executed
                await self.db.update_signal_executed(signal_id, True, trade_id)
                
                logger.info(f"{symbol}: Trade executed successfully (ID: {trade_id})")
            else:
                logger.warning(f"{symbol}: Trade execution failed")
                await self.db.update_signal_executed(signal_id, False, None, "Execution failed")
                
        except Exception as e:
            logger.error(f"{symbol}: Error executing trade: {e}")
            await self.db.update_signal_executed(signal_id, False, None, str(e))
    
    async def _execute_trade(self, position: KellyPosition) -> Optional[int]:
        """
        Execute a trade on the exchange.
        
        Args:
            position: KellyPosition with trade details
            
        Returns:
            Trade ID or None if failed
        """
        try:
            if self.settings.paper_trading:
                # Simulate trade
                logger.info(
                    f"PAPER TRADE: Buy {position.absolute_size:.4f} {position.symbol} "
                    f"@ ${position.entry_price:.2f}"
                )
                
                # Return mock trade ID
                return int(datetime.now().timestamp())
            
            # Real trade
            order_id = await self.adapter.buy(
                symbol=position.symbol,
                amount=position.absolute_size,
                price=position.entry_price,
                order_type='limit'
            )
            
            logger.info(f"Executed buy order: {order_id}")
            return order_id
            
        except Exception as e:
            logger.error(f"Trade execution failed: {e}")
            return None
    
    async def _close_trade(
        self,
        symbol: str,
        reason: str,
        exit_price: float
    ) -> None:
        """
        Close an open trade.
        
        Args:
            symbol: Trading symbol
            reason: 'stop_loss' or 'target'
            exit_price: Price at which trade was closed
        """
        if symbol not in self.open_trades:
            return
        
        trade = self.open_trades[symbol]
        
        # Calculate P&L
        if trade.side == 'buy':
            pnl_pct = (exit_price - trade.avg_entry) / trade.avg_entry * 100
            pnl = trade.position_size * (exit_price - trade.avg_entry)
        else:
            pnl_pct = (trade.avg_entry - exit_price) / trade.avg_entry * 100
            pnl = trade.position_size * (trade.avg_entry - exit_price)
        
        # Update trade state
        trade.status = reason
        trade.close_time = datetime.now()
        trade.exit_price = exit_price
        trade.pnl = pnl
        trade.pnl_pct = pnl_pct
        
        logger.info(
            f"{symbol}: Trade closed ({reason}) | "
            f"Entry: ${trade.entry_price:.2f} → Exit: ${exit_price:.2f} | "
            f"P&L: ${pnl:.2f} ({pnl_pct:.2f}%)"
        )
        
        # Record trade in database
        await self.db.record_trade(
            trade_id=trade.trade_id,
            symbol=symbol,
            side=trade.side,
            entry_price=trade.entry_price,
            exit_price=exit_price,
            position_size=trade.position_size,
            pnl=pnl,
            pnl_pct=pnl_pct,
            reason=reason,
            duration_seconds=(trade.close_time - trade.open_time).total_seconds()
        )
        
        # Update Kelly calculator with result
        if symbol in self.kelly_calculators:
            self.kelly_calculators[symbol].update_with_trade_result(
                symbol=symbol,
                win=pnl > 0,
                actual_return=pnl_pct
            )
        
        # Remove from open trades
        del self.open_trades[symbol]
        
        # Log performance summary
        performance = self.kelly_calculators.get(symbol, None)
        if performance:
            perf = performance.get_symbol_performance(symbol)
            logger.info(
                f"{symbol}: Performance - "
                f"{perf['trades']} trades, {perf['win_rate']:.1%} win rate, "
                f"{perf['total_return']:.1f}% total return"
            )
    
    async def stop(self) -> None:
        """Stop the trading bot."""
        self.running = False
        logger.info("Stopping trading bot...")
        
        # Close all open trades
        for symbol in list(self.open_trades.keys()):
            if symbol in self.current_price:
                await self._close_trade(
                    symbol,
                    'manual_close',
                    self.current_price[symbol]
                )
        
        # Close database connection
        self.db.close()
        
        logger.info("Trading bot stopped cleanly")
    
    def get_status(self) -> Dict[str, Any]:
        """Get current bot status."""
        return {
            'running': self.running,
            'symbols': list(self.settings.trading_symbols),
            'current_prices': self.current_price,
            'open_trades': {
                symbol: {
                    'entry': trade.entry_price,
                    'size': trade.position_size,
                    'status': trade.status,
                    'open_time': trade.open_time.isoformat()
                }
                for symbol, trade in self.open_trades.items()
            },
            'paper_trading': self.settings.paper_trading
        }


async def main():
    """Main entry point."""
    from utils.logger import setup_logging
    
    # Setup logging
    setup_logging(level='DEBUG')
    
    # Load settings
    settings = Settings()
    
    # Create and run bot
    bot = TradingBot(settings)
    
    try:
        await bot.initialize()
        await bot.run()
    except KeyboardInterrupt:
        logger.info("Received interrupt signal")
    finally:
        await bot.stop()


if __name__ == "__main__":
    asyncio.run(main())
