"""
Order management for the Tradier API.

This module provides classes and functions for placing, modifying, and
cancelling orders through the Tradier Brokerage API.
"""

from __future__ import annotations

from datetime import datetime
from decimal import Decimal
from enum import Enum
from typing import TYPE_CHECKING, Any

from loguru import logger
from pydantic import Field

from optrabot.broker.tradier.utils import TradierData

if TYPE_CHECKING:
    from optrabot.broker.tradier.session import Session


# ========== Enums ==========


class OrderClass(str, Enum):
    """Order classification."""
    EQUITY = 'equity'
    OPTION = 'option'
    MULTILEG = 'multileg'
    COMBO = 'combo'


class OrderSide(str, Enum):
    """Order side for simple orders."""
    BUY = 'buy'
    SELL = 'sell'
    BUY_TO_OPEN = 'buy_to_open'
    BUY_TO_CLOSE = 'buy_to_close'
    SELL_TO_OPEN = 'sell_to_open'
    SELL_TO_CLOSE = 'sell_to_close'


class OrderType(str, Enum):
    """Type of order."""
    MARKET = 'market'
    LIMIT = 'limit'
    STOP = 'stop'
    STOP_LIMIT = 'stop_limit'
    DEBIT = 'debit'       # For multileg - net debit
    CREDIT = 'credit'     # For multileg - net credit
    EVEN = 'even'         # For multileg - net zero


class OrderDuration(str, Enum):
    """Time in force for the order."""
    DAY = 'day'
    GTC = 'gtc'           # Good til cancelled
    PRE = 'pre'           # Pre-market
    POST = 'post'         # Post-market


class OrderStatus(str, Enum):
    """Status of an order."""
    OPEN = 'open'
    PENDING = 'pending'
    PARTIALLY_FILLED = 'partially_filled'
    FILLED = 'filled'
    EXPIRED = 'expired'
    CANCELED = 'canceled'
    REJECTED = 'rejected'
    ERROR = 'error'
    HELD = 'held'
    CALCULATED = 'calculated'
    ACCEPTED_FOR_BIDDING = 'accepted_for_bidding'


# ========== Order Leg Model ==========


class OrderLeg(TradierData):
    """
    Represents a leg in a multileg order.
    
    Attributes:
        id: Unique identifier for the leg
        type: Type of instrument (option, equity)
        symbol: OCC symbol for options or ticker for equity
        side: Order side (buy_to_open, sell_to_close, etc.)
        quantity: Number of contracts/shares
        status: Status of this leg
        exec_quantity: Executed quantity
        last_fill_price: Price of last fill
        last_fill_quantity: Quantity of last fill
    """
    id: int | None = None
    type: str | None = None
    symbol: str | None = None
    side: OrderSide | None = None
    quantity: Decimal | None = None
    status: OrderStatus | None = None
    exec_quantity: Decimal | None = None
    last_fill_price: Decimal | None = None
    last_fill_quantity: Decimal | None = None
    option_symbol: str | None = None
    avg_fill_price: Decimal | None = None


# ========== Order Response Models ==========


class PlacedOrder(TradierData):
    """
    Represents an order that has been placed or retrieved from Tradier.
    
    This model is used for both the response from placing an order and
    for retrieving existing orders.
    
    Attributes:
        id: Unique order identifier
        type: Order type (market, limit, etc.)
        symbol: Underlying symbol
        side: Order side
        quantity: Order quantity
        status: Current order status
        duration: Time in force
        price: Limit price (for limit orders)
        stop_price: Stop price (for stop orders)
        avg_fill_price: Average fill price
        exec_quantity: Executed quantity
        last_fill_price: Last fill price
        last_fill_quantity: Last fill quantity
        remaining_quantity: Remaining unfilled quantity
        create_date: When the order was created
        transaction_date: When the order was last updated
        class_: Order class (option, multileg, etc.)
        option_symbol: OCC option symbol
        leg: List of legs for multileg orders
    """
    id: int
    type: OrderType | None = None
    symbol: str | None = None
    side: OrderSide | None = None
    quantity: Decimal | None = None
    status: OrderStatus | None = None
    duration: OrderDuration | None = None
    price: Decimal | None = None
    stop_price: Decimal | None = None
    avg_fill_price: Decimal | None = None
    exec_quantity: Decimal | None = None
    last_fill_price: Decimal | None = None
    last_fill_quantity: Decimal | None = None
    remaining_quantity: Decimal | None = None
    create_date: datetime | None = None
    transaction_date: datetime | None = None
    class_: OrderClass | None = Field(default=None, alias='class')
    option_symbol: str | None = None
    num_legs: int | None = None
    leg: list[OrderLeg] | None = None
    strategy: str | None = None
    tag: str | None = None
    reason_description: str | None = None

    def __repr__(self) -> str:
        return (
            f'PlacedOrder(id={self.id}, status={self.status}, '
            f'symbol={self.symbol}, type={self.type})'
        )

    @property
    def is_filled(self) -> bool:
        """Check if the order is fully filled."""
        return self.status == OrderStatus.FILLED

    @property
    def is_open(self) -> bool:
        """Check if the order is still open/active."""
        return self.status in (OrderStatus.OPEN, OrderStatus.PENDING, OrderStatus.PARTIALLY_FILLED)

    @property
    def is_canceled(self) -> bool:
        """Check if the order was canceled."""
        return self.status == OrderStatus.CANCELED


