"""
Order placement and execution engine.

Provides abstract interface for order execution that can be implemented
for backtesting (simulated) or live trading (exchange API).
"""

from abc import ABC, abstractmethod
from dataclasses import dataclass, field
from datetime import datetime
from typing import List, Optional, Dict
from enum import Enum

from normalization import normalize_price, normalize_lot_size


class OrderType(Enum):
    """Order type enumeration."""
    MARKET = "market"
    LIMIT = "limit"
    STOP = "stop"


class OrderSide(Enum):
    """Order side enumeration."""
    BUY = "buy"
    SELL = "sell"


class OrderStatus(Enum):
    """Order status enumeration."""
    PENDING = "pending"
    OPEN = "open"
    CLOSED = "closed"
    CANCELLED = "cancelled"


@dataclass
class Order:
    """Order data structure."""
    order_id: int
    symbol: str
    side: OrderSide
    order_type: OrderType
    lots: float
    price: float
    tp_price: Optional[float] = None
    sl_price: Optional[float] = None
    status: OrderStatus = OrderStatus.PENDING
    open_time: Optional[datetime] = None
    close_time: Optional[datetime] = None
    close_price: Optional[float] = None
    profit: float = 0.0
    magic: int = 0
    comment: str = ""

    def is_open(self) -> bool:
        """Check if order is currently open."""
        return self.status == OrderStatus.OPEN

    def is_closed(self) -> bool:
        """Check if order is closed."""
        return self.status == OrderStatus.CLOSED


@dataclass
class Position:
    """Active position data structure."""
    position_id: int
    symbol: str
    side: OrderSide
    lots: float
    entry_price: float
    current_price: float
    tp_price: Optional[float] = None
    sl_price: Optional[float] = None
    open_time: Optional[datetime] = None
    magic: int = 0
    unrealized_pnl: float = 0.0

    def update_pnl(self, current_price: float) -> float:
        """Update and return unrealized P&L."""
        self.current_price = current_price
        if self.side == OrderSide.BUY:
            self.unrealized_pnl = self.lots * (current_price - self.entry_price)
        else:
            self.unrealized_pnl = self.lots * (self.entry_price - current_price)
        return self.unrealized_pnl


