"""TradingSetup - Synchronous wrapper for trading configuration.

Provides a sync API for:
- Querying strategies, instruments, accounts, portfolios, bindings, slots
- Creating portfolios and accounts
- Binding portfolios to accounts and accounts to slots

Uses threading + asyncio pattern (same as BatchTester) to wrap async operations.
"""

from __future__ import annotations

import asyncio
import logging
import threading
from dataclasses import dataclass
from datetime import datetime
from decimal import Decimal
from typing import TYPE_CHECKING, Any, Union

if TYPE_CHECKING:
    from tradepose_models.strategy import Blueprint, StrategyConfig

logger = logging.getLogger(__name__)


# =============================================================================
# INFO CLASSES (Simplified response models for sync API)
# =============================================================================


@dataclass
class BlueprintInfo:
    """Blueprint identifier with id and name."""

    id: str
    name: str


@dataclass
class StrategyInfo:
    """Strategy information."""

    id: str
    name: str
    base_instrument: str
    base_freq: str
    note: str
    base_blueprint: BlueprintInfo
    advanced_blueprints: list[BlueprintInfo]


@dataclass
class InstrumentInfo:
    """Instrument information."""

    id: int
    symbol: str
    account_source: str
    broker_type: str
    market_type: str
    base_currency: str
    quote_currency: str
    tick_size: str
    lot_size: str
    price_precision: int
    quantity_precision: int
    point_value: str
    status: str


@dataclass
class AccountInfo:
    """Account information."""

    id: str
    name: str
    broker_type: str
    available_markets: list[str]
    status: str
    is_archived: bool
    account_source: str | None = None


@dataclass
class SelectionInfo:
    """Blueprint selection in a portfolio."""

    strategy_id: str
    blueprint_id: str
    strategy_name: str
    blueprint_name: str


@dataclass
class PortfolioInfo:
    """Portfolio information."""

    id: str
    name: str
    capital: str
    currency: str
    account_source: str
    selections: list[SelectionInfo]


@dataclass
class BindingInfo:
    """Binding information."""

    id: str
    account_id: str
    portfolio_id: str
    execution_mode: str
    is_active: bool


@dataclass
class SlotInfo:
    """Slot information."""

    id: str
    node_seq: int
    slot_idx: int
    account_id: str | None
    bound_at: datetime | None


# =============================================================================
# TRADING SETUP CLASS
# =============================================================================