class OrderResponse(TradierData):
    """
    Response from placing an order.
    
    Contains the order details and any partner_id assigned by Tradier.
    """
    id: int
    status: str | None = None
    partner_id: str | None = None

    def __repr__(self) -> str:
        return f'OrderResponse(id={self.id}, status={self.status})'


# ========== Order Request Models ==========


class NewOrderLeg:
    """
    Represents a leg for a new multileg order request.
    
    This is a simple data class for building order requests,
    not a Pydantic model.
    """
    def __init__(
        self,
        option_symbol: str,
        side: OrderSide,
        quantity: int,
    ) -> None:
        self.option_symbol = option_symbol
        self.side = side
        self.quantity = quantity

    def to_params(self, index: int) -> dict[str, Any]:
        """Convert to request parameters with indexed keys."""
        return {
            f'option_symbol[{index}]': self.option_symbol,
            f'side[{index}]': self.side.value,
            f'quantity[{index}]': str(self.quantity),
        }


# ========== Order Functions ==========


def get_orders(
    session: Session,
    account_number: str,
    include_tags: bool = False,
) -> list[PlacedOrder]:
    """
    Get all orders for an account.
    
    :param session: The authenticated session.
    :param account_number: The account number.
    :param include_tags: Whether to include order tags.
    :returns: List of PlacedOrder objects.
    """
    params = {}
    if include_tags:
        params['includeTags'] = 'true'
    
    data = session._get(f'/accounts/{account_number}/orders', params=params)
    return _parse_orders_response(data)


async def a_get_orders(
    session: Session,
    account_number: str,
    include_tags: bool = False,
) -> list[PlacedOrder]:
    """
    Get all orders for an account (async).
    
    :param session: The authenticated session.
    :param account_number: The account number.
    :param include_tags: Whether to include order tags.
    :returns: List of PlacedOrder objects.
    """
    params = {}
    if include_tags:
        params['includeTags'] = 'true'
    
    data = await session._a_get(f'/accounts/{account_number}/orders', params=params)
    return _parse_orders_response(data)


def get_order(
    session: Session,
    account_number: str,
    order_id: int,
) -> PlacedOrder:
    """
    Get a specific order by ID.
    
    :param session: The authenticated session.
    :param account_number: The account number.
    :param order_id: The order ID to retrieve.
    :returns: PlacedOrder object.
    """
    data = session._get(f'/accounts/{account_number}/orders/{order_id}')
    order_data = data.get('order', {})
    return _parse_single_order(order_data)


async def a_get_order(
    session: Session,
    account_number: str,
    order_id: int,
) -> PlacedOrder:
    """
    Get a specific order by ID (async).
    
    :param session: The authenticated session.
    :param account_number: The account number.
    :param order_id: The order ID to retrieve.
    :returns: PlacedOrder object.
    """
    data = await session._a_get(f'/accounts/{account_number}/orders/{order_id}')
    order_data = data.get('order', {})
    return _parse_single_order(order_data)


