"""
Utility to run async code in both sync and async contexts.
"""

import asyncio
import threading
from typing import Coroutine, Any, TypeVar

T = TypeVar('T')


def _run_in_thread(coro: Coroutine[Any, Any, T]) -> T:
    """
    Run async code in a new thread with a new event loop.
    This is used when we're already in an async context.
    """
    result = None
    exception = None
    
    def run_in_new_loop():
        nonlocal result, exception
        # Create a new event loop in this thread
        new_loop = asyncio.new_event_loop()
        asyncio.set_event_loop(new_loop)
        try:
            result = new_loop.run_until_complete(coro)
        except Exception as e:
            exception = e
        finally:
            new_loop.close()
    
    thread = threading.Thread(target=run_in_new_loop)
    thread.start()
    thread.join()
    
    if exception:
        raise exception
    
    return result


def run_async(coro: Coroutine[Any, Any, T]) -> T:
    """
    Run async code in both sync and async contexts.
    
    This function detects if it's being called from an async context:
    - If called from sync code: Uses asyncio.run()
    - If called from async code: Uses a new thread with a new event loop to avoid conflicts
    
    Args:
        coro: The coroutine to run
        
    Returns:
        The result of the coroutine
    """
    try:
        # Try to get the running event loop
        loop = asyncio.get_running_loop()
        # If we get here, we're in an async context
        # Use a new thread with a new event loop
        return _run_in_thread(coro)
    except RuntimeError:
        # No running event loop, safe to use asyncio.run()
        try:
            return asyncio.run(coro)
        except RuntimeError as e:
            # If asyncio.run() fails (e.g., nested call), use thread approach
            if "cannot be called from a running event loop" in str(e):
                return _run_in_thread(coro)
            raise

