from typing import Any

from qubx import logger
from qubx.core.basics import Instrument, MarketType, Order, OrderRequest, OrderSide
from qubx.core.exceptions import OrderNotFound
from qubx.core.interfaces import IAccountProcessor, IBroker, IStrategyContext, ITimeProvider, ITradingManager

from .utils import EXCHANGE_MAPPINGS


class ClientIdStore:
    """Manages generation of unique client order IDs."""

    def __init__(self):
        """Initialize a client ID store."""
        self._order_id: int | None = None

    def generate_id(self, time_provider: ITimeProvider, symbol: str) -> str:
        """Generate a unique client order ID.

        Args:
            time_provider: Time provider to get current timestamp
            symbol: Trading symbol for the order

        Returns:
            A unique client order ID
        """
        # Initialize order ID from timestamp if not yet set
        if self._order_id is None:
            self._order_id = self._initialize_id_from_timestamp(time_provider)

        # Increment order ID to ensure uniqueness across calls
        self._order_id += 1

        # Create and return the unique ID
        return self._create_id(symbol, self._order_id)

    def _initialize_id_from_timestamp(self, time_provider: ITimeProvider) -> int:
        """Initialize the order ID from the current timestamp.

        Args:
            time_provider: Time provider to get current timestamp

        Returns:
            Initial order ID value
        """
        return time_provider.time().astype("int64") // 100_000_000

    def _create_id(self, symbol: str, order_id: int) -> str:
        """Create the ID from symbol and order ID.

        Args:
            symbol: Trading symbol
            order_id: Current order ID counter

        Returns:
            Client ID string
        """
        return "_".join(["qubx", symbol, str(order_id)])