class TradingSetup:
    """Synchronous trading configuration utility.

    Wraps async operations using threading + asyncio pattern.
    Users don't need to write async/await.

    Example:
        >>> from tradepose_client import TradingSetup
        >>>
        >>> setup = TradingSetup(api_key="tp_live_xxx")
        >>>
        >>> # Query existing resources (sync)
        >>> strategies = setup.list_strategies()
        >>> instruments = setup.list_instruments(symbol="BTC")
        >>> accounts = setup.list_accounts()
        >>> slots = setup.list_slots()
        >>>
        >>> # Create resources (sync)
        >>> portfolio = setup.create_portfolio(...)
        >>> account = setup.create_account(...)
        >>>
        >>> # Bind resources (sync)
        >>> binding = setup.bind_portfolio(account_id, portfolio_id)
        >>> slot = setup.bind_slot(node_seq=0, slot_idx=0, account_id=account_id)
    """

    def __init__(
        self,
        api_key: str,
        server_url: str = "https://api.tradepose.com",
    ):
        """Initialize TradingSetup.

        Args:
            api_key: API key for authentication
            server_url: Gateway server URL
        """
        self._api_key = api_key
        self._server_url = server_url

    # =========================================================================
    # QUERY METHODS (sync, I/O)
    # =========================================================================

    def list_strategies(self, *, include_archived: bool = True) -> list[StrategyInfo]:
        """List all strategies for the authenticated user.

        Args:
            include_archived: Whether to include archived strategies. Defaults to True.

        Returns:
            List of StrategyInfo objects
        """
        return self._run_async(self._async_list_strategies(include_archived=include_archived))

    def get_strategy(self, strategy_id: str) -> StrategyConfig:
        """Get complete strategy configuration by ID.

        Args:
            strategy_id: Strategy UUID (from StrategyInfo.id)

        Returns:
            StrategyConfig with full strategy details including blueprints
        """
        return self._run_async(self._async_get_strategy(strategy_id))

    def list_instruments(
        self,
        *,
        symbol: str | None = None,
        account_source: str | None = None,
        broker_type: str | None = None,
        market_type: str | None = None,
        limit: int = 100,
    ) -> list[InstrumentInfo]:
        """List available instruments.

        Args:
            symbol: Filter by symbol (partial match)
            account_source: Filter by account source (e.g., 'BINANCE')
            broker_type: Filter by broker type
            market_type: Filter by market type (e.g., 'spot', 'futures')
            limit: Maximum results (default: 100)

        Returns:
            List of InstrumentInfo objects
        """
        return self._run_async(
            self._async_list_instruments(
                symbol=symbol,
                account_source=account_source,
                broker_type=broker_type,
                market_type=market_type,
                limit=limit,
            )
        )

    def list_accounts(self) -> list[AccountInfo]:
        """List all accounts for the authenticated user.

        Returns:
            List of AccountInfo objects
        """
        return self._run_async(self._async_list_accounts())

    def list_portfolios(self) -> list[PortfolioInfo]:
        """List all portfolios for the authenticated user.

        Returns:
            List of PortfolioInfo objects
        """
        return self._run_async(self._async_list_portfolios())

    def list_bindings(self) -> list[BindingInfo]:
        """List all bindings for the authenticated user.

        Returns:
            List of BindingInfo objects
        """
        return self._run_async(self._async_list_bindings())

    def list_slots(self) -> list[SlotInfo]:
        """List all trader slots for the authenticated user.

        Returns:
            List of SlotInfo objects
        """
        return self._run_async(self._async_list_slots())

    # =========================================================================
    # FIND HELPER METHODS (sync, convenience)
    # =========================================================================

    def find_strategy(self, identifier: str) -> StrategyInfo | None:
        """Find a strategy by name or ID.

        Args:
            identifier: Strategy name or UUID to search for (exact match)

        Returns:
            StrategyInfo if found, None otherwise

        Example:
            >>> strategy = setup.find_strategy("SuperTrend_BTCUSDT")
            >>> strategy = setup.find_strategy("550e8400-e29b-41d4-a716-446655440000")
            >>> if strategy:
            ...     blueprint_ids = [strategy.base_blueprint.id]
        """
        strategies = self.list_strategies()
        for s in strategies:
            if s.name == identifier or s.id == identifier:
                return s
        return None

    def find_account(self, identifier: str) -> AccountInfo | None:
        """Find an account by name or ID.

        Args:
            identifier: Account name or UUID to search for (exact match)

        Returns:
            AccountInfo if found, None otherwise

        Example:
            >>> account = setup.find_account("binance_main")
            >>> account = setup.find_account("550e8400-e29b-41d4-a716-446655440000")
            >>> if account:
            ...     setup.bind_portfolio(account.id, "my_portfolio")
        """
        accounts = self.list_accounts()
        for a in accounts:
            if a.name == identifier or a.id == identifier:
                return a
        return None

    def find_portfolio(self, identifier: str) -> PortfolioInfo | None:
        """Find a portfolio by name or ID.

        Args:
            identifier: Portfolio name or UUID to search for (exact match)

        Returns:
            PortfolioInfo if found, None otherwise

        Example:
            >>> portfolio = setup.find_portfolio("supertrend_portfolio")
            >>> portfolio = setup.find_portfolio("550e8400-e29b-41d4-a716-446655440000")
            >>> if portfolio:
            ...     print(f"Capital: {portfolio.capital}")
        """
        portfolios = self.list_portfolios()
        for p in portfolios:
            if p.name == identifier or p.id == identifier:
                return p
        return None

    def find_available_slot(self) -> SlotInfo | None:
        """Find the first available (unbound) slot.

        Returns:
            SlotInfo if an available slot exists, None otherwise

        Example:
            >>> slot = setup.find_available_slot()
            >>> if slot:
            ...     setup.bind_slot(slot.node_seq, slot.slot_idx, account.id)
        """
        slots = self.list_slots()
        for s in slots:
            if s.account_id is None:
                return s
        return None

    def find_binding(self, *, account_id: str) -> BindingInfo | None:
        """Find a binding by account_id.

        Args:
            account_id: Account UUID to filter by

        Returns:
            BindingInfo if found, None otherwise

        Example:
            >>> account = setup.find_account("binance_main")
            >>> binding = setup.find_binding(account_id=account.id)
        """
        bindings = self.list_bindings()
        for b in bindings:
            if b.account_id == account_id:
                return b
        return None

    # =========================================================================
    # CREATE METHODS (sync, I/O)
    # =========================================================================

    def create_portfolio(
        self,
        name: str,
        capital: Decimal | int | float,
        currency: str,
        account_source: str,
        blueprints: list[Union[str, BlueprintInfo, "Blueprint"]],
    ) -> PortfolioInfo:
        """Create a new portfolio.

        Args:
            name: Portfolio name (unique per user)
            capital: Portfolio capital
            currency: Currency code (e.g., 'USDT', 'USD')
            account_source: Account source (e.g., 'BINANCE', 'FTMO')
            blueprints: List of blueprint identifiers. Accepts:
                - str: Blueprint UUID string
                - BlueprintInfo: Object with id attribute
                - Blueprint: tradepose_models.Blueprint with id attribute

        Returns:
            Created PortfolioInfo

        Raises:
            ValueError: If Blueprint.id is None (not saved to database)
        """
        # Convert blueprints to list of ID strings
        blueprint_ids: list[str] = []
        for bp in blueprints:
            if isinstance(bp, str):
                blueprint_ids.append(bp)
            elif hasattr(bp, "id"):
                bp_id = bp.id
                if bp_id is None:
                    raise ValueError(
                        f"Blueprint '{getattr(bp, 'name', bp)}' has no ID. "
                        "Save the strategy to database first using TradingSetup."
                    )
                blueprint_ids.append(str(bp_id))
            else:
                raise TypeError(
                    f"Expected str, BlueprintInfo, or Blueprint, got {type(bp).__name__}"
                )

        return self._run_async(
            self._async_create_portfolio(
                name=name,
                capital=capital,
                currency=currency,
                account_source=account_source,
                blueprint_ids=blueprint_ids,
            )
        )

    def create_account(
        self,
        name: str,
        broker_type: str,
        credentials: dict,
        *,
        available_markets: list[str] | None = None,
        account_source: str | None = None,
    ) -> AccountInfo:
        """Create a new broker account.

        Args:
            name: Account name (unique per user)
            broker_type: Broker type (e.g., 'binance', 'mt5')
            credentials: Broker credentials (will be encrypted)
            available_markets: Available market types (default: ['spot'])
            account_source: Account source (e.g., 'FTMO', 'FIVEPERCENT').
                Required for MT5 accounts.

        Returns:
            Created AccountInfo
        """
        return self._run_async(
            self._async_create_account(
                name=name,
                broker_type=broker_type,
                credentials=credentials,
                available_markets=available_markets,
                account_source=account_source,
            )
        )

    # =========================================================================
    # BIND METHODS (sync, I/O)
    # =========================================================================

    def bind_portfolio(
        self,
        account_id: str,
        portfolio_id: str,
        *,
        execution_mode: str = "signal_priority",
        skip_validation: bool = False,
    ) -> BindingInfo:
        """Bind a portfolio to an account.

        Validates that portfolio's account_source matches account's account_source
        before attempting to create the binding.

        Args:
            account_id: Account UUID
            portfolio_id: Portfolio UUID
            execution_mode: Execution mode (default: 'signal_priority')
            skip_validation: Skip client-side validation (default: False).
                Set to True when you've already verified account_source compatibility
                to avoid redundant API calls. Server still validates.

        Returns:
            Created BindingInfo

        Raises:
            BindingError: If account_source mismatch or resources not found

        Example:
            # Without skip_validation (4 HTTP calls):
            account = setup.find_account("binance_main")
            portfolio = setup.find_portfolio("crypto_port")
            binding = setup.bind_portfolio(account.id, portfolio.id)

            # With skip_validation (2 HTTP calls - recommended):
            account = setup.find_account("binance_main")
            portfolio = setup.find_portfolio("crypto_port")
            # Verify compatibility yourself
            if account.account_source == portfolio.account_source:
                binding = setup.bind_portfolio(
                    account.id, portfolio.id, skip_validation=True
                )
        """
        # Skip client-side validation if requested (server still validates)
        if skip_validation:
            return self._run_async(
                self._async_bind_portfolio(
                    account_id=account_id,
                    portfolio_id=portfolio_id,
                    execution_mode=execution_mode,
                )
            )

        from tradepose_client.exceptions import BindingError

        # Pre-validate account_source compatibility
        accounts = self.list_accounts()
        account = None
        for a in accounts:
            if a.id == account_id:
                account = a
                break

        if account is None:
            raise BindingError(
                message=f"Account with id '{account_id}' not found.",
                suggestion="Use list_accounts() to see available accounts.",
            )

        portfolios = self.list_portfolios()
        portfolio = None
        for p in portfolios:
            if p.id == portfolio_id:
                portfolio = p
                break

        if portfolio is None:
            raise BindingError(
                message=f"Portfolio with id '{portfolio_id}' not found.",
                suggestion="Use list_portfolios() to see available portfolios, "
                "or create_portfolio() to create a new one.",
            )

        # Check account_source compatibility
        # Account.account_source can be None for some broker types (e.g., binance)
        # Portfolio.account_source is always set
        account_src = account.account_source or account.broker_type.upper()
        portfolio_src = portfolio.account_source

        if account_src != portfolio_src:
            raise BindingError(
                message=(
                    f"Cannot bind account '{account.name}' (source: {account_src}) "
                    f"to portfolio '{portfolio.name}' (source: {portfolio_src})."
                ),
                account_name=account.name,
                portfolio_name=portfolio.name,
                account_source=account_src,
                portfolio_source=portfolio_src,
                suggestion=(
                    f"Account source must match portfolio account_source. "
                    f"Either change the portfolio's account_source to '{account_src}', "
                    f"or use an account with source '{portfolio_src}'."
                ),
            )

        return self._run_async(
            self._async_bind_portfolio(
                account_id=account_id,
                portfolio_id=portfolio_id,
                execution_mode=execution_mode,
            )
        )

    def bind_slot(
        self,
        node_seq: int,
        slot_idx: int,
        account_id: str,
    ) -> SlotInfo:
        """Bind an account to a trader slot.

        Args:
            node_seq: Node sequence number (0-based)
            slot_idx: Slot index within the node (0-based)
            account_id: Account UUID to bind

        Returns:
            Updated SlotInfo
        """
        return self._run_async(
            self._async_bind_slot(
                node_seq=node_seq,
                slot_idx=slot_idx,
                account_id=account_id,
            )
        )

    def unbind_slot(
        self,
        node_seq: int,
        slot_idx: int,
    ) -> SlotInfo:
        """Unbind an account from a trader slot.

        Args:
            node_seq: Node sequence number (0-based)
            slot_idx: Slot index within the node (0-based)

        Returns:
            Updated SlotInfo (account_id will be None)
        """
        return self._run_async(
            self._async_unbind_slot(
                node_seq=node_seq,
                slot_idx=slot_idx,
            )
        )

    # =========================================================================
    # INTERNAL: ASYNC IMPLEMENTATIONS
    # =========================================================================

    async def _async_list_strategies(self, *, include_archived: bool = True) -> list[StrategyInfo]:
        """Async implementation of list_strategies."""
        from tradepose_client import TradePoseClient

        async with TradePoseClient(api_key=self._api_key, server_url=self._server_url) as client:
            response = await client.strategies.list(archived=include_archived)
            return [
                StrategyInfo(
                    id=s.id,
                    name=s.name,
                    base_instrument=s.base_instrument,
                    base_freq=s.base_freq,
                    note=s.note,
                    base_blueprint=BlueprintInfo(
                        id=s.base_blueprint.id,
                        name=s.base_blueprint.name,
                    ),
                    advanced_blueprints=[
                        BlueprintInfo(id=bp.id, name=bp.name) for bp in s.advanced_blueprints
                    ],
                )
                for s in response.strategies
            ]

    async def _async_get_strategy(self, strategy_id: str) -> StrategyConfig:
        """Async implementation of get_strategy."""
        from tradepose_client import TradePoseClient

        async with TradePoseClient(api_key=self._api_key, server_url=self._server_url) as client:
            return await client.strategies.get(strategy_id)

    async def _async_list_instruments(
        self,
        *,
        symbol: str | None = None,
        account_source: str | None = None,
        broker_type: str | None = None,
        market_type: str | None = None,
        limit: int = 100,
    ) -> list[InstrumentInfo]:
        """Async implementation of list_instruments."""
        from tradepose_client import TradePoseClient

        async with TradePoseClient(api_key=self._api_key, server_url=self._server_url) as client:
            response = await client.instruments.list(
                symbol=symbol,
                account_source=account_source,
                broker_type=broker_type,
                market_type=market_type,
                limit=limit,
            )
            return [
                InstrumentInfo(
                    id=i.id,
                    symbol=i.symbol,
                    account_source=i.account_source,
                    broker_type=i.broker_type,
                    market_type=i.market_type,
                    base_currency=i.base_currency,
                    quote_currency=i.quote_currency,
                    tick_size=i.tick_size,
                    lot_size=i.lot_size,
                    price_precision=i.price_precision,
                    quantity_precision=i.quantity_precision,
                    point_value=i.point_value,
                    status=i.status,
                )
                for i in response.instruments
            ]

    async def _async_list_accounts(self) -> list[AccountInfo]:
        """Async implementation of list_accounts."""
        from tradepose_client import TradePoseClient

        async with TradePoseClient(api_key=self._api_key, server_url=self._server_url) as client:
            response = await client.accounts.list()
            return [
                AccountInfo(
                    id=a.id,
                    name=a.name,
                    broker_type=a.broker_type.value
                    if hasattr(a.broker_type, "value")
                    else str(a.broker_type),
                    available_markets=[
                        m.value if hasattr(m, "value") else str(m) for m in a.available_markets
                    ],
                    status=a.status.value if hasattr(a.status, "value") else str(a.status),
                    is_archived=a.is_archived,
                    account_source=a.account_source.value
                    if a.account_source and hasattr(a.account_source, "value")
                    else (str(a.account_source) if a.account_source else None),
                )
                for a in response.accounts
            ]

    async def _async_list_portfolios(self) -> list[PortfolioInfo]:
        """Async implementation of list_portfolios."""
        from tradepose_client import TradePoseClient

        async with TradePoseClient(api_key=self._api_key, server_url=self._server_url) as client:
            response = await client.portfolios.list()
            return [
                PortfolioInfo(
                    id=str(p.id),
                    name=p.name,
                    capital=str(p.capital),
                    currency=p.currency,
                    account_source=p.account_source,
                    selections=[
                        SelectionInfo(
                            strategy_id=str(s.strategy_id),
                            blueprint_id=str(s.blueprint_id),
                            strategy_name=s.strategy_name,
                            blueprint_name=s.blueprint_name,
                        )
                        for s in p.selections
                    ],
                )
                for p in response.portfolios
            ]

    async def _async_list_bindings(self) -> list[BindingInfo]:
        """Async implementation of list_bindings."""
        from tradepose_client import TradePoseClient

        async with TradePoseClient(api_key=self._api_key, server_url=self._server_url) as client:
            response = await client.bindings.list()
            return [
                BindingInfo(
                    id=str(b.id),
                    account_id=str(b.account_id),
                    portfolio_id=str(b.portfolio_id),
                    execution_mode=b.execution_mode,
                    is_active=b.is_active,
                )
                for b in response.bindings
            ]

    async def _async_list_slots(self) -> list[SlotInfo]:
        """Async implementation of list_slots."""
        from tradepose_client import TradePoseClient

        async with TradePoseClient(api_key=self._api_key, server_url=self._server_url) as client:
            response = await client.slots.list()
            return [
                SlotInfo(
                    id=s.id,
                    node_seq=s.node_seq,
                    slot_idx=s.slot_idx,
                    account_id=s.account_id,
                    bound_at=s.bound_at,
                )
                for s in response.slots
            ]

    async def _async_create_portfolio(
        self,
        name: str,
        capital: Decimal | int | float,
        currency: str,
        account_source: str,
        blueprint_ids: list[str],
    ) -> PortfolioInfo:
        """Async implementation of create_portfolio."""
        from tradepose_client import TradePoseClient
        from tradepose_client.resources.portfolios import PortfolioCreateRequest

        async with TradePoseClient(api_key=self._api_key, server_url=self._server_url) as client:
            request = PortfolioCreateRequest(
                name=name,
                capital=str(capital),
                currency=currency,
                account_source=account_source,
                blueprint_ids=blueprint_ids,
            )
            response = await client.portfolios.create(request)
            return PortfolioInfo(
                id=str(response.id),
                name=response.name,
                capital=str(response.capital),
                currency=response.currency,
                account_source=response.account_source,
                selections=[
                    SelectionInfo(
                        strategy_id=str(s.strategy_id),
                        blueprint_id=str(s.blueprint_id),
                        strategy_name=s.strategy_name,
                        blueprint_name=s.blueprint_name,
                    )
                    for s in response.selections
                ],
            )

    async def _async_create_account(
        self,
        name: str,
        broker_type: str,
        credentials: dict,
        available_markets: list[str] | None = None,
        account_source: str | None = None,
    ) -> AccountInfo:
        """Async implementation of create_account."""
        from tradepose_models.broker import BrokerType, MarketType
        from tradepose_models.enums import AccountSource

        from tradepose_client import TradePoseClient
        from tradepose_client.resources.accounts import AccountCreateRequest

        async with TradePoseClient(api_key=self._api_key, server_url=self._server_url) as client:
            markets = (
                [MarketType.SPOT]
                if not available_markets
                else [MarketType(m) for m in available_markets]
            )
            request = AccountCreateRequest(
                name=name,
                broker_type=BrokerType(broker_type),
                credentials=credentials,
                available_markets=markets,
                account_source=AccountSource(account_source) if account_source else None,
            )
            response = await client.accounts.create(request)
            return AccountInfo(
                id=response.id,
                name=response.name,
                broker_type=response.broker_type.value
                if hasattr(response.broker_type, "value")
                else str(response.broker_type),
                available_markets=[
                    m.value if hasattr(m, "value") else str(m) for m in response.available_markets
                ],
                status=response.status.value
                if hasattr(response.status, "value")
                else str(response.status),
                is_archived=response.is_archived,
                account_source=response.account_source.value
                if response.account_source and hasattr(response.account_source, "value")
                else (str(response.account_source) if response.account_source else None),
            )

    async def _async_bind_portfolio(
        self,
        account_id: str,
        portfolio_id: str,
        execution_mode: str = "signal_priority",
    ) -> BindingInfo:
        """Async implementation of bind_portfolio."""
        from tradepose_client import TradePoseClient
        from tradepose_client.resources.bindings import BindingCreateRequest

        async with TradePoseClient(api_key=self._api_key, server_url=self._server_url) as client:
            request = BindingCreateRequest(
                account_id=account_id,
                portfolio_id=portfolio_id,
                execution_mode=execution_mode,
            )
            response = await client.bindings.create(request)
            return BindingInfo(
                id=str(response.id),
                account_id=str(response.account_id),
                portfolio_id=str(response.portfolio_id),
                execution_mode=response.execution_mode,
                is_active=response.is_active,
            )

    async def _async_bind_slot(
        self,
        node_seq: int,
        slot_idx: int,
        account_id: str,
    ) -> SlotInfo:
        """Async implementation of bind_slot."""
        from tradepose_client import TradePoseClient

        async with TradePoseClient(api_key=self._api_key, server_url=self._server_url) as client:
            response = await client.slots.bind(node_seq, slot_idx, account_id)
            return SlotInfo(
                id=response.id,
                node_seq=response.node_seq,
                slot_idx=response.slot_idx,
                account_id=response.account_id,
                bound_at=response.bound_at,
            )

    async def _async_unbind_slot(
        self,
        node_seq: int,
        slot_idx: int,
    ) -> SlotInfo:
        """Async implementation of unbind_slot."""
        from tradepose_client import TradePoseClient

        async with TradePoseClient(api_key=self._api_key, server_url=self._server_url) as client:
            response = await client.slots.unbind(node_seq, slot_idx)
            return SlotInfo(
                id=response.id,
                node_seq=response.node_seq,
                slot_idx=response.slot_idx,
                account_id=response.account_id,
                bound_at=response.bound_at,
            )

    # =========================================================================
    # INTERNAL: ASYNC RUNNER (from BatchTester pattern)
    # =========================================================================

    def _run_async(self, coro) -> Any:
        """Run async coroutine in a separate thread.

        Uses threading to avoid blocking the main thread and to handle
        cases where an event loop is already running (e.g., Jupyter).

        Args:
            coro: Coroutine to execute

        Returns:
            Result from the coroutine
        """
        result_container = []
        exception_container = []

        def _run_in_thread():
            loop = asyncio.new_event_loop()
            asyncio.set_event_loop(loop)
            try:
                result = loop.run_until_complete(coro)
                result_container.append(result)
            except Exception as e:
                exception_container.append(e)
            finally:
                loop.close()

        thread = threading.Thread(target=_run_in_thread)
        thread.start()
        thread.join()

        if exception_container:
            raise exception_container[0]

        return result_container[0] if result_container else None
