import asyncio
import signal
from typing import Optional, Callable, Awaitable
from loguru import logger

from src.configs import MarketConfig, KuruMMConfig, KuruTopicsSignature
from src.manager.orders_manager import OrdersManager, SentOrders
from src.executor.orders_executor import OrdersExecutor
from src.feed.rpc_ws import RpcWebsocket
from src.user.user import User
from src.manager.order import Order, OrderSide, OrderType, OrderStatus
from src.utils.utils import string_to_bytes32


class KuruClient:
    """
    Unified client for managing Kuru market maker operations.

    This class initializes and manages three core components:
    - OrdersManager: Manages order state and lifecycle
    - OrdersExecutor: Executes orders on-chain
    - RpcWebsocket: Listens to blockchain events via WebSocket

    Usage:
        # Option 1: Manual start/stop
        client = await KuruClient.create(market_config, kuru_config)
        # ... use client ...
        await client.stop()

        # Option 2: Async context manager (recommended)
        async with await KuruClient.create(market_config, kuru_config) as client:
            # ... use client ...
            pass
    """

    def __init__(self):
        """Internal constructor - use create() factory method instead."""
        raise NotImplementedError("Use KuruClient.create() async factory method")

    @classmethod
    async def create(
        cls, market_config: MarketConfig, kuru_mm_config: KuruMMConfig
    ) -> "KuruClient":
        """
        Create and initialize KuruClient with market and connection configurations.

        Args:
            market_config: Market-specific settings (addresses, decimals, etc.)
            kuru_mm_config: User/connection settings (RPC URLs, private key, etc.)

        Returns:
            Initialized KuruClient instance

        Raises:
            ConnectionError: If RPC connection fails
        """
        # Create instance without calling __init__
        self = cls.__new__(cls)

        # Store configs
        self.market_config = market_config
        self.kuru_mm_config = kuru_mm_config

        # Initialize User (manages user operations)
        self.user = User(
            user_address=kuru_mm_config.user_address,
            market_config=market_config,
            rpc_url=kuru_mm_config.rpc_url,
            private_key=kuru_mm_config.private_key,
            kuru_api_url=kuru_mm_config.kuru_api_url,
        )

        # Initialize OrdersManager (manages order state) - now async
        self.orders_manager = await OrdersManager.create(rpc_url=kuru_mm_config.rpc_url)

        # Initialize OrdersExecutor (executes orders on-chain)
        self.executor = OrdersExecutor(
            mm_entrypoint_address=market_config.mm_entrypoint_address,
            config=kuru_mm_config,
            order_book_address=market_config.market_address,
            market_config=market_config,
        )

        # Shutdown event for graceful shutdown on signals
        self._shutdown_event = asyncio.Event()

        # Task reference for log processing
        self._log_processing_task: Optional[asyncio.Task] = None

        # Order callback consumer
        self._order_callback: Optional[Callable[[Order], Awaitable[None]]] = None
        self._order_consumer_task: Optional[asyncio.Task] = None

        # Initialize RpcWebsocket (listens to blockchain events)
        self.websocket = RpcWebsocket(
            rpc_url=kuru_mm_config.rpc_ws_url,
            orderbook_address=market_config.market_address,
            user_address=kuru_mm_config.user_address,
            orders_manager=self.orders_manager,
            market_config=market_config,
            shutdown_event=self._shutdown_event,
        )

        return self

    def set_order_callback(
        self, callback: Optional[Callable[[Order], Awaitable[None]]]
    ) -> None:
        """
        Set callback function to automatically process orders from the queue.

        The callback will be called for each order that is placed, filled, or cancelled.
        The consumer task runs in the background during the client lifecycle.

        Args:
            callback: Async function that receives an Order object.
                      Set to None to disable the callback.

        Example:
            async def my_order_handler(order: Order):
                logger.info(f"Order {order.cloid} status: {order.status}")

            client.set_order_callback(my_order_handler)
            await client.start()

        Note:
            - If callback raises an exception, it will be logged and processing continues
            - Direct queue access (client.orders_manager.processed_orders_queue) remains available
            - Callback can be set before or after calling start()
        """
        self._order_callback = callback
        logger.info(f"Order callback {'set' if callback else 'cleared'}")

        # If client is already started and callback is set, start consumer
        if (
            callback is not None
            and self._log_processing_task is not None
            and not self._log_processing_task.done()
        ):
            if self._order_consumer_task is None or self._order_consumer_task.done():
                self._order_consumer_task = asyncio.create_task(self._consume_orders())
                logger.info("Order consumer task started")

    async def start(self) -> None:
        """
        Connect to websocket, subscribe to logs, and start processing.

        This method:
        1. Sets up signal handlers for graceful shutdown (SIGINT, SIGTERM)
        2. Authorizes MM Entrypoint
        3. Connects to the WebSocket
        4. Subscribes to contract events
        5. Starts processing logs in the background

        Note: OrdersManager is already connected during initialization via KuruClient.create()
        """
        # Authorize MM Entrypoint
        logger.info(
            f"Authorizing MM Entrypoint for user {self.user.user_address} with MM Entrypoint {self.user.mm_entrypoint_address}"
        )
        await self.user.eip_7702_auth()

        # Connect to websocket
        await self.websocket.connect()

        # Subscribe to logs
        await self.websocket.create_log_subscription(KuruTopicsSignature)

        # Start processing logs in background task
        self._log_processing_task = asyncio.create_task(
            self.websocket.process_subscription_logs()
        )

        # Start order consumer if callback is set
        if self._order_callback is not None:
            self._order_consumer_task = asyncio.create_task(self._consume_orders())
            logger.info("Order consumer task started")

    async def place_orders(self, orders: list[Order], post_only: bool = True) -> str:
        """
        Place orders on the market.

        Args:
            orders: List of orders to place
            post_only: Whether orders should be post-only

        Returns:
            Transaction hash as hex string
        """
        # split the orders list into buy orders, sell orders and cancel orders
        buy_orders = [order for order in orders if order.side == OrderSide.BUY]
        sell_orders = [order for order in orders if order.side == OrderSide.SELL]
        cancel_orders = [
            order for order in orders if order.order_type == OrderType.CANCEL
        ]

        # sort the buy orders by price in descending order
        buy_orders.sort(key=lambda x: x.price, reverse=True)
        # sort the sell orders by price in ascending order
        sell_orders.sort(key=lambda x: x.price)

        # Register all orders BEFORE sending transaction
        orders_to_register = buy_orders + sell_orders
        for order in orders_to_register:
            order.update_status(OrderStatus.ORDER_SENT)
            self.orders_manager.cloid_to_order[order.cloid] = order


        buy_cloids = [string_to_bytes32(order.cloid) for order in buy_orders]
        buy_prices = [order.price for order in buy_orders]
        buy_sizes = [order.size for order in buy_orders]

        sell_cloids = [string_to_bytes32(order.cloid) for order in sell_orders]
        sell_prices = [order.price for order in sell_orders]
        sell_sizes = [order.size for order in sell_orders]

        cancel_cloids = [string_to_bytes32(order.cloid) for order in cancel_orders]

        kuru_order_ids_to_cancel = []
        orders_to_cancel_metadata = []  # For access list optimization

        for cancel_order in cancel_orders:
            # Get kuru order ID (existing logic)
            kuru_order_id = self.orders_manager.get_kuru_order_id(cancel_order.cloid)
            if kuru_order_id is None:
                logger.error(
                    f"Kuru order ID not found for cancel order {cancel_order.cloid}"
                )
                continue

            kuru_order_ids_to_cancel.append(kuru_order_id)

            # Get original order to extract price and side for access list
            original_order = self.orders_manager.cloid_to_order.get(cancel_order.cloid)
            if (
                original_order
                and original_order.price is not None
                and original_order.side is not None
            ):
                # Convert price to integer using market precision
                price_int = int(original_order.price * self.market_config.price_precision)
                is_buy = original_order.side == OrderSide.BUY

                orders_to_cancel_metadata.append((kuru_order_id, price_int, is_buy))
                logger.debug(
                    f"Added cancel metadata for {cancel_order.cloid}: "
                    f"order_id={kuru_order_id}, price={price_int}, is_buy={is_buy}"
                )
            else:
                logger.warning(
                    f"Could not get price/side for cancel order {cancel_order.cloid}, "
                    "access list will be less optimal"
                )

        txhash = await self.executor.place_order(
            buy_cloids,
            sell_cloids,
            cancel_cloids,
            buy_prices,
            buy_sizes,
            sell_prices,
            sell_sizes,
            kuru_order_ids_to_cancel,
            orders_to_cancel_metadata,  # Pass metadata for access list
            post_only=False,
        )

        all_orders = orders_to_register + cancel_orders

        # Set txhash on all orders and register txhash mapping
        for order in all_orders:
            order.set_txhash(txhash)

        # Register sent orders by txhash
        self.orders_manager.txhash_to_sent_orders[txhash] = SentOrders(
            buy_orders=buy_orders, sell_orders=sell_orders, cancel_orders=cancel_orders
        )

        # Add to pending transactions cache
        await self.orders_manager.pending_transactions.set(txhash, txhash)

        return txhash

    async def place_market_buy(
        self,
        quote_amount: float,
        min_amount_out: float,
        is_margin: bool = True,
        is_fill_or_kill: bool = False,
    ) -> str:
        """
        Place a market buy order.

        This function buys base tokens by spending quote tokens at the best available market price.
        The order executes immediately.

        Args:
            quote_amount: Amount of quote tokens to spend
            min_amount_out: Minimum base tokens to receive (slippage protection)
            is_margin: Whether to use margin account (default: True)
            is_fill_or_kill: Execute fully or revert (default: False)

        Returns:
            Transaction hash as hex string
        """
        txhash = await self.executor.place_market_buy(
            quote_amount, min_amount_out, is_margin, is_fill_or_kill
        )

        # Add to pending transactions cache
        await self.orders_manager.pending_transactions.set(txhash, txhash)

        return txhash

    async def place_market_sell(
        self,
        size: float,
        min_amount_out: float,
        is_margin: bool = True,
        is_fill_or_kill: bool = False,
    ) -> str:
        """
        Place a market sell order.

        This function sells base tokens to receive quote tokens at the best available market price.
        The order executes immediately.

        Args:
            size: Amount of base tokens to sell
            min_amount_out: Minimum quote tokens to receive (slippage protection)
            is_margin: Whether to use margin account (default: True)
            is_fill_or_kill: Execute fully or revert (default: False)

        Returns:
            Transaction hash as hex string
        """
        txhash = await self.executor.place_market_sell(
            size, min_amount_out, is_margin, is_fill_or_kill
        )

        # Add to pending transactions cache
        await self.orders_manager.pending_transactions.set(txhash, txhash)

        return txhash

    async def cancel_all_active_orders_for_market(self, use_access_list: bool = True) -> None:
        """
        Cancel all active orders for the market.
        Loops until all orders are cancelled by checking the API every 3 seconds.

        Args:
            use_access_list: If True, build transaction with EIP-2930 access list for gas optimization.
                             If False, build transaction without access list (default: True)
        """

        logger.warning(f"Cancelling all active orders for market {self.user.market_address}")

        while True:
            # Fetch active orders from API
            orders = self.user.get_active_orders()

            # Check if there are any active orders
            if not orders:
                logger.info("No more active orders to cancel")
                break

            # Extract order data - format depends on use_access_list parameter
            orders_to_cancel = []
            for order in orders:
                order_id = order["orderid"]

                if use_access_list:
                    # Include metadata for access list optimization
                    price = int(order["price"])  # Price already in precision units (from API)
                    is_buy = order["isbuy"]  # Boolean from API
                    orders_to_cancel.append((order_id, price, is_buy))
                else:
                    # Just order IDs - no access list
                    orders_to_cancel.append(order_id)

            # Log message depends on whether access list is used
            access_list_msg = "with access list" if use_access_list else "without access list"
            order_ids = [order_id for order_id, _, _ in orders_to_cancel] if use_access_list else orders_to_cancel
            logger.info(
                f"Cancelling {len(orders_to_cancel)} active orders {access_list_msg}: {order_ids}"
            )

            # Cancel orders - executor will detect format and handle appropriately
            txhash = await self.executor.cancel_orders_with_kuru_order_ids(orders_to_cancel)
            logger.success(f"Cancelled {len(orders_to_cancel)} orders with txhash: {txhash}")

            # Sleep for 3 seconds before checking again
            await asyncio.sleep(3)


    async def stop(self, signal_num: Optional[int] = None) -> None:
        """
        Gracefully shutdown the client.

        This method:
        1. Cancels the log processing task
        2. Cancels the order consumer task (if running)
        3. Processes any remaining orders
        4. Disconnects from the WebSocket

        Args:
            signal_num: Optional signal number if triggered by a signal
        """
        if signal_num:
            signal_name = signal.Signals(signal_num).name
            logger.info(f"Stopping client due to signal {signal_name} ({signal_num})")
        else:
            logger.info("Stopping client...")
        # Cancel log processing task if running
        if (
            self._log_processing_task is not None
            and not self._log_processing_task.done()
        ):
            self._log_processing_task.cancel()
            try:
                await asyncio.wait_for(self._log_processing_task, timeout=3.0)
            except asyncio.TimeoutError:
                logger.warning("Log processing task cancellation timed out after 3s")
            except asyncio.CancelledError:
                pass
            except Exception as e:
                logger.debug(f"Error during log processing task cleanup: {e}")

        # Cancel order consumer task if running
        if (
            self._order_consumer_task is not None
            and not self._order_consumer_task.done()
        ):
            self._order_consumer_task.cancel()
            try:
                await asyncio.wait_for(self._order_consumer_task, timeout=3.0)
            except asyncio.TimeoutError:
                logger.warning("Order consumer task cancellation timed out after 3s")
            except asyncio.CancelledError:
                pass
            except Exception as e:
                logger.debug(f"Error during order consumer task cleanup: {e}")

            # Process any remaining orders in queue before shutdown
            if self._order_callback is not None:
                logger.info("Processing remaining orders before shutdown...")
                remaining_count = 0
                while not self.orders_manager.processed_orders_queue.empty():
                    try:
                        order = self.orders_manager.processed_orders_queue.get_nowait()
                        await self._order_callback(order)
                        remaining_count += 1
                    except asyncio.QueueEmpty:
                        break
                    except Exception as e:
                        logger.error(f"Error processing remaining order: {e}")

                if remaining_count > 0:
                    logger.info(f"Processed {remaining_count} remaining orders")

        # Disconnect websocket
        await self.websocket.disconnect()

        # Close all HTTP provider sessions
        try:
            await self.user.close()
        except Exception as e:
            logger.debug(f"Error closing user session: {e}")

        try:
            await self.orders_manager.close()
        except Exception as e:
            logger.debug(f"Error closing orders_manager session: {e}")

        try:
            await self.executor.close()
        except Exception as e:
            logger.debug(f"Error closing executor session: {e}")

        logger.info("Client stopped")

    def _setup_signal_handlers(self, loop: asyncio.AbstractEventLoop) -> None:
        """
        Setup signal handlers for graceful shutdown on SIGINT and SIGTERM.

        Args:
            loop: The asyncio event loop to register handlers with
        """
        for sig in (signal.SIGINT, signal.SIGTERM):
            loop.add_signal_handler(sig, lambda s=sig: self._signal_handler(s, loop))

    def _signal_handler(self, sig_num: int, loop: asyncio.AbstractEventLoop) -> None:
        """
        Handle SIGINT and SIGTERM signals for graceful shutdown.

        Args:
            sig_num: The signal number received (e.g., signal.SIGINT)
            loop: The asyncio event loop
        """
        signal_name = signal.Signals(sig_num).name
        logger.info(
            f"Received signal {signal_name} ({sig_num}), initiating graceful shutdown..."
        )

        self._shutdown_event.set()

        loop.call_soon(self._schedule_stop, sig_num)

    def _schedule_stop(self, sig_num: int) -> None:
        """Schedule stop() to run in the event loop."""
        try:
            asyncio.create_task(self.stop(sig_num))
        except RuntimeError:
            pass

    async def _consume_orders(self) -> None:
        """
        Background task to consume orders from the queue and invoke callback.

        Runs continuously while client is active and callback is set.
        Handles exceptions from user callbacks gracefully.
        """
        logger.info("Order consumer started")

        try:
            while True:
                # Check for shutdown signal
                if self._shutdown_event.is_set():
                    logger.info("Shutdown signal received, stopping order consumer...")
                    break

                # Get order from queue with timeout to check shutdown periodically
                try:
                    order = await asyncio.wait_for(
                        self.orders_manager.processed_orders_queue.get(), timeout=1.0
                    )
                except asyncio.TimeoutError:
                    # No orders, continue loop to check shutdown
                    continue

                # Skip if callback was cleared
                if self._order_callback is None:
                    continue

                # Invoke callback
                try:
                    await self._order_callback(order)
                except Exception as e:
                    logger.error(
                        f"Error in order callback for order {order.cloid}: {e}",
                        exc_info=True,
                    )
                    # Continue processing - don't let one error stop the consumer

        except asyncio.CancelledError:
            logger.debug("Order consumer cancelled")
            raise
        except Exception as e:
            logger.error(f"Fatal error in order consumer: {e}", exc_info=True)
        finally:
            logger.info("Order consumer stopped")

    async def __aenter__(self) -> "KuruClient":
        """
        Async context manager entry.

        Note: With factory pattern, client is already initialized by KuruClient.create()
        This method simply returns self.

        Returns:
            The KuruClient instance
        """
        return self

    async def __aexit__(self, exc_type, exc_val, exc_tb) -> None:
        """
        Async context manager exit - automatically stops the client.

        Args:
            exc_type: Exception type if an exception occurred
            exc_val: Exception value if an exception occurred
            exc_tb: Exception traceback if an exception occurred
        """
        await self.stop()
