"""Order execution engine for OHLCV-based backtesting."""
from dataclasses import dataclass
from typing import Optional, Literal

import numpy as np

from xtrading_models import Order, Fill, Execution, CommissionReport, BarData
from xtrading_models.order import StopOrder, StopLimitOrder, TrailingStopMarket


@dataclass
class ExecutionConfig:
    """Configuration for execution engine behavior."""

    # Resolution strategy for ambiguous orders (multiple orders in same bar)
    ambiguity_strategy: Literal["skip", "execute_all", "postpone", "randomize"] = "skip"

    # Slippage model: deterministic (exact fill at limit) or statistical (normal distribution)
    slippage_model: Literal["none", "normal"] = "none"

    # Standard deviation divider for normal distribution slippage
    # Price range / std_divider = std for normal distribution
    # Higher values = less slippage variance
    std_divider: int = 1000

    # Random seed for reproducible statistical slippage
    random_seed: Optional[int] = None

    # Flat commission per fill in USD
    commission_per_fill: float = 0.0


class ExecutionEngine:
    """Executes orders against OHLCV bar data with recursive parent-child support."""

    ADVERSE_SCALE = 1.0    # magnitude when bar direction worsens the fill
    FAVORABLE_SCALE = 0.25  # magnitude when bar direction improves the fill

    def __init__(self, config: Optional[ExecutionConfig] = None):
        self._config = config or ExecutionConfig()
        self._rng = np.random.default_rng(self._config.random_seed)

    def _apply_slippage(self, fill_price: float, bar: BarData, action: str,
                        next_fragment_price: Optional[float] = None) -> float:
        """Apply volatility-based slippage to a fill price.

        Slippage always worsens the fill (BUY pays more, SELL receives less).
        Magnitude is influenced by price direction at the fill point:
        - For trail orders: next_fragment_price determines direction
        - For other orders: bar direction (close vs open) determines direction

        When direction aligns with the adverse side, full magnitude applies.
        When direction opposes, magnitude is scaled down to FAVORABLE_SCALE.
        Result is clamped to [bar.low, bar.high].
        """
        if self._config.slippage_model == "none":
            return fill_price

        bar_range = bar.high - bar.low
        if bar_range == 0:
            return fill_price

        std = bar_range / self._config.std_divider
        magnitude = abs(self._rng.normal(0, std))

        # Determine price direction at fill point
        if next_fragment_price is not None:
            price_moving_up = next_fragment_price > fill_price
        else:
            price_moving_up = bar.close > bar.open

        # BUY adverse = price moving up (pay more); SELL adverse = price moving down
        if action in ("BUY", "BOT"):
            adverse = price_moving_up
            slippage = magnitude * (self.ADVERSE_SCALE if adverse else self.FAVORABLE_SCALE)
            result = fill_price + slippage
        else:  # SELL / SLD
            adverse = not price_moving_up
            slippage = magnitude * (self.ADVERSE_SCALE if adverse else self.FAVORABLE_SCALE)
            result = fill_price - slippage

        return max(bar.low, min(bar.high, result))

    def execute(self, order: Order, bar: BarData, parent_id: int = 0) -> list[Fill]:
        """Recursively execute order and its children.

        Args:
            order: Order to execute (may have children)
            bar: Bar data to execute against
            parent_id: Parent order ID (0 for top-level orders)

        Returns:
            List of fills (empty if order didn't execute)
        """
        fills = self._try_fill_order(order, bar, parent_id) or []

        if fills and order.children:
            modified_bar = self._create_modified_bar(bar, fills[0].execution.price)
            for child in order.children:
                child_fills = self.execute(child, modified_bar, parent_id=order.orderId)
                fills.extend(child_fills)

        return fills

    def _try_fill_order(self, order: Order, bar: BarData, parent_id: int = 0) -> Optional[list[Fill]]:
        """Fill single order based on type.

        Args:
            order: Order to fill
            bar: Bar data to execute against
            parent_id: Parent order ID (0 for parent orders)

        Returns:
            List of fills if order executes, None otherwise
        """
        if order.orderType == 'MKT':
            return self._fill_market(order, bar, parent_id)
        elif order.orderType == 'LMT':
            return self._fill_limit(order, bar, parent_id)
        elif order.orderType == 'STP':
            return self._fill_stop(order, bar, parent_id)  # type: ignore[arg-type]
        elif order.orderType == 'STP LMT':
            return self._fill_stop_limit(order, bar, parent_id)  # type: ignore[arg-type]
        elif order.orderType == 'TRAIL':
            return self._fill_trail(order, bar, parent_id)  # type: ignore[arg-type]
        elif order.orderType == 'TRAIL LIMIT':
            raise NotImplementedError("Trailing Stop Limit execution not implemented")
        return None

    def _create_modified_bar(self, original: BarData, new_open: float) -> BarData:
        """Create modified bar with adjusted open price (aggressive approach).

        Only open is modified - we don't know if bar extremes happened before/after trigger.

        Args:
            original: Original bar data
            new_open: New open price (typically the fill price)

        Returns:
            New BarData with modified open
        """
        return BarData(
            date=original.date,
            open=new_open,
            high=max(new_open, original.high),
            low=min(new_open, original.low),
            close=original.close,
            volume=original.volume
        )

    def _fill_market(self, order: Order, bar: BarData, parent_id: int = 0) -> Optional[list[Fill]]:
        """Fill market order at open price."""
        fill_price = bar.open
        fill_price = self._apply_slippage(fill_price, bar, order.action)

        execution = Execution(
            orderId=order.orderId,
            time=bar.date,
            shares=order.totalQuantity,
            price=fill_price,
            side=order.action,
        )

        commission = CommissionReport(
            commission=self._config.commission_per_fill,
            currency="USD",
        )

        fill = Fill(
                order=order,
                execution=execution,
                commissionReport=commission,
                time=bar.date,
                parentId=parent_id)

        return [fill]

    def _evaluate_limit_price(self, action: str, limit_price: float, bar: BarData) -> Optional[float]:
        """Evaluate if a limit price would fill against a bar.

        Returns fill price (with slippage applied) if limit is reached, None otherwise.
        """
        if action == "BUY":
            if bar.low <= limit_price:
                fill_price = limit_price if bar.open > limit_price else bar.open
                return self._apply_slippage(fill_price, bar, action)
            return None
        else:  # SELL
            if bar.high >= limit_price:
                fill_price = limit_price if bar.open < limit_price else bar.open
                return self._apply_slippage(fill_price, bar, action)
            return None

    def _fill_limit(self, order: Order, bar: BarData, parent_id: int = 0) -> Optional[list[Fill]]:
        """Fill limit order if price is reached."""
        fill_price = self._evaluate_limit_price(order.action, order.price, bar)
        if fill_price is None:
            return None

        execution = Execution(
            orderId=order.orderId,
            time=bar.date,
            shares=order.totalQuantity,
            price=fill_price,
            side=order.action,
        )

        commission = CommissionReport(
            commission=self._config.commission_per_fill,
            currency="USD",
        )

        fill = Fill(
            order=order,
            execution=execution,
            commissionReport=commission,
            time=bar.date,
            parentId=parent_id,
        )

        return [fill]

    def _fill_stop(self, order: StopOrder, bar: BarData, parent_id: int = 0) -> Optional[list[Fill]]:
        """Fill stop order if triggered - fills at trigger point."""
        stop_price = order.price

        if order.action == "BUY":
            # BUY stop: triggers when price goes at/above stop
            if bar.high >= stop_price:
                fill_price = stop_price if bar.open < stop_price else bar.open
            else:
                return None
        else:  # SELL
            # SELL stop: triggers when price goes at/below stop
            if bar.low <= stop_price:
                fill_price = stop_price if bar.open > stop_price else bar.open
            else:
                return None

        fill_price = self._apply_slippage(fill_price, bar, order.action)
        order.triggered = True
        order.triggerPrice = fill_price

        execution = Execution(
            orderId=order.orderId,
            time=bar.date,
            shares=order.totalQuantity,
            price=fill_price,
            side=order.action,
        )

        commission = CommissionReport(
            commission=self._config.commission_per_fill,
            currency="USD",
        )

        fill = Fill(
            order=order,
            execution=execution,
            commissionReport=commission,
            time=bar.date,
            parentId=parent_id,
        )

        return [fill]

    def _fill_stop_limit(self, order: StopLimitOrder, bar: BarData, parent_id: int = 0) -> Optional[list[Fill]]:
        """Fill stop-limit order: stop triggers internally, then evaluates as limit.

        When stop is not yet triggered:
        - Check if stop price is hit
        - If hit: set triggered=True, create modified bar, evaluate limit
        - If limit fills: return 1 fill at limit price
        - If limit doesn't fill: return empty list (order stays pending with triggered=True)

        When already triggered (from a previous bar):
        - Evaluate limit against full bar
        """
        if not order.triggered:
            # Check stop trigger
            stop_price = order.price
            if order.action == "BUY":
                if bar.high < stop_price:
                    return None  # Stop not triggered
                trigger_price = stop_price if bar.open < stop_price else bar.open
            else:  # SELL
                if bar.low > stop_price:
                    return None  # Stop not triggered
                trigger_price = stop_price if bar.open > stop_price else bar.open

            order.triggered = True
            order.triggerPrice = trigger_price
            eval_bar = self._create_modified_bar(bar, trigger_price)
        else:
            eval_bar = bar

        # Evaluate limit price
        fill_price = self._evaluate_limit_price(order.action, order.limitPrice, eval_bar)
        if fill_price is None:
            return []  # Triggered but limit not filled; order stays pending

        execution = Execution(
            orderId=order.orderId,
            time=bar.date,
            shares=order.totalQuantity,
            price=fill_price,
            side=order.action,
        )

        commission = CommissionReport(
            commission=self._config.commission_per_fill,
            currency="USD",
        )

        fill = Fill(
            order=order,
            execution=execution,
            commissionReport=commission,
            time=bar.date,
            parentId=parent_id,
        )

        return [fill]

    def _fill_trail(self, order: TrailingStopMarket, bar: BarData, parent_id: int = 0) -> Optional[list[Fill]]:
        """
        Stateless execution for TrailingStopMarket (BUY only).
        - Updates order.extremePrice and order.currentStopPrice in-place.
        """
        # Note: Bullish bar flow assumption: prev_extremePrice [optional] -> open -> low -> high -> close
        #       Bearish bar flow assumption: prev_extremePrice [optional] -> open -> high -> low -> close

        fragments = []
        if bar.close > bar.open:
            fragments = [bar.open, bar.low, bar.high, bar.close]
        else:
            fragments = [bar.open, bar.high, bar.low, bar.close]

        fill_price = None
        prev_price = None
        trigger_index = None
        # Evaluate each fragment in order for trigger
        for i, price in enumerate(fragments):
            if order.action == 'BUY':
                # Update extreme and stop prices on motion down or initialization
                if order.stopPrice is None or price <= order.extremePrice:  # type: ignore
                    order.extremePrice = price
                    order.stopPrice = order.extremePrice + order.trailingDistance if order.trailingDistance is not None else \
                        order.extremePrice * (1 + order.trailingPercent / 100)  # type: ignore
                # Check for trigger on motion up
                elif price >= order.stopPrice:
                    # Triggered
                    fill_price = order.stopPrice if prev_price is not None and prev_price < order.stopPrice else price
                    trigger_index = i
                    break

            elif order.action == 'SELL':
                # Update extreme and stop prices on motion up or initialization
                if order.stopPrice is None or price >= order.extremePrice:  # type: ignore
                    order.extremePrice = price
                    order.stopPrice = order.extremePrice - order.trailingDistance if order.trailingDistance is not None else \
                        order.extremePrice * (1 - order.trailingPercent / 100)  # type: ignore
                # Check for trigger on motion down
                elif price <= order.stopPrice:
                    # Triggered
                    fill_price = order.stopPrice if prev_price is not None and prev_price > order.stopPrice else price
                    trigger_index = i
                    break

            else:
                raise ValueError(f"Unsupported action for TrailingStopMarket: {order.action}")

            prev_price = price

        if fill_price is not None:
            # Use next fragment for slippage direction; fall back to close
            next_frag = fragments[trigger_index + 1] if trigger_index + 1 < len(fragments) else bar.close
            fill_price = self._apply_slippage(fill_price, bar, order.action, next_frag)

            execution = Execution(
                orderId=order.orderId,
                time=bar.date,
                shares=order.totalQuantity,
                price=fill_price,
                side=order.action,
            )

            commission = CommissionReport(
                commission=self._config.commission_per_fill,
                currency="USD",
            )

            fill = Fill(
                order=order,
                execution=execution,
                commissionReport=commission,
                time=bar.date,
                parentId=parent_id,
            )

            return [fill]

        return None