def place_option_order(
    session: Session,
    account_number: str,
    symbol: str,
    option_symbol: str,
    side: OrderSide,
    quantity: int,
    order_type: OrderType,
    duration: OrderDuration = OrderDuration.DAY,
    price: Decimal | None = None,
    stop: Decimal | None = None,
    tag: str | None = None,
    preview: bool = False,
) -> OrderResponse:
    """
    Place a single-leg option order.
    
    :param session: The authenticated session.
    :param account_number: The account number.
    :param symbol: The underlying symbol (e.g., "SPX").
    :param option_symbol: The OCC option symbol.
    :param side: Order side (buy_to_open, sell_to_close, etc.).
    :param quantity: Number of contracts.
    :param order_type: Order type (market, limit, stop, stop_limit).
    :param duration: Time in force (day, gtc).
    :param price: Limit price for limit/stop_limit orders.
    :param stop: Stop price for stop/stop_limit orders.
    :param tag: Optional order tag (max 255 chars).
    :param preview: If True, preview the order without placing.
    :returns: OrderResponse with order ID.
    
    Example::
    
        response = place_option_order(
            session,
            account_number="VA12345678",
            symbol="SPX",
            option_symbol="SPXW250117C05000000",
            side=OrderSide.BUY_TO_OPEN,
            quantity=1,
            order_type=OrderType.LIMIT,
            price=Decimal("1.50"),
        )
        print(f"Order placed: {response.id}")
    """
    data = {
        'class': OrderClass.OPTION.value,
        'symbol': symbol,
        'option_symbol': option_symbol,
        'side': side.value,
        'quantity': str(quantity),
        'type': order_type.value,
        'duration': duration.value,
    }
    
    if price is not None:
        data['price'] = str(price)
    if stop is not None:
        data['stop'] = str(stop)
    if tag:
        data['tag'] = tag[:255]
    if preview:
        data['preview'] = 'true'
    
    response = session._post(f'/accounts/{account_number}/orders', data=data)
    return _parse_order_response(response, preview)


async def a_place_option_order(
    session: Session,
    account_number: str,
    symbol: str,
    option_symbol: str,
    side: OrderSide,
    quantity: int,
    order_type: OrderType,
    duration: OrderDuration = OrderDuration.DAY,
    price: Decimal | None = None,
    stop: Decimal | None = None,
    tag: str | None = None,
    preview: bool = False,
) -> OrderResponse:
    """
    Place a single-leg option order (async).
    
    See place_option_order for parameter documentation.
    """
    data = {
        'class': OrderClass.OPTION.value,
        'symbol': symbol,
        'option_symbol': option_symbol,
        'side': side.value,
        'quantity': str(quantity),
        'type': order_type.value,
        'duration': duration.value,
    }
    
    if price is not None:
        data['price'] = str(price)
    if stop is not None:
        data['stop'] = str(stop)
    if tag:
        data['tag'] = tag[:255]
    if preview:
        data['preview'] = 'true'
    
    response = await session._a_post(f'/accounts/{account_number}/orders', data=data)
    return _parse_order_response(response, preview)


def place_multileg_order(
    session: Session,
    account_number: str,
    symbol: str,
    legs: list[NewOrderLeg],
    order_type: OrderType,
    duration: OrderDuration = OrderDuration.DAY,
    price: Decimal | None = None,
    tag: str | None = None,
    preview: bool = False,
) -> OrderResponse:
    """
    Place a multileg (spread) option order.
    
    :param session: The authenticated session.
    :param account_number: The account number.
    :param symbol: The underlying symbol (e.g., "SPX").
    :param legs: List of NewOrderLeg objects defining each leg.
    :param order_type: Order type (debit, credit, even for spreads).
    :param duration: Time in force (day, gtc).
    :param price: Net price for the spread (positive for credit, negative for debit).
    :param tag: Optional order tag (max 255 chars).
    :param preview: If True, preview the order without placing.
    :returns: OrderResponse with order ID.
    
    Example::
    
        legs = [
            NewOrderLeg("SPXW250117P04950000", OrderSide.SELL_TO_OPEN, 1),
            NewOrderLeg("SPXW250117P04900000", OrderSide.BUY_TO_OPEN, 1),
        ]
        response = place_multileg_order(
            session,
            account_number="VA12345678",
            symbol="SPX",
            legs=legs,
            order_type=OrderType.CREDIT,
            price=Decimal("1.50"),
        )
    """
    data = {
        'class': OrderClass.MULTILEG.value,
        'symbol': symbol,
        'type': order_type.value,
        'duration': duration.value,
    }
    
    if price is not None:
        data['price'] = str(abs(price))
    if tag:
        data['tag'] = tag[:255]
    if preview:
        data['preview'] = 'true'
    
    # Add leg parameters
    for i, leg in enumerate(legs):
        data.update(leg.to_params(i))
    
    response = session._post(f'/accounts/{account_number}/orders', data=data)
    return _parse_order_response(response, preview)


async def a_place_multileg_order(
    session: Session,
    account_number: str,
    symbol: str,
    legs: list[NewOrderLeg],
    order_type: OrderType,
    duration: OrderDuration = OrderDuration.DAY,
    price: Decimal | None = None,
    tag: str | None = None,
    preview: bool = False,
) -> OrderResponse:
    """
    Place a multileg (spread) option order (async).
    
    See place_multileg_order for parameter documentation.
    """
    data = {
        'class': OrderClass.MULTILEG.value,
        'symbol': symbol,
        'type': order_type.value,
        'duration': duration.value,
    }
    
    if price is not None:
        data['price'] = str(abs(price))
    if tag:
        data['tag'] = tag[:255]
    if preview:
        data['preview'] = 'true'
    
    # Add leg parameters
    for i, leg in enumerate(legs):
        data.update(leg.to_params(i))
    
    response = await session._a_post(f'/accounts/{account_number}/orders', data=data)
    return _parse_order_response(response, preview)


