import asyncio
import logging
import json
import websockets
from typing import Dict, Optional
from dataclasses import dataclass, field
import time
import torch
from concurrent.futures import ThreadPoolExecutor
import os
from dotenv import load_dotenv


# Load environment variables
load_dotenv()

# Configure logging
logging.basicConfig(
    level=logging.DEBUG,
    format='%(asctime)s.%(msecs)03d [%(levelname)s] [%(filename)s:%(lineno)d] %(message)s',
    datefmt='%Y-%m-%d %H:%M:%S'
)
logger = logging.getLogger(__name__)

@dataclass
class PriceData:
    price: float
    volume: float
    bid: float
    ask: float
    timestamp: float
    spread: float = field(init=False)

    def __post_init__(self):
        self.spread = (self.ask - self.bid) / self.price if self.price > 0 else float('inf')

@dataclass
class ConnectionMetrics:
    messages_received: int = 0
    reconnect_count: int = 0
    total_latency: float = 0.0
    peak_latency: float = 0.0
    errors: int = 0
    mempool_hits: int = 0
    processed_txs: int = 0
    start_time: float = field(default_factory=time.time)

class HeliusWebSocketClient:
    def __init__(self, config: TradingConfig):
        """Initialize WebSocket client for Helius and QuickNode"""
        logger.info("Initializing enhanced WebSocket client...")
        
        # Load credentials from environment with fallback to config if necessary
        self.helius_ws = os.getenv('MARGA_WS_URL', config.helius_ws)
        self.helius_key = os.getenv('HELIUS_API_KEY')  # Ensure this is set in .env
        self.quicknode_ws = os.getenv('QUICKNODE_WEBSOCKET', config.quicknode_ws)
        self.quicknode_key = os.getenv('QUICKNODE_API_KEY')  # Ensure this is set in .env
        
        if not all([self.helius_ws, self.helius_key, self.quicknode_ws, self.quicknode_key]):
            raise ValueError("Missing required environment variables")
        
        # WebSocket configurations
        self.ping_interval = config.ws_ping_interval
        self.ping_timeout = config.ws_ping_timeout
        self.close_timeout = config.ws_close_timeout
        self.max_message_size = config.ws_max_message_size
        
        # WebSocket connections
        self.ws_helius: Optional[websockets.WebSocketClientProtocol] = None
        self.ws_quicknode: Optional[websockets.WebSocketClientProtocol] = None
        
        # State tracking
        self.active = False
        self.metrics = ConnectionMetrics()
        self.price_data: Optional[PriceData] = None
        self.last_price_update = 0
        
        # Processing queues
        self.mempool_queue = asyncio.Queue(maxsize=10000)
        self.price_queue = asyncio.Queue(maxsize=1000)
        self.process_queue = asyncio.Queue(maxsize=5000)
        
        # Important addresses from config
        self.RAYDIUM_PROGRAM = config.raydium_program_id
        self.SOL_WSOL_MARKET = config.sol_wsol_accounts["serum_market"]
        self.AMM_ID = config.sol_wsol_accounts["amm_id"]
        
        # Initialize GPU if available
        self.device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
        if torch.cuda.is_available():
            torch.backends.cuda.matmul.allow_tf32 = True
            torch.backends.cudnn.benchmark = True
            
            # Initialize CUDA streams
            self.streams = {
                'process': torch.cuda.Stream(priority=-2),
                'compute': torch.cuda.Stream(priority=-1),
                'update': torch.cuda.Stream(priority=0)
            }
        
        # Thread pool for background tasks
        self.thread_pool = ThreadPoolExecutor(max_workers=2)
        
        logger.info("WebSocket client initialized")

    async def start(self):
        """Start WebSocket connections and processors"""
        logger.debug("[INFO] Starting Helius WebSocket Client...")
        try:
            self.active = True
            
            # Start connections and processors
            tasks = [
                asyncio.create_task(self._maintain_helius_connection()),
                asyncio.create_task(self._maintain_quicknode_connection()),
                asyncio.create_task(self._process_messages()),
                asyncio.create_task(self._monitor_connections())
            ]
            
            # Wait for connections to establish
            await asyncio.gather(*tasks)
            
        except Exception as e:
            logger.error(f"Failed to start WebSocket client: {e}")
            self.active = False
            raise

    async def _maintain_helius_connection(self):
        """Maintain Helius WebSocket connection"""
        reconnect_delay = 0.1
        max_reconnect_delay = 5.0
        
        while self.active:
            try:
                async with websockets.connect(
                    self.helius_ws,
                    ping_interval=self.ping_interval,
                    ping_timeout=self.ping_timeout,
                    close_timeout=self.close_timeout,
                    max_size=self.max_message_size
                ) as ws:
                    self.ws_helius = ws
                    logger.info("Connected to Helius WebSocket")
                    reconnect_delay = 0.1  # Reset delay on successful connection
                    
                    # Subscribe to required programs and accounts
                    await self._subscribe_helius()
                    
                    # Process messages
                    async for message in ws:
                        if not self.active:
                            break
                            
                        try:
                            data = json.loads(message)
                            await self._handle_helius_message(data)
                        except Exception as e:
                            logger.error(f"Error processing Helius message: {e}")
                            
            except websockets.exceptions.ConnectionClosed:
                logger.warning("Helius connection closed")
                self.ws_helius = None
            except Exception as e:
                logger.error(f"Helius connection error: {e}")
                self.metrics.errors += 1
            
            if self.active:
                await asyncio.sleep(reconnect_delay)
                reconnect_delay = min(reconnect_delay * 2, max_reconnect_delay)
                self.metrics.reconnect_count += 1

    async def _maintain_quicknode_connection(self):
        """Maintain QuickNode WebSocket connection"""
        reconnect_delay = 0.1
        max_reconnect_delay = 5.0
        
        while self.active:
            try:
                async with websockets.connect(
                    self.quicknode_ws,
                    ping_interval=self.ping_interval,
                    ping_timeout=self.ping_timeout,
                    close_timeout=self.close_timeout,
                    max_size=self.max_message_size
                ) as ws:
                    self.ws_quicknode = ws
                    logger.info("Connected to QuickNode WebSocket")
                    reconnect_delay = 0.1  # Reset delay on successful connection
                    
                    # Subscribe to market data
                    await self._subscribe_quicknode()
                    
                    # Process messages
                    async for message in ws:
                        if not self.active:
                            break
                            
                        try:
                            data = json.loads(message)
                            await self._handle_quicknode_message(data)
                        except Exception as e:
                            logger.error(f"Error processing QuickNode message: {e}")
                            
            except websockets.exceptions.ConnectionClosed:
                logger.warning("QuickNode connection closed")
                self.ws_quicknode = None
            except Exception as e:
                logger.error(f"QuickNode connection error: {e}")
                self.metrics.errors += 1
            
            if self.active:
                await asyncio.sleep(reconnect_delay)
                reconnect_delay = min(reconnect_delay * 2, max_reconnect_delay)
                self.metrics.reconnect_count += 1

    async def _subscribe_helius(self):
        """Subscribe to Helius data streams"""
        logger.debug("[INFO] Subscribing to Helius streams...")
        try:
            # Subscribe to Raydium program
            await self.ws_helius.send(json.dumps({
                "jsonrpc": "2.0",
                "id": "raydium_sub",
                "method": "programSubscribe",
                "params": [
                    self.RAYDIUM_PROGRAM,
                    {"encoding": "base64", "commitment": "processed"}
                ]
            }))
            logger.info(f"[INFO] Subscribed to Raydium program: {self.RAYDIUM_PROGRAM}")

            # Subscribe to SOL/wSOL market
            await self.ws_helius.send(json.dumps({
                "jsonrpc": "2.0",
                "id": "market_sub",
                "method": "accountSubscribe",
                "params": [
                    self.SOL_WSOL_MARKET,
                    {"encoding": "base64", "commitment": "processed"}
                ]
            }))
            logger.info(f"[INFO] Subscribed to market: {self.SOL_WSOL_MARKET}")

            # Subscribe to AMM account
            await self.ws_helius.send(json.dumps({
                "jsonrpc": "2.0",
                "id": "amm_sub",
                "method": "accountSubscribe",
                "params": [
                    self.AMM_ID,
                    {"encoding": "base64", "commitment": "processed"}
                ]
            }))
            logger.info(f"[INFO] Subscribed to AMM account: {self.AMM_ID}")

            logger.info("Helius subscriptions completed")
        except Exception as e:
            logger.error(f"Helius subscription error: {e}")
            raise

    async def _subscribe_quicknode(self):
        """Subscribe to QuickNode market data"""
        logger.debug("[INFO] Subscribing to QuickNode market data...")
        try:
            await self.ws_quicknode.send(json.dumps({
                "jsonrpc": "2.0",
                "id": "market_data",
                "method": "subscribeToMarket",
                "params": [
                    self.SOL_WSOL_MARKET,
                    {
                        "commitment": "processed",
                        "encoding": "jsonParsed",
                        "levels": 20  # Order book depth
                    }
                ]
            }))
            logger.info(f"[INFO] Subscribed to QuickNode market: {self.SOL_WSOL_MARKET}")

        except Exception as e:
            logger.error(f"QuickNode subscription error: {e}")
            raise

    async def _handle_helius_message(self, message: Dict):
        """Process Helius WebSocket messages"""
        try:
            logger.debug(f"[DEBUG] Handling Helius message: {message}")

            if 'method' in message:
                if message['method'] == 'programNotification':
                    program_data = message['params']['result']
                    if self.RAYDIUM_PROGRAM in str(program_data):
                        await self.mempool_queue.put(program_data)
                        self.metrics.mempool_hits += 1
                        logger.debug("Mempool hit detected")
                elif message['method'] == 'accountNotification':
                    await self._handle_account_update(message['params']['result'])
                    
            self.metrics.messages_received += 1
            
        except Exception as e:
            logger.error(f"Error handling Helius message: {e}")
            self.metrics.errors += 1

    async def _handle_quicknode_message(self, message: Dict):
        """Process QuickNode WebSocket messages"""
        try:
            logger.debug(f"[DEBUG] Handling QuickNode message: {message}")

            if 'method' in message and message['method'] == 'marketData':
                market_data = message['params']['data']
                
                # Update price data if valid
                price_data = PriceData(
                    price=float(market_data['lastPrice']),
                    volume=float(market_data['volume24h']),
                    bid=float(market_data['bestBid']),
                    ask=float(market_data['bestAsk']),
                    timestamp=time.time()
                )
                self.price_data = price_data
                self.last_price_update = time.time()
                await self.price_queue.put(price_data)
                logger.debug(f"Price update: {self.price_data}")
            
            self.metrics.messages_received += 1
            
        except Exception as e:
            logger.error(f"Error handling QuickNode message: {e}")
            self.metrics.errors += 1

    async def _handle_account_update(self, account_data: Dict):
        """Process account update messages"""
        try:
            logger.debug(f"[DEBUG] Handling account update for: {account_data}")

            account = account_data.get('pubkey')
            if not account:
                return
                
            if account == self.SOL_WSOL_MARKET:
                await self.process_queue.put({
                    'type': 'market_update',
                    'data': account_data
                })
            elif account == self.AMM_ID:
                await self.process_queue.put({
                    'type': 'amm_update',
                    'data': account_data
                })
                
        except Exception as e:
            logger.error(f"Error handling account update: {e}")

    async def _process_messages(self):
        """Process messages from all queues"""
        while self.active:
            try:
                # Process with timeout to prevent blocking
                await asyncio.wait_for(self._process_message_batch(), timeout=0.1)
            except asyncio.TimeoutError:
                continue
            except Exception as e:
                logger.error(f"Message processing error: {e}")
                await asyncio.sleep(0.1)

    async def _process_message_batch(self):
        """Process a batch of messages from queues"""
        try:
            # Process up to 100 messages per batch
            for _ in range(100):
                if not self.mempool_queue.empty():
                    message = await self.mempool_queue.get()
                    self.metrics.processed_txs += 1
                    await self.process_queue.put({
                        'type': 'mempool',
                        'data': message
                    })
                    logger.debug("Processed mempool message")

                if not self.price_queue.empty():
                    price_data = await self.price_queue.get()
                    await self.process_queue.put({
                        'type': 'price',
                        'data': price_data
                    })
                    logger.debug("Processed price update")

                await asyncio.sleep(0)  # Yield to other tasks
                
        except Exception as e:
            logger.error(f"Batch processing error: {e}")

    async def _monitor_connections(self):
        """Monitor WebSocket connections health"""
        logger.debug("[INFO] Starting connection monitoring...")
        while self.active:
            try:
                current_time = time.time()
                
                # Check Helius connection
                if self.ws_helius and self.ws_helius.closed:
                    logger.warning("Helius connection closed")
                    self.ws_helius = None
                    
                # Check QuickNode connection
                if self.ws_quicknode and self.ws_quicknode.closed:
                    logger.warning("QuickNode connection closed")
                    self.ws_quicknode = None
                    
                # Check price data freshness
                if current_time - self.last_price_update > 5:
                    logger.warning("Price data stale")
                    
                await asyncio.sleep(1)

            except Exception as e:
                logger.error(f"Connection monitoring error: {e}")
                await asyncio.sleep(1)

    async def cleanup(self):
        """Cleanup WebSocket connections and resources"""
        logger.info("Starting WebSocket cleanup...")
        self.active = False
        
        try:
            # Close WebSocket connections
            if self.ws_helius:
                await self.ws_helius.close()
                logger.info("Helius WebSocket closed")
                
            if self.ws_quicknode:
                await self.ws_quicknode.close()
                logger.info("QuickNode WebSocket closed")
            
            # Clear queues
            await self._clear_queues()
            
            # Clean up GPU resources
            if torch.cuda.is_available():
                try:
                    torch.cuda.synchronize()
                    for stream in self.streams.values():
                        stream.synchronize()
                    torch.cuda.empty_cache()
                    logger.info("GPU resources cleaned up")
                except Exception as e:
                    logger.error(f"GPU cleanup error: {e}")
            
            # Shutdown thread pool
            self.thread_pool.shutdown(wait=False)
            
            # Log final metrics
            final_metrics = self.get_connection_status()
            logger.info(f"Final WebSocket metrics: {json.dumps(final_metrics, indent=2)}")
            
        except Exception as e:
            logger.error(f"Error during cleanup: {e}")
            raise
        finally:
            logger.info("WebSocket cleanup completed")

    async def _clear_queues(self):
        """Clear all message queues"""
        try:
            logger.debug("Clearing all message queues...")
            queues = [self.mempool_queue, self.price_queue, self.process_queue]
            for queue in queues:
                while not queue.empty():
                    try:
                        queue.get_nowait()
                    except asyncio.QueueEmpty:
                        break
            logger.info("All queues cleared")
        except Exception as e:
            logger.error(f"Error clearing queues: {e}")

    async def get_current_price(self) -> Optional[PriceData]:
        """Get current price data with freshness check"""
        if self.price_data and time.time() - self.last_price_update < 5:
            logger.debug(f"[DEBUG] Current price data retrieved: {self.price_data}")
            return self.price_data
        logger.debug("[DEBUG] No fresh price data available.")
        return None

    async def get_connection_status(self) -> Dict:
        """Get current connection status"""
        status = {
            'helius_connected': bool(self.ws_helius and not self.ws_helius.closed),
            'quicknode_connected': bool(self.ws_quicknode and not self.ws_quicknode.closed),
            'price_data_fresh': bool(self.price_data and time.time() - self.last_price_update < 5),
            'mempool_queue_size': self.mempool_queue.qsize(),
            'price_queue_size': self.price_queue.qsize(),
            'process_queue_size': self.process_queue.qsize(),
            'metrics': {
                'messages_received': self.metrics.messages_received,
                'reconnect_count': self.metrics.reconnect_count,
                'errors': self.metrics.errors,
                'mempool_hits': self.metrics.mempool_hits,
                'processed_txs': self.metrics.processed_txs,
                'uptime': time.time() - self.metrics.start_time
            }
        }
        logger.debug(f"[DEBUG] Connection status: {status}")
        return status

    async def get_market_depth(self) -> Dict:
        """Get current market depth"""
        logger.debug("[INFO] Getting current market depth...")
        try:
            if not self.ws_quicknode:
                logger.warning("[WARNING] QuickNode WebSocket is not connected.")
                return {}

            await self.ws_quicknode.send(json.dumps({
                "jsonrpc": "2.0",
                "id": "depth",
                "method": "getOrderbook",
                "params": [
                    self.SOL_WSOL_MARKET,
                    20  # Depth levels
                ]
            }))

            response = await self.ws_quicknode.recv()
            data = json.loads(response)
            
            if 'result' in data:
                logger.debug("[DEBUG] Market depth data retrieved.")
                return {
                    'bids': data['result']['bids'],
                    'asks': data['result']['asks'],
                    'timestamp': time.time()
                }
            logger.warning("[WARNING] No market depth data found in response.")
            return {}

        except Exception as e:
            logger.error(f"Error getting market depth: {e}")
            return {}

    def _calculate_latency(self, start_time: float) -> float:
        """Calculate message processing latency"""
        latency = time.time() - start_time
        self.metrics.total_latency += latency
        self.metrics.peak_latency = max(self.metrics.peak_latency, latency)
        logger.debug(f"[DEBUG] Latency calculated: {latency:.3f}s")
        return latency

    def _log_error(self, error: Exception, context: str = ""):
        """Log error with context"""
        error_msg = f"{context} error: {str(error)}" if context else str(error)
        logger.error(error_msg)
        self.metrics.errors += 1   PLEASE FIX THIS TO ONLY USE MY .ENV NOT CONFIG FILE PLEASE