"""
MetaTrader 5 Trading Client

Uses official MetaTrader5 Python library to connect to MT5 terminal for trading.
Note: Requires Windows platform and MT5 terminal installed.
"""

import time
import threading
from dataclasses import dataclass, field
from typing import Optional, Dict, Any, List
from datetime import datetime

from app.utils.logger import get_logger
from app.services.mt5_trading.symbols import normalize_symbol, parse_symbol

logger = get_logger(__name__)

# Lazy import MetaTrader5 to allow other features to work without it installed
mt5 = None


def _ensure_mt5():
    """Ensure MetaTrader5 is imported."""
    global mt5
    if mt5 is None:
        try:
            import MetaTrader5 as _mt5
            mt5 = _mt5
        except ImportError:
            raise ImportError(
                "MetaTrader5 is not installed. Run: pip install MetaTrader5\n"
                "Note: This library only works on Windows with MT5 terminal installed."
            )
    return mt5


@dataclass
class MT5Config:
    """MT5 connection configuration."""
    login: int = 0  # MT5 account number
    password: str = ""  # MT5 password
    server: str = ""  # Broker server name (e.g., "ICMarkets-Demo")
    terminal_path: str = ""  # Optional: path to terminal64.exe
    timeout: int = 60000  # Connection timeout in milliseconds
    magic_number: int = 123456  # EA magic number for identifying orders


@dataclass
class OrderResult:
    """Order execution result."""
    success: bool
    order_id: int = 0
    deal_id: int = 0
    filled: float = 0.0
    price: float = 0.0
    status: str = ""
    message: str = ""
    raw: Dict[str, Any] = field(default_factory=dict)