def modify_order(
    session: Session,
    account_number: str,
    order_id: int,
    order_type: OrderType | None = None,
    duration: OrderDuration | None = None,
    price: Decimal | None = None,
    stop: Decimal | None = None,
) -> OrderResponse:
    """
    Modify an existing order.
    
    :param session: The authenticated session.
    :param account_number: The account number.
    :param order_id: The order ID to modify.
    :param order_type: New order type (optional).
    :param duration: New time in force (optional).
    :param price: New limit price (optional).
    :param stop: New stop price (optional).
    :returns: OrderResponse with the modified order ID.
    
    Note: You can only modify certain fields. The order must be open.
    """
    data = {}
    if order_type:
        data['type'] = order_type.value
    if duration:
        data['duration'] = duration.value
    if price is not None:
        data['price'] = str(price)
    if stop is not None:
        data['stop'] = str(stop)
    
    if not data:
        raise ValueError('At least one modification parameter must be provided')
    
    response = session._put(f'/accounts/{account_number}/orders/{order_id}', data=data)
    return _parse_order_response(response, preview=False)


async def a_modify_order(
    session: Session,
    account_number: str,
    order_id: int,
    order_type: OrderType | None = None,
    duration: OrderDuration | None = None,
    price: Decimal | None = None,
    stop: Decimal | None = None,
) -> OrderResponse:
    """
    Modify an existing order (async).
    
    See modify_order for parameter documentation.
    """
    data = {}
    if order_type:
        data['type'] = order_type.value
    if duration:
        data['duration'] = duration.value
    if price is not None:
        data['price'] = str(price)
    if stop is not None:
        data['stop'] = str(stop)
    
    if not data:
        raise ValueError('At least one modification parameter must be provided')
    
    response = await session._a_put(f'/accounts/{account_number}/orders/{order_id}', data=data)
    return _parse_order_response(response, preview=False)


def cancel_order(
    session: Session,
    account_number: str,
    order_id: int,
) -> None:
    """
    Cancel an open order.
    
    :param session: The authenticated session.
    :param account_number: The account number.
    :param order_id: The order ID to cancel.
    """
    session._delete(f'/accounts/{account_number}/orders/{order_id}')
    logger.debug(f'Cancelled order {order_id}')


async def a_cancel_order(
    session: Session,
    account_number: str,
    order_id: int,
) -> None:
    """
    Cancel an open order (async).
    
    :param session: The authenticated session.
    :param account_number: The account number.
    :param order_id: The order ID to cancel.
    """
    await session._a_delete(f'/accounts/{account_number}/orders/{order_id}')
    logger.debug(f'Cancelled order {order_id}')


# ========== Parsing Helpers ==========


def _parse_orders_response(data: dict) -> list[PlacedOrder]:
    """Parse the orders list response."""
    orders_data = data.get('orders', {})
    
    if not orders_data or orders_data == 'null':
        return []
    
    order_list = orders_data.get('order', [])
    
    # Handle single order (not wrapped in list)
    if isinstance(order_list, dict):
        order_list = [order_list]
    
    orders = []
    for order_data in order_list:
        try:
            order = _parse_single_order(order_data)
            orders.append(order)
        except Exception as e:
            logger.warning(f'Failed to parse order: {e}')
    
    logger.trace(f'Retrieved {len(orders)} orders')
    return orders


def _parse_single_order(order_data: dict) -> PlacedOrder:
    """Parse a single order from the API response."""
    # Parse legs if present
    legs = None
    if 'leg' in order_data:
        leg_data = order_data['leg']
        if isinstance(leg_data, dict):
            leg_data = [leg_data]
        legs = [OrderLeg(**leg) for leg in leg_data]
        order_data = order_data.copy()
        order_data['leg'] = legs
    
    return PlacedOrder(**order_data)


def _parse_order_response(data: dict, preview: bool) -> OrderResponse:
    """Parse the order placement response."""
    order_data = data.get('order', {})
    
    if preview:
        # Preview response has different structure
        logger.debug(f'Order preview: {order_data}')
        return OrderResponse(
            id=0,
            status='preview',
        )
    
    return OrderResponse(
        id=order_data.get('id', 0),
        status=order_data.get('status'),
        partner_id=order_data.get('partner_id'),
    )