class OrderEngine(ABC):
    """
    Abstract base class for order execution engines.

    Implementations provide either simulated (backtest) or
    real (exchange API) order execution.
    """

    def __init__(self, symbol: str):
        """
        Initialize order engine.

        Args:
            symbol: Trading pair symbol
        """
        self.symbol = symbol
        self._order_counter = 0
        self.orders: Dict[int, Order] = {}
        self.positions: Dict[int, Position] = {}

    def _next_order_id(self) -> int:
        """Generate next order ID."""
        self._order_counter += 1
        return self._order_counter

    @abstractmethod
    def place_buy_order(
        self,
        lots: float,
        tp_price: Optional[float] = None,
        sl_price: Optional[float] = None,
        magic: int = 0,
        comment: str = ""
    ) -> Optional[Order]:
        """
        Place a buy order.

        Args:
            lots: Position size
            tp_price: Take profit price
            sl_price: Stop loss price
            magic: Magic number for tracking
            comment: Order comment

        Returns:
            Order object if successful, None otherwise
        """
        pass

    @abstractmethod
    def place_sell_order(
        self,
        lots: float,
        tp_price: Optional[float] = None,
        sl_price: Optional[float] = None,
        magic: int = 0,
        comment: str = ""
    ) -> Optional[Order]:
        """
        Place a sell order.

        Args:
            lots: Position size
            tp_price: Take profit price
            sl_price: Stop loss price
            magic: Magic number for tracking
            comment: Order comment

        Returns:
            Order object if successful, None otherwise
        """
        pass

    @abstractmethod
    def close_order(
        self,
        order_id: int,
        close_price: Optional[float] = None
    ) -> Optional[float]:
        """
        Close an open order/position.

        Args:
            order_id: Order ID to close
            close_price: Price to close at (optional, uses current if not specified)

        Returns:
            Profit/loss from the closed position
        """
        pass

    @abstractmethod
    def close_position(
        self,
        position_id: int,
        close_price: Optional[float] = None
    ) -> Optional[float]:
        """
        Close an open position.

        Args:
            position_id: Position ID to close
            close_price: Price to close at

        Returns:
            Profit/loss from the closed position
        """
        pass

    @abstractmethod
    def get_current_price(self) -> tuple:
        """
        Get current bid/ask prices.

        Returns:
            Tuple of (bid, ask) prices
        """
        pass

    @abstractmethod
    def update_prices(self, bid: float, ask: float) -> None:
        """
        Update current market prices.

        Args:
            bid: Current bid price
            ask: Current ask price
        """
        pass

    def get_open_orders(self) -> List[Order]:
        """
        Get all open orders.

        Returns:
            List of open orders
        """
        return [o for o in self.orders.values() if o.is_open()]

    def get_open_positions(self) -> List[Position]:
        """
        Get all open positions.

        Returns:
            List of open positions
        """
        return list(self.positions.values())

    def get_orders_by_magic(self, magic: int) -> List[Order]:
        """
        Get orders by magic number.

        Args:
            magic: Magic number to filter by

        Returns:
            List of matching orders
        """
        return [o for o in self.orders.values() if o.magic == magic]

    def get_positions_by_magic(self, magic: int) -> List[Position]:
        """
        Get positions by magic number.

        Args:
            magic: Magic number to filter by

        Returns:
            List of matching positions
        """
        return [p for p in self.positions.values() if p.magic == magic]

    def check_tp_hit(self, position: Position, current_price: float) -> bool:
        """
        Check if take profit has been hit.

        Args:
            position: Position to check
            current_price: Current market price

        Returns:
            True if TP has been hit
        """
        if position.tp_price is None:
            return False

        if position.side == OrderSide.BUY:
            return current_price >= position.tp_price
        else:
            return current_price <= position.tp_price

    def check_sl_hit(self, position: Position, current_price: float) -> bool:
        """
        Check if stop loss has been hit.

        Args:
            position: Position to check
            current_price: Current market price

        Returns:
            True if SL has been hit
        """
        if position.sl_price is None:
            return False

        if position.side == OrderSide.BUY:
            return current_price <= position.sl_price
        else:
            return current_price >= position.sl_price


