"""
Manual Trading Broker Manager for BrighterTrading.

Manages broker instances (PaperBroker/LiveBroker) for manual (non-strategy) trading.
This provides a unified interface for placing orders, tracking positions, and
managing order lifecycle for manual trades.
"""

import logging
from typing import Any, Callable, Dict, List, Optional

from brokers.base_broker import OrderSide, OrderType, OrderStatus, Position, OrderResult
from brokers.paper_broker import PaperBroker
from brokers.live_broker import LiveBroker

logger = logging.getLogger(__name__)


class ManualTradingBrokerManager:
    """
    Manages broker instances for manual (non-strategy) trading.

    Key design principles:
    - Reuses mode-aware exchange connections from ExchangeInterface
    - Separate orders (unfilled) from positions (filled/aggregated)
    - Single owner for broker polling (strategy_execution_loop only)
    """

    def __init__(self, data_cache: Any = None, exchange_interface: Any = None, users: Any = None):
        """
        Initialize the ManualTradingBrokerManager.

        :param data_cache: DataCache instance for persistence.
        :param exchange_interface: ExchangeInterface instance for exchange connections.
        :param users: Users instance for API key lookup.
        """
        self._paper_brokers: Dict[int, PaperBroker] = {}  # user_id -> broker
        self._live_brokers: Dict[int, Dict[str, LiveBroker]] = {}  # user_id -> {exchange_mode_key: broker}
        self.data_cache = data_cache
        self.exchange_interface = exchange_interface
        self.users = users

    def get_paper_broker(self, user_id: int, initial_balance: float = 10000.0) -> PaperBroker:
        """
        Get or create a PaperBroker for a user.

        :param user_id: The user ID.
        :param initial_balance: Initial balance for new brokers.
        :return: PaperBroker instance.
        """
        if user_id not in self._paper_brokers:
            # Create price provider that uses exchange_interface
            # Accepts either 'symbol' or 'exchange:symbol' format
            def price_provider(symbol_key: str) -> float:
                if self.exchange_interface:
                    try:
                        # Parse exchange:symbol format if present
                        if ':' in symbol_key:
                            exchange, symbol = symbol_key.split(':', 1)
                            return self.exchange_interface.get_price(symbol, exchange)
                        else:
                            return self.exchange_interface.get_price(symbol_key)
                    except Exception as e:
                        logger.warning(f"Failed to get price for {symbol_key}: {e}")
                return 0.0

            broker = PaperBroker(
                price_provider=price_provider,
                data_cache=self.data_cache,
                initial_balance=initial_balance
            )

            # Try to load saved state
            state_id = f"manual_paper_{user_id}"
            broker.load_state(state_id)

            self._paper_brokers[user_id] = broker
            logger.info(f"Created paper broker for user {user_id}")

        return self._paper_brokers[user_id]

    def get_live_broker(
        self,
        user_id: int,
        exchange_name: str,
        testnet: bool,
        user_name: str
    ) -> Optional[LiveBroker]:
        """
        Get or create a LiveBroker for a user+exchange+mode.

        Uses mode-aware exchange connection from exchange_interface.
        Validates that the exchange's sandbox mode matches the requested testnet flag.

        Policy: One mode per exchange per user. Cannot have both testnet and production
        simultaneously for the same exchange. Returns None if mode conflict.

        :param user_id: The user ID.
        :param exchange_name: Exchange name (e.g., 'binance').
        :param testnet: Whether to use testnet mode.
        :param user_name: Username for exchange lookup.
        :return: LiveBroker instance or None if not configured or mode conflict.
        """
        # Use 'testnet'/'production' to match what's stored in trade.broker_mode
        requested_mode = 'testnet' if testnet else 'production'
        broker_key = f"{exchange_name}_{requested_mode}"
        opposite_mode = 'production' if testnet else 'testnet'
        opposite_key = f"{exchange_name}_{opposite_mode}"

        if user_id not in self._live_brokers:
            self._live_brokers[user_id] = {}

        # Check for mode conflict - reject if opposite mode already active
        if opposite_key in self._live_brokers[user_id]:
            logger.error(
                f"Mode conflict: {exchange_name} already active in {opposite_mode} mode "
                f"for user {user_id}. Disconnect first before switching to {requested_mode}."
            )
            return None

        if broker_key not in self._live_brokers[user_id]:
            if not self.exchange_interface:
                logger.error("No exchange interface configured")
                return None

            try:
                # Get exchange connection from exchange_interface
                try:
                    exchange = self.exchange_interface.get_exchange(
                        ename=exchange_name,
                        uname=user_name
                    )
                except ValueError:
                    exchange = None  # Exchange doesn't exist yet

                # CRITICAL: Verify exchange testnet mode matches requested mode
                if exchange:
                    exchange_is_testnet = bool(getattr(exchange, 'testnet', False))
                    if exchange_is_testnet != testnet:
                        # Exchange mode mismatch - need to reconnect with correct mode
                        logger.warning(
                            f"Exchange '{exchange_name}' is in "
                            f"{'testnet' if exchange_is_testnet else 'production'} mode, "
                            f"but requested {'testnet' if testnet else 'production'}. "
                            f"Reconnecting with correct mode."
                        )
                        # Get API keys and reconnect with correct mode
                        if self.users:
                            api_keys = self.users.get_api_keys(user_name, exchange_name)
                            if api_keys:
                                self.exchange_interface.connect_exchange(
                                    exchange_name=exchange_name,
                                    user_name=user_name,
                                    api_keys=api_keys,
                                    testnet=testnet
                                )
                                exchange = self.exchange_interface.get_exchange(
                                    ename=exchange_name, uname=user_name
                                )

                # If exchange doesn't exist or isn't configured, try to load API keys
                if not exchange or not exchange.configured:
                    if self.users:
                        logger.info(f"Exchange '{exchange_name}' not configured, loading API keys...")
                        api_keys = self.users.get_api_keys(user_name, exchange_name)
                        if api_keys:
                            success = self.exchange_interface.connect_exchange(
                                exchange_name=exchange_name,
                                user_name=user_name,
                                api_keys=api_keys,
                                testnet=testnet
                            )
                            if success:
                                exchange = self.exchange_interface.get_exchange(
                                    ename=exchange_name, uname=user_name
                                )

                if not exchange or not exchange.configured:
                    logger.error(f"Exchange {exchange_name} not configured for user {user_name}")
                    return None

                # Final verification: exchange mode MUST match requested mode
                exchange_is_testnet = bool(getattr(exchange, 'testnet', False))
                if exchange_is_testnet != testnet:
                    logger.error(
                        f"Exchange mode mismatch after reconnect: exchange is "
                        f"{'testnet' if exchange_is_testnet else 'production'}, "
                        f"but requested {'testnet' if testnet else 'production'}"
                    )
                    return None

                broker = LiveBroker(
                    exchange=exchange,
                    testnet=testnet,
                    data_cache=self.data_cache
                )
                broker.connect()

                # Try to load saved state
                state_id = f"manual_live_{user_id}_{broker_key}"
                broker.load_state(state_id)

                self._live_brokers[user_id][broker_key] = broker
                logger.info(f"Created live broker for user {user_id}, {broker_key}")

            except Exception as e:
                logger.error(f"Failed to create live broker: {e}", exc_info=True)
                return None

        return self._live_brokers[user_id].get(broker_key)

    def set_prices(self, price_updates: Dict[str, float]) -> None:
        """
        Update current prices for all paper brokers.

        :param price_updates: Dict mapping symbol to price.
        """
        for broker in self._paper_brokers.values():
            for symbol, price in price_updates.items():
                # Handle exchange:symbol format
                if ':' in symbol:
                    _, sym = symbol.split(':', 1)
                else:
                    sym = symbol
                broker.update_price(sym, price)

    def update_all_brokers(self, price_updates: Dict[str, float]) -> List[Dict]:
        """
        Update all brokers and return fill events.

        Called from strategy_execution_loop for single-owner polling.

        :param price_updates: Dict mapping symbol to price.
        :return: List of fill events with user_id and broker_kind added.
        """
        events = []

        # Update prices for paper brokers
        self.set_prices(price_updates)

        # Process paper brokers
        for user_id, broker in self._paper_brokers.items():
            try:
                fill_events = broker.update()
                for e in fill_events:
                    e['user_id'] = user_id
                    e['broker_kind'] = 'paper'
                    e['broker_key'] = 'paper'
                events.extend(fill_events)

                # Save state after updates
                state_id = f"manual_paper_{user_id}"
                broker.save_state(state_id)
            except Exception as ex:
                logger.error(f"Error updating paper broker for user {user_id}: {ex}")

        # Process live brokers
        for user_id, brokers in self._live_brokers.items():
            for broker_key, broker in brokers.items():
                try:
                    fill_events = broker.update()
                    for e in fill_events:
                        e['user_id'] = user_id
                        e['broker_kind'] = 'live'
                        e['broker_key'] = broker_key
                    events.extend(fill_events)

                    # Save state after updates
                    state_id = f"manual_live_{user_id}_{broker_key}"
                    broker.save_state(state_id)
                except Exception as ex:
                    logger.error(f"Error updating live broker {broker_key} for user {user_id}: {ex}")

        return events

    def get_all_open_orders(self, user_id: int) -> List[Dict]:
        """
        Get all open orders for a user across all brokers.

        :param user_id: The user ID.
        :return: List of open order dicts with broker_key added.
        """
        orders = []

        # Paper broker orders
        if user_id in self._paper_brokers:
            broker = self._paper_brokers[user_id]
            for order in broker.get_open_orders():
                order['broker_key'] = 'paper'
                order['broker_kind'] = 'paper'
                orders.append(order)

        # Live broker orders
        if user_id in self._live_brokers:
            for broker_key, broker in self._live_brokers[user_id].items():
                try:
                    for order in broker.get_open_orders():
                        order['broker_key'] = broker_key
                        order['broker_kind'] = 'live'
                        orders.append(order)
                except Exception as e:
                    logger.warning(f"Could not get orders from {broker_key}: {e}")

        return orders

    def get_all_positions(self, user_id: int) -> List[Dict]:
        """
        Get all positions for a user across all brokers.

        :param user_id: The user ID.
        :return: List of position dicts with broker_key added.
        """
        positions = []

        # Paper broker positions
        if user_id in self._paper_brokers:
            broker = self._paper_brokers[user_id]
            for pos in broker.get_all_positions():
                pos_dict = pos.to_dict()
                pos_dict['broker_key'] = 'paper'
                pos_dict['broker_kind'] = 'paper'
                positions.append(pos_dict)

        # Live broker positions
        if user_id in self._live_brokers:
            for broker_key, broker in self._live_brokers[user_id].items():
                try:
                    for pos in broker.get_all_positions():
                        pos_dict = pos.to_dict()
                        pos_dict['broker_key'] = broker_key
                        pos_dict['broker_kind'] = 'live'
                        positions.append(pos_dict)
                except Exception as e:
                    logger.warning(f"Could not get positions from {broker_key}: {e}")

        return positions

    def cancel_order(self, user_id: int, order_id: str, broker_key: str) -> Dict:
        """
        Cancel an open order.

        :param user_id: The user ID.
        :param order_id: The order ID to cancel.
        :param broker_key: The broker key ('paper' or 'exchange_mode').
        :return: Dict with success status and message.
        """
        broker = self._get_broker(user_id, broker_key)
        if not broker:
            return {"success": False, "message": f"Broker not found: {broker_key}"}

        try:
            result = broker.cancel_order(order_id)
            if result:
                # Save state after cancel
                if broker_key == 'paper':
                    state_id = f"manual_paper_{user_id}"
                else:
                    state_id = f"manual_live_{user_id}_{broker_key}"
                broker.save_state(state_id)

            return {
                "success": result,
                "message": "Order cancelled" if result else "Failed to cancel order"
            }
        except Exception as e:
            logger.error(f"Error cancelling order {order_id}: {e}")
            return {"success": False, "message": str(e)}

    def close_position(self, user_id: int, symbol: str, broker_key: str) -> Dict:
        """
        Close an entire position (position-first operation).

        :param user_id: The user ID.
        :param symbol: Trading symbol to close.
        :param broker_key: The broker key ('paper' or 'exchange_mode').
        :return: Dict with success status and OrderResult details.
        """
        broker = self._get_broker(user_id, broker_key)
        if not broker:
            return {"success": False, "message": f"Broker not found: {broker_key}"}

        try:
            result = broker.close_position(symbol)

            if result.success:
                # Save state after close
                if broker_key == 'paper':
                    state_id = f"manual_paper_{user_id}"
                else:
                    state_id = f"manual_live_{user_id}_{broker_key}"
                broker.save_state(state_id)

            return {
                "success": result.success,
                "message": result.message,
                "order_id": result.order_id,
                "filled_qty": result.filled_qty,
                "filled_price": result.filled_price
            }
        except Exception as e:
            logger.error(f"Error closing position {symbol}: {e}")
            return {"success": False, "message": str(e)}

    def get_position(self, user_id: int, symbol: str, broker_key: str) -> Optional[Position]:
        """
        Get a specific position.

        :param user_id: The user ID.
        :param symbol: Trading symbol.
        :param broker_key: The broker key.
        :return: Position or None.
        """
        broker = self._get_broker(user_id, broker_key)
        if not broker:
            return None
        return broker.get_position(symbol)

    def place_order(
        self,
        user_id: int,
        broker_key: str,
        symbol: str,
        side: OrderSide,
        order_type: OrderType,
        size: float,
        price: Optional[float] = None
    ) -> OrderResult:
        """
        Place an order through the specified broker.

        :param user_id: The user ID.
        :param broker_key: The broker key ('paper' or 'exchange_mode').
        :param symbol: Trading symbol.
        :param side: OrderSide.BUY or OrderSide.SELL.
        :param order_type: OrderType.MARKET or OrderType.LIMIT.
        :param size: Order size.
        :param price: Limit price (required for limit orders).
        :return: OrderResult.
        """
        broker = self._get_broker(user_id, broker_key)
        if not broker:
            return OrderResult(
                success=False,
                message=f"Broker not found: {broker_key}"
            )

        try:
            result = broker.place_order(
                symbol=symbol,
                side=side,
                order_type=order_type,
                size=size,
                price=price
            )

            if result.success:
                # Save state after order
                if broker_key == 'paper':
                    state_id = f"manual_paper_{user_id}"
                else:
                    state_id = f"manual_live_{user_id}_{broker_key}"
                broker.save_state(state_id)

            return result
        except Exception as e:
            logger.error(f"Error placing order: {e}")
            return OrderResult(success=False, message=str(e))

    def _get_broker(self, user_id: int, broker_key: str):
        """
        Get a broker by user_id and broker_key.

        :param user_id: The user ID.
        :param broker_key: 'paper' or 'exchange_mode' format.
        :return: Broker instance or None.
        """
        if broker_key == 'paper':
            return self._paper_brokers.get(user_id)

        if user_id in self._live_brokers:
            return self._live_brokers[user_id].get(broker_key)

        return None

    def get_broker_balance(self, user_id: int, broker_key: str) -> float:
        """
        Get the total balance/equity for a broker.

        :param user_id: The user ID.
        :param broker_key: The broker key.
        :return: Total balance.
        """
        broker = self._get_broker(user_id, broker_key)
        if not broker:
            return 0.0
        return broker.get_balance()

    def get_available_balance(self, user_id: int, broker_key: str) -> float:
        """
        Get the available balance (not locked in orders).

        :param user_id: The user ID.
        :param broker_key: The broker key.
        :return: Available balance.
        """
        broker = self._get_broker(user_id, broker_key)
        if not broker:
            return 0.0
        return broker.get_available_balance()

    def recover_brokers_for_trades(self, trades: List[Any], get_username_func) -> int:
        """
        Recover/recreate brokers for persisted trades after a restart.

        This ensures that broker-managed trades are properly tracked after
        the application restarts. Should be called after loading trades from DB.

        :param trades: List of Trade objects with broker_order_id.
        :param get_username_func: Function that takes user_id and returns username.
        :return: Number of brokers recovered.
        """
        recovered = 0

        # Group trades by (user_id, broker_kind, broker_key)
        broker_needed: Dict[tuple, Dict] = {}

        for trade in trades:
            if not trade.broker_order_id:
                continue  # Not a broker-managed trade

            user_id = trade.creator
            if not user_id:
                continue

            if trade.broker_kind == 'paper':
                key = (user_id, 'paper', 'paper')
                broker_needed[key] = {'kind': 'paper'}
            elif trade.broker_kind == 'live' and trade.broker_exchange and trade.broker_mode:
                broker_key = f"{trade.broker_exchange}_{trade.broker_mode}"
                testnet = trade.broker_mode == 'testnet'
                key = (user_id, 'live', broker_key)
                broker_needed[key] = {
                    'kind': 'live',
                    'exchange': trade.broker_exchange,
                    'testnet': testnet
                }

        # Recreate each needed broker
        for (user_id, kind, broker_key), info in broker_needed.items():
            try:
                if kind == 'paper':
                    # Check if already exists
                    if user_id not in self._paper_brokers:
                        self.get_paper_broker(user_id)
                        logger.info(f"Recovered paper broker for user {user_id}")
                        recovered += 1
                else:
                    # Live broker
                    if user_id not in self._live_brokers or broker_key not in self._live_brokers.get(user_id, {}):
                        user_name = get_username_func(user_id)
                        if user_name:
                            broker = self.get_live_broker(
                                user_id=user_id,
                                exchange_name=info['exchange'],
                                testnet=info['testnet'],
                                user_name=user_name
                            )
                            if broker:
                                logger.info(f"Recovered live broker {broker_key} for user {user_id}")
                                recovered += 1
                            else:
                                logger.warning(f"Could not recover live broker {broker_key} for user {user_id}")
            except Exception as e:
                logger.error(f"Error recovering broker {broker_key} for user {user_id}: {e}")

        return recovered