class MT5Client:
    """
    MetaTrader 5 Trading Client
    
    Usage:
        config = MT5Config(
            login=12345678,
            password="your_password",
            server="ICMarkets-Demo"
        )
        client = MT5Client(config)
        
        if client.connect():
            # Place order
            result = client.place_market_order("EURUSD", "buy", 0.1)
            
            # Get positions
            positions = client.get_positions()
            
            client.disconnect()
    """
    
    def __init__(self, config: Optional[MT5Config] = None):
        self.config = config or MT5Config()
        self._connected = False
        self._lock = threading.Lock()
    
    @property
    def connected(self) -> bool:
        """Check if connected to MT5 terminal."""
        if not self._connected:
            return False
        try:
            _ensure_mt5()
            info = mt5.terminal_info()
            return info is not None and info.connected
        except Exception:
            return False
    
    def connect(self) -> bool:
        """
        Connect to MT5 terminal.
        
        Returns:
            True if connected successfully
        """
        with self._lock:
            if self.connected:
                return True
            
            try:
                _ensure_mt5()
                
                # Initialize MT5 connection
                init_params = {}
                
                if self.config.terminal_path:
                    init_params["path"] = self.config.terminal_path
                
                if self.config.login and self.config.password and self.config.server:
                    init_params["login"] = self.config.login
                    init_params["password"] = self.config.password
                    init_params["server"] = self.config.server
                    init_params["timeout"] = self.config.timeout
                
                logger.info(f"Connecting to MT5: server={self.config.server}, login={self.config.login}")
                
                if init_params:
                    initialized = mt5.initialize(**init_params)
                else:
                    # Connect to already running terminal
                    initialized = mt5.initialize()
                
                if not initialized:
                    error = mt5.last_error()
                    logger.error(f"MT5 initialization failed: {error}")
                    return False
                
                self._connected = True
                
                # Log account info
                account_info = mt5.account_info()
                if account_info:
                    logger.info(f"MT5 connected: account={account_info.login}, "
                               f"server={account_info.server}, balance={account_info.balance}")
                else:
                    logger.warning("MT5 connected but account info not available")
                
                return True
                
            except Exception as e:
                logger.error(f"MT5 connection failed: {e}")
                self._connected = False
                return False
    
    def disconnect(self):
        """Disconnect from MT5 terminal."""
        with self._lock:
            if self._connected:
                try:
                    _ensure_mt5()
                    mt5.shutdown()
                except Exception as e:
                    logger.warning(f"MT5 disconnect exception: {e}")
                finally:
                    self._connected = False
                    logger.info("MT5 disconnected")
    
    def _ensure_connected(self):
        """Ensure connection is established."""
        if not self.connected:
            if not self.connect():
                raise ConnectionError("Cannot connect to MT5 terminal")
    
    # ==================== Order Methods ====================
    
    def place_market_order(
        self,
        symbol: str,
        side: str,
        volume: float,
        deviation: int = 20,
        comment: str = "QuantDinger",
    ) -> OrderResult:
        """
        Place a market order.
        
        Args:
            symbol: Trading symbol (e.g., "EURUSD")
            side: Direction ("buy" or "sell")
            volume: Lot size (e.g., 0.1 = 1 mini lot)
            deviation: Maximum price deviation in points
            comment: Order comment
            
        Returns:
            OrderResult
        """
        try:
            self._ensure_connected()
            _ensure_mt5()
            
            # Normalize symbol
            symbol = normalize_symbol(symbol)
            
            # Get symbol info
            symbol_info = mt5.symbol_info(symbol)
            if symbol_info is None:
                return OrderResult(
                    success=False,
                    message=f"Symbol not found: {symbol}"
                )
            
            # Validate volume against symbol constraints
            volume_float = float(volume)
            if volume_float < symbol_info.volume_min:
                return OrderResult(
                    success=False,
                    message=f"Volume {volume_float} is less than minimum {symbol_info.volume_min}"
                )
            if volume_float > symbol_info.volume_max:
                return OrderResult(
                    success=False,
                    message=f"Volume {volume_float} exceeds maximum {symbol_info.volume_max}"
                )
            # Round volume to lot step
            volume_step = symbol_info.volume_step
            if volume_step > 0:
                volume_float = round(volume_float / volume_step) * volume_step
            
            if not symbol_info.visible:
                # Enable symbol in Market Watch
                if not mt5.symbol_select(symbol, True):
                    return OrderResult(
                        success=False,
                        message=f"Failed to select symbol: {symbol}"
                    )
            
            # Get current price
            tick = mt5.symbol_info_tick(symbol)
            if tick is None:
                return OrderResult(
                    success=False,
                    message=f"Failed to get tick for: {symbol}"
                )
            
            # Determine order type and price
            if side.lower() == "buy":
                order_type = mt5.ORDER_TYPE_BUY
                price = tick.ask
            else:
                order_type = mt5.ORDER_TYPE_SELL
                price = tick.bid
            
            # Determine filling mode based on symbol properties
            # Different brokers support different filling modes
            filling_mode = mt5.ORDER_FILLING_IOC  # Default
            if symbol_info.filling_mode & mt5.ORDER_FILLING_IOC:
                filling_mode = mt5.ORDER_FILLING_IOC
            elif symbol_info.filling_mode & mt5.ORDER_FILLING_FOK:
                filling_mode = mt5.ORDER_FILLING_FOK
            elif symbol_info.filling_mode & mt5.ORDER_FILLING_RETURN:
                filling_mode = mt5.ORDER_FILLING_RETURN
            
            # Prepare order request
            request = {
                "action": mt5.TRADE_ACTION_DEAL,
                "symbol": symbol,
                "volume": volume_float,  # Use validated and rounded volume
                "type": order_type,
                "price": price,
                "deviation": deviation,
                "magic": self.config.magic_number,
                "comment": comment,
                "type_time": mt5.ORDER_TIME_GTC,
                "type_filling": filling_mode,
            }
            
            # Send order
            result = mt5.order_send(request)
            
            if result is None:
                error = mt5.last_error()
                return OrderResult(
                    success=False,
                    message=f"Order send failed: {error}"
                )
            
            if result.retcode != mt5.TRADE_RETCODE_DONE:
                return OrderResult(
                    success=False,
                    order_id=result.order if hasattr(result, 'order') else 0,
                    status=str(result.retcode),
                    message=f"Order rejected: {result.comment}",
                    raw=result._asdict() if hasattr(result, '_asdict') else {}
                )
            
            return OrderResult(
                success=True,
                order_id=result.order,
                deal_id=result.deal,
                filled=result.volume,
                price=result.price,
                status="filled",
                message="Order executed",
                raw=result._asdict() if hasattr(result, '_asdict') else {}
            )
            
        except Exception as e:
            logger.error(f"Market order failed: {e}")
            return OrderResult(
                success=False,
                message=str(e)
            )
    
    def place_limit_order(
        self,
        symbol: str,
        side: str,
        volume: float,
        price: float,
        comment: str = "QuantDinger",
    ) -> OrderResult:
        """
        Place a pending limit order.
        
        Args:
            symbol: Trading symbol
            side: Direction ("buy" or "sell")
            volume: Lot size
            price: Limit price
            comment: Order comment
            
        Returns:
            OrderResult
        """
        try:
            self._ensure_connected()
            _ensure_mt5()
            
            symbol = normalize_symbol(symbol)
            
            symbol_info = mt5.symbol_info(symbol)
            if symbol_info is None:
                return OrderResult(
                    success=False,
                    message=f"Symbol not found: {symbol}"
                )
            
            # Validate and round volume (same as market order)
            volume_float = float(volume)
            if volume_float < symbol_info.volume_min:
                return OrderResult(
                    success=False,
                    message=f"Volume {volume_float} below minimum {symbol_info.volume_min}"
                )
            if volume_float > symbol_info.volume_max:
                return OrderResult(
                    success=False,
                    message=f"Volume {volume_float} exceeds maximum {symbol_info.volume_max}"
                )
            volume_step = symbol_info.volume_step
            if volume_step > 0:
                volume_float = round(volume_float / volume_step) * volume_step
            
            if not symbol_info.visible:
                if not mt5.symbol_select(symbol, True):
                    return OrderResult(
                        success=False,
                        message=f"Failed to select symbol: {symbol}"
                    )
            
            tick = mt5.symbol_info_tick(symbol)
            if tick is None:
                return OrderResult(
                    success=False,
                    message=f"Failed to get tick for: {symbol}"
                )
            
            # Determine order type based on side and price relative to market
            if side.lower() == "buy":
                if price < tick.ask:
                    order_type = mt5.ORDER_TYPE_BUY_LIMIT
                else:
                    order_type = mt5.ORDER_TYPE_BUY_STOP
            else:
                if price > tick.bid:
                    order_type = mt5.ORDER_TYPE_SELL_LIMIT
                else:
                    order_type = mt5.ORDER_TYPE_SELL_STOP
            
            # Determine filling mode (consistent with market order)
            filling_mode = mt5.ORDER_FILLING_RETURN  # RETURN is safest for pending orders
            if symbol_info.filling_mode & mt5.ORDER_FILLING_FOK:
                filling_mode = mt5.ORDER_FILLING_FOK
            elif symbol_info.filling_mode & mt5.ORDER_FILLING_IOC:
                filling_mode = mt5.ORDER_FILLING_IOC
            elif symbol_info.filling_mode & mt5.ORDER_FILLING_RETURN:
                filling_mode = mt5.ORDER_FILLING_RETURN
            
            request = {
                "action": mt5.TRADE_ACTION_PENDING,
                "symbol": symbol,
                "volume": volume_float,
                "type": order_type,
                "price": price,
                "magic": self.config.magic_number,
                "comment": comment,
                "type_time": mt5.ORDER_TIME_GTC,
                "type_filling": filling_mode,
            }
            
            result = mt5.order_send(request)
            
            if result is None:
                error = mt5.last_error()
                return OrderResult(
                    success=False,
                    message=f"Order send failed: {error}"
                )
            
            if result.retcode != mt5.TRADE_RETCODE_DONE:
                return OrderResult(
                    success=False,
                    status=str(result.retcode),
                    message=f"Order rejected: {result.comment}",
                )
            
            return OrderResult(
                success=True,
                order_id=result.order,
                price=price,
                status="pending",
                message="Pending order placed",
                raw=result._asdict() if hasattr(result, '_asdict') else {}
            )
            
        except Exception as e:
            logger.error(f"Limit order failed: {e}")
            return OrderResult(
                success=False,
                message=str(e)
            )
    
    def close_position(
        self,
        ticket: int,
        volume: Optional[float] = None,
        deviation: int = 20,
        comment: str = "QuantDinger close",
    ) -> OrderResult:
        """
        Close an open position.
        
        Args:
            ticket: Position ticket number
            volume: Volume to close (None = close all)
            deviation: Maximum price deviation
            comment: Order comment
            
        Returns:
            OrderResult
        """
        try:
            self._ensure_connected()
            _ensure_mt5()
            
            # Get position info
            position = mt5.positions_get(ticket=ticket)
            if not position:
                return OrderResult(
                    success=False,
                    message=f"Position not found: {ticket}"
                )
            
            pos = position[0]
            symbol = pos.symbol
            
            # Get symbol info for filling mode
            symbol_info = mt5.symbol_info(symbol)
            if symbol_info is None:
                return OrderResult(
                    success=False,
                    message=f"Symbol not found: {symbol}"
                )
            
            # Get tick
            tick = mt5.symbol_info_tick(symbol)
            if tick is None:
                return OrderResult(
                    success=False,
                    message=f"Failed to get tick for: {symbol}"
                )
            
            # Determine close direction and price
            if pos.type == mt5.POSITION_TYPE_BUY:
                order_type = mt5.ORDER_TYPE_SELL
                price = tick.bid
            else:
                order_type = mt5.ORDER_TYPE_BUY
                price = tick.ask
            
            close_volume = volume if volume else pos.volume
            
            # Determine filling mode based on symbol properties
            filling_mode = mt5.ORDER_FILLING_IOC  # Default
            if symbol_info.filling_mode & mt5.ORDER_FILLING_IOC:
                filling_mode = mt5.ORDER_FILLING_IOC
            elif symbol_info.filling_mode & mt5.ORDER_FILLING_FOK:
                filling_mode = mt5.ORDER_FILLING_FOK
            elif symbol_info.filling_mode & mt5.ORDER_FILLING_RETURN:
                filling_mode = mt5.ORDER_FILLING_RETURN
            
            request = {
                "action": mt5.TRADE_ACTION_DEAL,
                "symbol": symbol,
                "volume": float(close_volume),
                "type": order_type,
                "position": ticket,
                "price": price,
                "deviation": deviation,
                "magic": self.config.magic_number,
                "comment": comment,
                "type_time": mt5.ORDER_TIME_GTC,
                "type_filling": filling_mode,
            }
            
            result = mt5.order_send(request)
            
            if result is None or result.retcode != mt5.TRADE_RETCODE_DONE:
                return OrderResult(
                    success=False,
                    message=f"Close failed: {result.comment if result else 'Unknown error'}"
                )
            
            return OrderResult(
                success=True,
                order_id=result.order,
                deal_id=result.deal,
                filled=result.volume,
                price=result.price,
                status="closed",
                message="Position closed",
            )
            
        except Exception as e:
            logger.error(f"Close position failed: {e}")
            return OrderResult(
                success=False,
                message=str(e)
            )
    
    def cancel_order(self, ticket: int) -> bool:
        """
        Cancel a pending order.
        
        Args:
            ticket: Order ticket number
            
        Returns:
            True if cancelled successfully
        """
        try:
            self._ensure_connected()
            _ensure_mt5()
            
            request = {
                "action": mt5.TRADE_ACTION_REMOVE,
                "order": ticket,
            }
            
            result = mt5.order_send(request)
            
            if result is None or result.retcode != mt5.TRADE_RETCODE_DONE:
                logger.warning(f"Cancel order failed: {result.comment if result else 'Unknown'}")
                return False
            
            logger.info(f"Order {ticket} cancelled")
            return True
            
        except Exception as e:
            logger.error(f"Cancel order failed: {e}")
            return False
    
    # ==================== Query Methods ====================
    
    def get_account_info(self) -> Dict[str, Any]:
        """
        Get account information.
        
        Returns:
            Account info dictionary
        """
        try:
            self._ensure_connected()
            _ensure_mt5()
            
            info = mt5.account_info()
            if info is None:
                return {"success": False, "error": "Failed to get account info"}
            
            return {
                "success": True,
                "login": info.login,
                "server": info.server,
                "name": info.name,
                "currency": info.currency,
                "balance": info.balance,
                "equity": info.equity,
                "margin": info.margin,
                "margin_free": info.margin_free,
                "margin_level": info.margin_level,
                "profit": info.profit,
                "leverage": info.leverage,
                "trade_allowed": info.trade_allowed,
                "trade_expert": info.trade_expert,
            }
            
        except Exception as e:
            logger.error(f"Get account info failed: {e}")
            return {"success": False, "error": str(e)}
    
    def get_positions(self, symbol: Optional[str] = None) -> List[Dict[str, Any]]:
        """
        Get open positions.
        
        Args:
            symbol: Filter by symbol (optional)
            
        Returns:
            List of positions
        """
        try:
            self._ensure_connected()
            _ensure_mt5()
            
            if symbol:
                positions = mt5.positions_get(symbol=normalize_symbol(symbol))
            else:
                positions = mt5.positions_get()
            
            if positions is None:
                return []
            
            result = []
            for pos in positions:
                result.append({
                    "ticket": pos.ticket,
                    "symbol": pos.symbol,
                    "type": "buy" if pos.type == mt5.POSITION_TYPE_BUY else "sell",
                    "volume": pos.volume,
                    "price_open": pos.price_open,
                    "price_current": pos.price_current,
                    "sl": pos.sl,
                    "tp": pos.tp,
                    "profit": pos.profit,
                    "swap": pos.swap,
                    "magic": pos.magic,
                    "comment": pos.comment,
                    "time": datetime.fromtimestamp(pos.time).isoformat(),
                })
            
            return result
            
        except Exception as e:
            logger.error(f"Get positions failed: {e}")
            return []
    
    def get_orders(self, symbol: Optional[str] = None) -> List[Dict[str, Any]]:
        """
        Get pending orders.
        
        Args:
            symbol: Filter by symbol (optional)
            
        Returns:
            List of orders
        """
        try:
            self._ensure_connected()
            _ensure_mt5()
            
            if symbol:
                orders = mt5.orders_get(symbol=normalize_symbol(symbol))
            else:
                orders = mt5.orders_get()
            
            if orders is None:
                return []
            
            result = []
            for order in orders:
                order_type_map = {
                    mt5.ORDER_TYPE_BUY_LIMIT: "buy_limit",
                    mt5.ORDER_TYPE_SELL_LIMIT: "sell_limit",
                    mt5.ORDER_TYPE_BUY_STOP: "buy_stop",
                    mt5.ORDER_TYPE_SELL_STOP: "sell_stop",
                }
                
                result.append({
                    "ticket": order.ticket,
                    "symbol": order.symbol,
                    "type": order_type_map.get(order.type, str(order.type)),
                    "volume_initial": order.volume_initial,
                    "volume_current": order.volume_current,
                    "price_open": order.price_open,
                    "price_current": order.price_current,
                    "sl": order.sl,
                    "tp": order.tp,
                    "magic": order.magic,
                    "comment": order.comment,
                    "time_setup": datetime.fromtimestamp(order.time_setup).isoformat(),
                })
            
            return result
            
        except Exception as e:
            logger.error(f"Get orders failed: {e}")
            return []
    
    def get_quote(self, symbol: str) -> Dict[str, Any]:
        """
        Get real-time quote.
        
        Args:
            symbol: Symbol code
            
        Returns:
            Quote data
        """
        try:
            self._ensure_connected()
            _ensure_mt5()
            
            symbol = normalize_symbol(symbol)
            
            # Select symbol
            symbol_info = mt5.symbol_info(symbol)
            if symbol_info is None:
                return {"success": False, "error": f"Symbol not found: {symbol}"}
            
            if not symbol_info.visible:
                mt5.symbol_select(symbol, True)
            
            tick = mt5.symbol_info_tick(symbol)
            if tick is None:
                return {"success": False, "error": f"Failed to get tick: {symbol}"}
            
            return {
                "success": True,
                "symbol": symbol,
                "bid": tick.bid,
                "ask": tick.ask,
                "last": tick.last,
                "volume": tick.volume,
                "time": datetime.fromtimestamp(tick.time).isoformat(),
                "spread": round((tick.ask - tick.bid) / symbol_info.point, 1),
            }
            
        except Exception as e:
            logger.error(f"Get quote failed: {e}")
            return {"success": False, "error": str(e)}
    
    def get_symbols(self, group: str = "*") -> List[Dict[str, Any]]:
        """
        Get available symbols.
        
        Args:
            group: Filter by group pattern (e.g., "*USD*", "Forex*")
            
        Returns:
            List of symbol info
        """
        try:
            self._ensure_connected()
            _ensure_mt5()
            
            symbols = mt5.symbols_get(group=group)
            if symbols is None:
                return []
            
            result = []
            for s in symbols:
                result.append({
                    "name": s.name,
                    "description": s.description,
                    "path": s.path,
                    "currency_base": s.currency_base,
                    "currency_profit": s.currency_profit,
                    "digits": s.digits,
                    "point": s.point,
                    "trade_mode": s.trade_mode,
                    "volume_min": s.volume_min,
                    "volume_max": s.volume_max,
                    "volume_step": s.volume_step,
                })
            
            return result
            
        except Exception as e:
            logger.error(f"Get symbols failed: {e}")
            return []
    
    def get_connection_status(self) -> Dict[str, Any]:
        """Get connection status."""
        try:
            _ensure_mt5()
            terminal_info = mt5.terminal_info() if self._connected else None
            account_info = mt5.account_info() if self._connected else None
            
            return {
                "connected": self.connected,
                "login": self.config.login,
                "server": self.config.server,
                "account_login": account_info.login if account_info else None,
                "account_server": account_info.server if account_info else None,
                "terminal_connected": terminal_info.connected if terminal_info else False,
                "trade_allowed": terminal_info.trade_allowed if terminal_info else False,
            }
        except Exception as e:
            return {
                "connected": False,
                "error": str(e),
            }


# Global singleton (optional)
_global_client: Optional[MT5Client] = None
_global_lock = threading.Lock()


def get_mt5_client(config: Optional[MT5Config] = None) -> MT5Client:
    """
    Get global MT5 client singleton.
    
    Args:
        config: Configuration (only effective on first call)
        
    Returns:
        MT5Client instance
    """
    global _global_client
    
    with _global_lock:
        if _global_client is None:
            _global_client = MT5Client(config)
        return _global_client


def reset_mt5_client():
    """Reset global client (disconnect and clear instance)."""
    global _global_client
    
    with _global_lock:
        if _global_client is not None:
            _global_client.disconnect()
            _global_client = None
