"""
Position Tracker Module
Real-time tracking of open positions from Hyperliquid exchange
"""

import threading
import logging
from datetime import datetime
from typing import Dict, List, Optional, Callable, Any

from hyperliquid_monitor_plus.utils.sdk_adapter import get_info

from hyperliquid_monitor_plus.positions.types import (
    LivePosition,
    PositionChange,
    PositionSnapshot,
    AccountState,
    PositionEventType
)


# Setup logging
logger = logging.getLogger(__name__)

# Callback types
PositionChangeCallback = Callable[[PositionChange], None]
SnapshotCallback = Callable[[PositionSnapshot], None]


class PositionTracker:
    """
    Tracks open positions for whale wallets in real-time
    
    Features:
    - Fetch positions from Hyperliquid API
    - Detect position changes (open/close/increase/decrease)
    - Calculate liquidation risk
    - Maintain position history
    - Thread-safe operations
    - Callbacks for position events
    """
    
    def __init__(
        self,
        address: str,
        position_callback: Optional[PositionChangeCallback] = None,
        snapshot_callback: Optional[SnapshotCallback] = None,
        use_testnet: bool = False
    ):
        """
        Initialize Position Tracker
        
        Args:
            address: Wallet address to track
            position_callback: Callback for position changes
            snapshot_callback: Callback for position snapshots
            use_testnet: Use testnet API instead of mainnet
        """
        self._validate_address(address)
        self._address = address.lower()
        self._position_callback = position_callback
        self._snapshot_callback = snapshot_callback
        
        # Initialize Hyperliquid Info API
        self._info = get_info(use_testnet=use_testnet)
        
        # Internal state
        self._positions: Dict[str, LivePosition] = {}
        self._account_state: Optional[AccountState] = None
        self._snapshots: List[PositionSnapshot] = []
        self._changes: List[PositionChange] = []
        self._lock = threading.RLock()
        self._last_update: Optional[datetime] = None
    
    @staticmethod
    def _validate_address(address: str) -> None:
        """Validate Ethereum address format"""
        if not isinstance(address, str) or len(address) != 42 or not address.startswith('0x'):
            raise ValueError(f"Invalid Ethereum address format: {address}")
    
    def fetch_positions(self) -> PositionSnapshot:
        """
        Fetch current positions from exchange
        
        Returns:
            PositionSnapshot with all current positions
        """
        with self._lock:
            try:
                # Fetch user state from Hyperliquid
                user_state = self._info.user_state(self._address)
                
                if not user_state:
                    logger.warning(f"No user state returned for {self._address}")
                    return self._create_empty_snapshot()
                
                # Parse positions
                new_positions = self._parse_positions(user_state)
                
                # Parse account state
                account_state = self._parse_account_state(user_state)
                self._account_state = account_state
                
                # Detect changes
                changes = self._detect_changes(new_positions)
                
                # Update internal state
                old_positions = self._positions.copy()
                self._positions = new_positions
                self._last_update = datetime.now()
                
                # Process changes
                for change in changes:
                    self._changes.append(change)
                    if self._position_callback:
                        try:
                            self._position_callback(change)
                        except Exception as e:
                            logger.error(f"Position callback error: {e}")
                
                # Create snapshot
                snapshot = PositionSnapshot(
                    timestamp=datetime.now(),
                    address=self._address,
                    positions=new_positions.copy(),
                    total_unrealized_pnl=sum(p.unrealized_pnl for p in new_positions.values()),
                    total_margin_used=account_state.total_margin_used,
                    account_value=account_state.account_value,
                    free_margin=account_state.free_margin
                )
                
                self._snapshots.append(snapshot)
                
                # Trigger snapshot callback
                if self._snapshot_callback:
                    try:
                        self._snapshot_callback(snapshot)
                    except Exception as e:
                        logger.error(f"Snapshot callback error: {e}")
                
                return snapshot
                
            except Exception as e:
                logger.error(f"Error fetching positions: {e}")
                raise
    
    def _parse_positions(self, user_state: Dict) -> Dict[str, LivePosition]:
        """
        Parse positions from user state response
        
        Args:
            user_state: Response from info.user_state()
            
        Returns:
            Dictionary of coin -> LivePosition
        """
        positions = {}
        
        # Get asset positions from user state
        asset_positions = user_state.get("assetPositions", [])
        
        for asset_pos in asset_positions:
            position_data = asset_pos.get("position", {})
            
            coin = position_data.get("coin", "")
            if not coin:
                continue
            
            # Parse size and determine side
            size_str = position_data.get("szi", "0")
            size = float(size_str)
            
            if size > 0:
                side = "LONG"
                abs_size = size
            elif size < 0:
                side = "SHORT"
                abs_size = abs(size)
            else:
                side = "NEUTRAL"
                abs_size = 0.0
            
            # Parse other fields
            entry_price = float(position_data.get("entryPx", 0))
            
            # Get mark price from position or calculate
            # Note: API may not always return mark price directly
            unrealized_pnl = float(position_data.get("unrealizedPnl", 0))
            
            # Calculate mark price from unrealized PnL if not provided
            mark_price = entry_price  # Default
            if abs_size > 0 and entry_price > 0:
                if side == "LONG":
                    mark_price = entry_price + (unrealized_pnl / abs_size)
                elif side == "SHORT":
                    mark_price = entry_price - (unrealized_pnl / abs_size)
            
            # Parse liquidation price
            liq_px = position_data.get("liquidationPx")
            liquidation_price = float(liq_px) if liq_px else None
            
            # Parse leverage and margin
            leverage_data = position_data.get("leverage", {})
            if isinstance(leverage_data, dict):
                leverage = float(leverage_data.get("value", 1))
                max_leverage = float(leverage_data.get("rawUsd", 50))
            else:
                leverage = float(leverage_data) if leverage_data else 1.0
                max_leverage = 50.0
            
            margin_used = float(position_data.get("marginUsed", 0))
            
            # Create position
            position = LivePosition(
                coin=coin,
                side=side,
                size=abs_size,
                entry_price=entry_price,
                mark_price=mark_price,
                liquidation_price=liquidation_price,
                unrealized_pnl=unrealized_pnl,
                margin_used=margin_used,
                leverage=leverage,
                max_leverage=max_leverage,
                funding_rate=float(position_data.get("cumFunding", {}).get("sinceOpen", 0)),
                last_updated=datetime.now()
            )
            
            positions[coin] = position
        
        return positions
    
    def _parse_account_state(self, user_state: Dict) -> AccountState:
        """
        Parse account state from user state response
        
        Args:
            user_state: Response from info.user_state()
            
        Returns:
            AccountState object
        """
        margin_summary = user_state.get("marginSummary", {})
        cross_margin = user_state.get("crossMarginSummary", {})
        
        account_value = float(margin_summary.get("accountValue", 0))
        total_margin_used = float(margin_summary.get("totalMarginUsed", 0))
        
        # Calculate free margin
        free_margin = account_value - total_margin_used
        
        # Get withdrawable amount
        withdrawable = float(user_state.get("withdrawable", 0))
        
        return AccountState(
            address=self._address,
            account_value=account_value,
            total_margin_used=total_margin_used,
            free_margin=free_margin,
            withdrawable=withdrawable,
            cross_margin_summary=cross_margin if cross_margin else None,
            timestamp=datetime.now()
        )
    
    def _detect_changes(self, new_positions: Dict[str, LivePosition]) -> List[PositionChange]:
        """
        Detect changes between old and new positions
        
        Args:
            new_positions: Newly fetched positions
            
        Returns:
            List of position changes
        """
        changes = []
        now = datetime.now()
        
        # Check for new or modified positions
        for coin, new_pos in new_positions.items():
            old_pos = self._positions.get(coin)
            
            if old_pos is None:
                # New position opened
                if new_pos.is_open():
                    change = PositionChange(
                        timestamp=now,
                        coin=coin,
                        event_type="OPENED",
                        old_position=None,
                        new_position=new_pos,
                        size_change=new_pos.size,
                        pnl_change=new_pos.unrealized_pnl
                    )
                    changes.append(change)
            else:
                # Existing position - check for changes
                size_change = new_pos.size - old_pos.size
                pnl_change = new_pos.unrealized_pnl - old_pos.unrealized_pnl
                
                # Determine event type
                event_type: PositionEventType = "UPDATED"
                
                if not new_pos.is_open() and old_pos.is_open():
                    event_type = "CLOSED"
                elif abs(size_change) > 0.0001:  # Significant size change
                    if size_change > 0:
                        event_type = "INCREASED"
                    else:
                        event_type = "DECREASED"
                
                # Only record if there's a meaningful change
                if event_type != "UPDATED" or abs(pnl_change) > 0.01:
                    change = PositionChange(
                        timestamp=now,
                        coin=coin,
                        event_type=event_type,
                        old_position=old_pos,
                        new_position=new_pos,
                        size_change=size_change,
                        pnl_change=pnl_change
                    )
                    changes.append(change)
        
        # Check for closed positions (in old but not in new)
        for coin, old_pos in self._positions.items():
            if coin not in new_positions and old_pos.is_open():
                # Position was closed
                empty_pos = LivePosition(coin=coin)
                change = PositionChange(
                    timestamp=now,
                    coin=coin,
                    event_type="CLOSED",
                    old_position=old_pos,
                    new_position=empty_pos,
                    size_change=-old_pos.size,
                    pnl_change=-old_pos.unrealized_pnl
                )
                changes.append(change)
        
        return changes
    
    def _create_empty_snapshot(self) -> PositionSnapshot:
        """Create an empty snapshot"""
        return PositionSnapshot(
            timestamp=datetime.now(),
            address=self._address,
            positions={},
            total_unrealized_pnl=0.0,
            total_margin_used=0.0,
            account_value=0.0,
            free_margin=0.0
        )
    
    def get_position(self, coin: str) -> Optional[LivePosition]:
        """
        Get current position for a coin
        
        Args:
            coin: Coin symbol
            
        Returns:
            LivePosition or None if not found
        """
        with self._lock:
            return self._positions.get(coin)
    
    def get_all_positions(self) -> Dict[str, LivePosition]:
        """
        Get all current positions
        
        Returns:
            Dictionary of coin -> LivePosition
        """
        with self._lock:
            return self._positions.copy()
    
    def get_open_positions(self) -> Dict[str, LivePosition]:
        """
        Get only open positions
        
        Returns:
            Dictionary of coin -> LivePosition for open positions
        """
        with self._lock:
            return {
                coin: pos for coin, pos in self._positions.items()
                if pos.is_open()
            }
    
    def get_account_state(self) -> Optional[AccountState]:
        """
        Get current account state
        
        Returns:
            AccountState or None if not fetched yet
        """
        with self._lock:
            return self._account_state
    
    def get_risky_positions(self, threshold: float = 10.0) -> List[LivePosition]:
        """
        Get positions at liquidation risk
        
        Args:
            threshold: Percentage threshold for risk
            
        Returns:
            List of at-risk positions
        """
        with self._lock:
            return [
                pos for pos in self._positions.values()
                if pos.is_at_risk(threshold)
            ]
    
    def get_snapshots(self, limit: Optional[int] = None) -> List[PositionSnapshot]:
        """
        Get position snapshots history
        
        Args:
            limit: Optional limit on number of snapshots
            
        Returns:
            List of snapshots (newest first)
        """
        with self._lock:
            snapshots = self._snapshots.copy()
            snapshots.reverse()  # Newest first
            if limit:
                return snapshots[:limit]
            return snapshots
    
    def get_changes(self, coin: Optional[str] = None) -> List[PositionChange]:
        """
        Get position changes history
        
        Args:
            coin: Optional coin to filter by
            
        Returns:
            List of position changes
        """
        with self._lock:
            if coin:
                return [c for c in self._changes if c.coin == coin]
            return self._changes.copy()
    
    def get_statistics(self) -> Dict[str, Any]:
        """
        Get comprehensive statistics
        
        Returns:
            Dictionary with position statistics
        """
        with self._lock:
            open_positions = self.get_open_positions()
            
            total_unrealized = sum(p.unrealized_pnl for p in open_positions.values())
            total_notional = sum(p.notional_value for p in open_positions.values())
            risky = self.get_risky_positions()
            
            # Find best and worst
            best_pos = max(open_positions.values(), key=lambda p: p.unrealized_pnl) if open_positions else None
            worst_pos = min(open_positions.values(), key=lambda p: p.unrealized_pnl) if open_positions else None
            largest = max(open_positions.values(), key=lambda p: p.notional_value) if open_positions else None
            
            return {
                "address": self._address,
                "open_positions_count": len(open_positions),
                "total_unrealized_pnl": total_unrealized,
                "total_notional_value": total_notional,
                "risky_positions_count": len(risky),
                "account_value": self._account_state.account_value if self._account_state else 0.0,
                "margin_used": self._account_state.total_margin_used if self._account_state else 0.0,
                "free_margin": self._account_state.free_margin if self._account_state else 0.0,
                "margin_utilization": self._account_state.margin_ratio if self._account_state else 0.0,
                "best_position": best_pos.coin if best_pos else None,
                "best_pnl": best_pos.unrealized_pnl if best_pos else 0.0,
                "worst_position": worst_pos.coin if worst_pos else None,
                "worst_pnl": worst_pos.unrealized_pnl if worst_pos else 0.0,
                "largest_position": largest.coin if largest else None,
                "largest_notional": largest.notional_value if largest else 0.0,
                "total_snapshots": len(self._snapshots),
                "total_changes": len(self._changes),
                "last_updated": self._last_update.isoformat() if self._last_update else None,
                "positions": {
                    coin: {
                        "side": pos.side,
                        "size": pos.size,
                        "entry_price": pos.entry_price,
                        "mark_price": pos.mark_price,
                        "unrealized_pnl": pos.unrealized_pnl,
                        "pnl_percentage": pos.pnl_percentage,
                        "leverage": pos.leverage,
                        "liquidation_price": pos.liquidation_price,
                        "distance_to_liq": pos.distance_to_liquidation
                    }
                    for coin, pos in open_positions.items()
                }
            }
    
    def reset(self) -> None:
        """Reset all tracking data"""
        with self._lock:
            self._positions.clear()
            self._account_state = None
            self._snapshots.clear()
            self._changes.clear()
            self._last_update = None
    
    def get_summary_report(self) -> str:
        """
        Generate a text summary report of positions
        
        Returns:
            Formatted string report
        """
        stats = self.get_statistics()
        
        report_lines = [
            "=" * 50,
            "POSITION TRACKER REPORT",
            "=" * 50,
            "",
            f"Address: {stats['address'][:10]}...{stats['address'][-8:]}",
            f"Last Updated: {stats['last_updated'] or 'Never'}",
            "",
            "ACCOUNT STATUS",
            f"  Account Value: ${stats['account_value']:,.2f}",
            f"  Margin Used: ${stats['margin_used']:,.2f}",
            f"  Free Margin: ${stats['free_margin']:,.2f}",
            f"  Margin Utilization: {stats['margin_utilization']:.1f}%",
            "",
            "POSITIONS OVERVIEW",
            f"  Open Positions: {stats['open_positions_count']}",
            f"  Total Unrealized PnL: ${stats['total_unrealized_pnl']:,.2f}",
            f"  Total Notional Value: ${stats['total_notional_value']:,.2f}",
            f"  At-Risk Positions: {stats['risky_positions_count']}",
        ]
        
        if stats['best_position']:
            report_lines.append(f"  Best: {stats['best_position']} (${stats['best_pnl']:,.2f})")
        if stats['worst_position']:
            report_lines.append(f"  Worst: {stats['worst_position']} (${stats['worst_pnl']:,.2f})")
        if stats['largest_position']:
            report_lines.append(f"  Largest: {stats['largest_position']} (${stats['largest_notional']:,.2f})")
        
        if stats['positions']:
            report_lines.append("")
            report_lines.append("OPEN POSITIONS DETAIL:")
            for coin, pos_info in stats['positions'].items():
                liq_dist = pos_info['distance_to_liq']
                liq_str = f"{liq_dist:.1f}% to liq" if liq_dist else "N/A"
                report_lines.append(
                    f"  {coin}: {pos_info['side']} {pos_info['size']:.4f} @ "
                    f"${pos_info['entry_price']:.2f} -> ${pos_info['mark_price']:.2f} "
                    f"({pos_info['pnl_percentage']:+.1f}%) [{liq_str}]"
                )
        
        report_lines.extend(["", "=" * 50])
        
        return "\n".join(report_lines)