class TradingManager(ITradingManager):
    _context: IStrategyContext
    _brokers: list[IBroker]
    _account: IAccountProcessor
    _strategy_name: str

    _client_id_store: ClientIdStore
    _exchange_to_broker: dict[str, IBroker]

    def __init__(
        self, context: IStrategyContext, brokers: list[IBroker], account: IAccountProcessor, strategy_name: str
    ) -> None:
        self._context = context
        self._brokers = brokers
        self._account = account
        self._strategy_name = strategy_name
        self._client_id_store = ClientIdStore()
        self._exchange_to_broker = {broker.exchange(): broker for broker in brokers}

    def trade(
        self,
        instrument: Instrument,
        amount: float,
        price: float | None = None,
        time_in_force="gtc",
        client_id: str | None = None,
        **options,
    ) -> Order:
        size_adj = self._adjust_size(instrument, amount)
        side = self._get_side(amount)
        type = self._get_order_type(instrument, price, options)
        price = self._adjust_price(instrument, price, amount)
        client_id = client_id or self._generate_order_client_id(instrument.symbol)

        logger.debug(
            f"[<g>{instrument.symbol}</g>] :: Sending (blocking) {type} {side} {size_adj} {' @ ' + str(price) if price else ''} -> (client_id: <r>{client_id})</r> ..."
        )

        order = self._get_broker(instrument.exchange).send_order(
            instrument=instrument,
            order_side=side,
            order_type=type,
            amount=size_adj,
            price=price,
            time_in_force=time_in_force,
            client_id=client_id,
            **options,
        )

        if order is not None:
            self._account.add_active_orders({order.id: order})

        return order

    def trade_async(
        self,
        instrument: Instrument,
        amount: float,
        price: float | None = None,
        time_in_force="gtc",
        client_id: str | None = None,
        **options,
    ) -> None:
        size_adj = self._adjust_size(instrument, amount)
        side = self._get_side(amount)
        type = self._get_order_type(instrument, price, options)
        price = self._adjust_price(instrument, price, amount)
        client_id = client_id or self._generate_order_client_id(instrument.symbol)

        logger.debug(
            f"[<g>{instrument.symbol}</g>] :: Sending (async) {type} {side} {size_adj} {' @ ' + str(price) if price else ''} -> (client_id: <r>{client_id})</r> ..."
        )

        self._get_broker(instrument.exchange).send_order_async(
            instrument=instrument,
            order_side=side,
            order_type=type,
            amount=size_adj,
            price=price,
            time_in_force=time_in_force,
            client_id=client_id,
            **options,
        )

    def submit_orders(self, order_requests: list[OrderRequest]) -> list[Order]:
        raise NotImplementedError("Not implemented yet")

    def set_target_position(
        self, instrument: Instrument, target: float, price: float | None = None, time_in_force="gtc", **options
    ) -> Order:
        raise NotImplementedError("Not implemented yet")

    def set_target_leverage(
        self, instrument: Instrument, leverage: float, price: float | None = None, **options
    ) -> Order:
        raise NotImplementedError("Not implemented yet")

    def close_position(self, instrument: Instrument, without_signals: bool = False) -> None:
        position = self._account.get_position(instrument)

        if position.quantity == 0:
            logger.debug(f"[<g>{instrument.symbol}</g>] :: Position already closed or zero size")
            return

        if without_signals:
            closing_amount = -position.quantity
            logger.debug(
                f"[<g>{instrument.symbol}</g>] :: Closing position {position.quantity} with market order for {closing_amount}"
            )
            self.trade(instrument, closing_amount, reduceOnly=True)
        else:
            logger.debug(
                f"[<g>{instrument.symbol}</g>] :: Closing position {position.quantity} by emitting signal with 0 target"
            )
            signal = instrument.signal(self._context, 0, comment="Close position trade")
            self._context.emit_signal(signal)

    def close_positions(self, market_type: MarketType | None = None, without_signals: bool = False) -> None:
        positions = self._account.get_positions()

        positions_to_close = []
        for instrument, position in positions.items():
            if market_type is None or instrument.market_type == market_type:
                if position.is_open():
                    positions_to_close.append(instrument)

        if not positions_to_close:
            logger.debug(f"No open positions to close{f' for market type {market_type}' if market_type else ''}")
            return

        logger.debug(
            f"Closing {len(positions_to_close)} positions{f' for market type {market_type}' if market_type else ''}"
        )

        for instrument in positions_to_close:
            self.close_position(instrument, without_signals)

    def cancel_order(self, order_id: str, exchange: str | None = None) -> bool:
        """Cancel a specific order synchronously."""
        if not order_id:
            return False
        if exchange is None:
            exchange = self._brokers[0].exchange()
        try:
            success = self._get_broker(exchange).cancel_order(order_id)
            if success:
                self._account.remove_order(order_id, exchange)
            return success
        except OrderNotFound:
            # Order was already cancelled or doesn't exist
            # Still try to remove it from account to keep state consistent
            self._account.remove_order(order_id, exchange)
            return False  # Return False since order wasn't found
        except Exception as e:
            logger.error(f"Error canceling order {order_id}: {e}")
            return False  # Return False for any other errors

    def cancel_order_async(self, order_id: str, exchange: str | None = None) -> None:
        """Cancel a specific order asynchronously (non blocking)."""
        if not order_id:
            return
        if exchange is None:
            exchange = self._brokers[0].exchange()
        try:
            self._get_broker(exchange).cancel_order_async(order_id)
            # Note: For async, we remove the order optimistically
            # The actual removal will be confirmed via order status updates
            self._account.remove_order(order_id, exchange)
        except OrderNotFound:
            # Order was already cancelled or doesn't exist
            # Still try to remove it from account to keep state consistent
            self._account.remove_order(order_id, exchange)

    def cancel_orders(self, instrument: Instrument) -> None:
        for o in self._account.get_orders(instrument).values():
            self.cancel_order(o.id, instrument.exchange)

    def update_order(self, order_id: str, price: float, amount: float, exchange: str | None = None) -> Order:
        """Update an existing limit order with new price and amount."""
        if not order_id:
            raise ValueError("Order ID is required")
        if exchange is None:
            exchange = self._brokers[0].exchange()

        # Get the existing order to determine instrument for adjustments
        active_orders = self._account.get_orders(exchange=exchange)
        existing_order = active_orders.get(order_id)
        if not existing_order:
            # Let broker handle the OrderNotFound - just pass through
            logger.debug(f"Updating order {order_id}: {amount} @ {price} on {exchange}")
        else:
            # Apply TradingManager-level adjustments before sending to broker
            instrument = existing_order.instrument
            adjusted_amount = self._adjust_size(instrument, amount)
            adjusted_price = self._adjust_price(instrument, price, amount)
            if adjusted_price is None:
                raise ValueError(f"Price adjustment failed for {instrument.symbol}")
            # Update the values to use adjusted ones
            amount = adjusted_amount
            price = adjusted_price

        try:
            updated_order = self._get_broker(exchange).update_order(order_id, price, abs(amount))

            if updated_order is not None:
                # Update account tracking with new order info
                self._account.process_order(updated_order)
                logger.info(f"[<g>{updated_order.instrument.symbol}</g>] :: Successfully updated order {order_id}")

            return updated_order
        except Exception as e:
            logger.error(f"Error updating order {order_id}: {e}")
            raise e

    def _generate_order_client_id(self, symbol: str) -> str:
        return self._client_id_store.generate_id(self._context, symbol)

    def exchanges(self) -> list[str]:
        return list(self._exchange_to_broker.keys())

    def _adjust_size(self, instrument: Instrument, amount: float) -> float:
        size_adj = instrument.round_size_down(abs(amount))
        if size_adj < instrument.min_size:
            raise ValueError(
                f"[{instrument.symbol}] Attempt to trade size {abs(amount)} less than minimal allowed {instrument.min_size} !"
            )
        return size_adj

    def _adjust_price(self, instrument: Instrument, price: float | None, amount: float) -> float | None:
        if price is None:
            return price
        return instrument.round_price_down(price) if amount > 0 else instrument.round_price_up(price)

    def _get_side(self, amount: float) -> OrderSide:
        return "BUY" if amount > 0 else "SELL"

    def _get_order_type(self, instrument: Instrument, price: float | None, options: dict[str, Any]) -> str:
        if price is None:
            return "market"
        if (stp_type := options.get("stop_type")) is not None:
            return f"stop_{stp_type}"
        return "limit"

    def _get_broker(self, exchange: str) -> IBroker:
        if exchange in self._exchange_to_broker:
            return self._exchange_to_broker[exchange]
        if exchange in EXCHANGE_MAPPINGS and EXCHANGE_MAPPINGS[exchange] in self._exchange_to_broker:
            return self._exchange_to_broker[EXCHANGE_MAPPINGS[exchange]]
        raise ValueError(f"Broker for exchange {exchange} not found")