class BacktestOrderEngine(OrderEngine):
    """
    Order engine for backtesting.

    Simulates order execution with configurable spread and slippage.
    """

    def __init__(
        self,
        symbol: str,
        initial_balance: float = 10000.0,
        commission_percent: float = 0.1,
        slippage_pips: float = 0.0
    ):
        """
        Initialize backtest order engine.

        Args:
            symbol: Trading pair symbol
            initial_balance: Starting balance
            commission_percent: Commission per trade (percentage)
            slippage_pips: Simulated slippage in pips
        """
        super().__init__(symbol)
        self.initial_balance = initial_balance
        self.balance = initial_balance
        self.commission_percent = commission_percent
        self.slippage_pips = slippage_pips

        self._current_bid = 0.0
        self._current_ask = 0.0
        self._current_time: Optional[datetime] = None

        self.trade_history: List[Order] = []
        self.total_commission = 0.0

    def set_time(self, time: datetime) -> None:
        """Set current simulation time."""
        self._current_time = time

    def update_prices(self, bid: float, ask: float) -> None:
        """Update current market prices."""
        self._current_bid = bid
        self._current_ask = ask

        # Update P&L for all open positions
        for position in self.positions.values():
            position.update_pnl(bid)

    def get_current_price(self) -> tuple:
        """Get current bid/ask prices."""
        return (self._current_bid, self._current_ask)

    def _calculate_commission(self, lots: float, price: float) -> float:
        """Calculate commission for a trade."""
        position_value = lots * price
        return position_value * (self.commission_percent / 100)

    def place_buy_order(
        self,
        lots: float,
        tp_price: Optional[float] = None,
        sl_price: Optional[float] = None,
        magic: int = 0,
        comment: str = ""
    ) -> Optional[Order]:
        """Place a buy order (executed at ask price)."""
        lots = normalize_lot_size(lots, self.symbol)
        execution_price = self._current_ask

        # Apply slippage (buy higher)
        if self.slippage_pips > 0:
            from normalization import pips_to_price
            execution_price += pips_to_price(self.slippage_pips, self.symbol)

        execution_price = normalize_price(execution_price, self.symbol)

        # Margin check: refuse order if equity is depleted
        equity = self.get_equity()
        if equity <= 0:
            return None

        # Calculate and deduct commission
        commission = self._calculate_commission(lots, execution_price)
        self.total_commission += commission
        self.balance -= commission

        order_id = self._next_order_id()

        order = Order(
            order_id=order_id,
            symbol=self.symbol,
            side=OrderSide.BUY,
            order_type=OrderType.MARKET,
            lots=lots,
            price=execution_price,
            tp_price=normalize_price(tp_price, self.symbol) if tp_price else None,
            sl_price=normalize_price(sl_price, self.symbol) if sl_price else None,
            status=OrderStatus.OPEN,
            open_time=self._current_time,
            magic=magic,
            comment=comment
        )

        self.orders[order_id] = order

        # Create position
        position = Position(
            position_id=order_id,
            symbol=self.symbol,
            side=OrderSide.BUY,
            lots=lots,
            entry_price=execution_price,
            current_price=self._current_bid,
            tp_price=order.tp_price,
            sl_price=order.sl_price,
            open_time=self._current_time,
            magic=magic
        )
        position.update_pnl(self._current_bid)
        self.positions[order_id] = position

        return order

    def place_sell_order(
        self,
        lots: float,
        tp_price: Optional[float] = None,
        sl_price: Optional[float] = None,
        magic: int = 0,
        comment: str = ""
    ) -> Optional[Order]:
        """Place a sell order (executed at bid price)."""
        lots = normalize_lot_size(lots, self.symbol)
        execution_price = self._current_bid

        # Apply slippage (sell lower)
        if self.slippage_pips > 0:
            from normalization import pips_to_price
            execution_price -= pips_to_price(self.slippage_pips, self.symbol)

        execution_price = normalize_price(execution_price, self.symbol)

        # Margin check: refuse order if equity is depleted
        equity = self.get_equity()
        if equity <= 0:
            return None

        # Calculate and deduct commission
        commission = self._calculate_commission(lots, execution_price)
        self.total_commission += commission
        self.balance -= commission

        order_id = self._next_order_id()

        order = Order(
            order_id=order_id,
            symbol=self.symbol,
            side=OrderSide.SELL,
            order_type=OrderType.MARKET,
            lots=lots,
            price=execution_price,
            tp_price=normalize_price(tp_price, self.symbol) if tp_price else None,
            sl_price=normalize_price(sl_price, self.symbol) if sl_price else None,
            status=OrderStatus.OPEN,
            open_time=self._current_time,
            magic=magic,
            comment=comment
        )

        self.orders[order_id] = order

        # Create position
        position = Position(
            position_id=order_id,
            symbol=self.symbol,
            side=OrderSide.SELL,
            lots=lots,
            entry_price=execution_price,
            current_price=self._current_ask,
            tp_price=order.tp_price,
            sl_price=order.sl_price,
            open_time=self._current_time,
            magic=magic
        )
        position.update_pnl(self._current_ask)
        self.positions[order_id] = position

        return order

    def close_order(
        self,
        order_id: int,
        close_price: Optional[float] = None
    ) -> Optional[float]:
        """Close an open order."""
        if order_id not in self.orders:
            return None

        order = self.orders[order_id]
        if not order.is_open():
            return None

        # Determine close price
        if close_price is None:
            if order.side == OrderSide.BUY:
                close_price = self._current_bid
            else:
                close_price = self._current_ask

        close_price = normalize_price(close_price, self.symbol)

        # Calculate profit
        if order.side == OrderSide.BUY:
            profit = order.lots * (close_price - order.price)
        else:
            profit = order.lots * (order.price - close_price)

        # Calculate and deduct closing commission
        commission = self._calculate_commission(order.lots, close_price)
        self.total_commission += commission
        profit -= commission

        # Update order
        order.status = OrderStatus.CLOSED
        order.close_time = self._current_time
        order.close_price = close_price
        order.profit = profit

        # Update balance
        self.balance += profit

        # Remove position
        if order_id in self.positions:
            del self.positions[order_id]

        # Add to trade history
        self.trade_history.append(order)

        return profit

    def close_position(
        self,
        position_id: int,
        close_price: Optional[float] = None
    ) -> Optional[float]:
        """Close an open position."""
        return self.close_order(position_id, close_price)

    def get_equity(self) -> float:
        """Get current equity (balance + unrealized P&L)."""
        unrealized = sum(p.unrealized_pnl for p in self.positions.values())
        return self.balance + unrealized

    def get_margin_used(self) -> float:
        """Get margin used by open positions (simplified: position value)."""
        return sum(p.lots * p.current_price for p in self.positions.values())

    def process_tp_sl(self, high_price: float, low_price: float) -> List[tuple]:
        """
        Process TP/SL hits within a price range (candle).

        Args:
            high_price: High price of the period
            low_price: Low price of the period

        Returns:
            List of (order_id, profit, close_type) tuples
        """
        closed = []

        for position_id in list(self.positions.keys()):
            position = self.positions[position_id]

            # Check TP hit
            if position.tp_price is not None:
                tp_hit = False
                if position.side == OrderSide.BUY and high_price >= position.tp_price:
                    tp_hit = True
                elif position.side == OrderSide.SELL and low_price <= position.tp_price:
                    tp_hit = True

                if tp_hit:
                    profit = self.close_position(position_id, position.tp_price)
                    if profit is not None:
                        closed.append((position_id, profit, "TP"))
                        continue

            # Check SL hit
            if position.sl_price is not None:
                sl_hit = False
                if position.side == OrderSide.BUY and low_price <= position.sl_price:
                    sl_hit = True
                elif position.side == OrderSide.SELL and high_price >= position.sl_price:
                    sl_hit = True

                if sl_hit:
                    profit = self.close_position(position_id, position.sl_price)
                    if profit is not None:
                        closed.append((position_id, profit, "SL"))

        return closed

    def get_statistics(self) -> dict:
        """Get trading statistics."""
        if not self.trade_history:
            return {
                "total_trades": 0,
                "winning_trades": 0,
                "losing_trades": 0,
                "win_rate": 0.0,
                "total_profit": 0.0,
                "total_commission": self.total_commission,
                "net_profit": -self.total_commission,
                "largest_win": 0.0,
                "largest_loss": 0.0,
                "average_win": 0.0,
                "average_loss": 0.0
            }

        winners = [t for t in self.trade_history if t.profit > 0]
        losers = [t for t in self.trade_history if t.profit < 0]

        total_profit = sum(t.profit for t in self.trade_history)
        total_wins = sum(t.profit for t in winners) if winners else 0
        total_losses = sum(t.profit for t in losers) if losers else 0

        return {
            "total_trades": len(self.trade_history),
            "winning_trades": len(winners),
            "losing_trades": len(losers),
            "win_rate": len(winners) / len(self.trade_history) * 100 if self.trade_history else 0,
            "total_profit": total_profit,
            "total_commission": self.total_commission,
            "net_profit": total_profit,
            "largest_win": max((t.profit for t in winners), default=0),
            "largest_loss": min((t.profit for t in losers), default=0),
            "average_win": total_wins / len(winners) if winners else 0,
            "average_loss": total_losses / len(losers) if losers else 0
        }
