import asyncio
import inspect
from typing import TypeVar, Callable

T = TypeVar('T')


def sync_wrapper(async_func: Callable[..., T]) -> Callable[..., T]:
    """
    Decorator to create a synchronous wrapper for an async method.
    Works in Jupyter, standard Python, and nested event loops.
    """

    def wrapper(*args, **kwargs):
        coro = async_func(*args, **kwargs)

        try:
            # Try to get the current event loop
            loop = asyncio.get_running_loop()
        except RuntimeError:
            # No event loop running - create one and run
            return asyncio.run(coro)

        # Event loop is already running (e.g., in Jupyter)
        # We need to use nest_asyncio or run in a new thread
        try:
            import nest_asyncio
            nest_asyncio.apply()
            return loop.run_until_complete(coro)
        except ImportError:
            # If nest_asyncio not available, use a thread with a new loop
            import concurrent.futures
            with concurrent.futures.ThreadPoolExecutor() as executor:
                future = executor.submit(asyncio.run, coro)
                return future.result()

    return wrapper


class MyAsyncClass:
    """Example class with async methods."""

    async def fetch_data_async(self, id: int) -> dict:
        """Async method that fetches data."""
        await asyncio.sleep(0.1)  # Simulate I/O
        return {"id": id, "data": f"Data for {id}"}

    async def process_async(self, value: str) -> str:
        """Another async method."""
        await asyncio.sleep(0.05)
        return value.upper()

    # Synchronous wrappers
    @sync_wrapper
    async def fetch_data(self, id: int) -> dict:
        """Synchronous wrapper for fetch_data_async."""
        return await self.fetch_data_async(id)

    @sync_wrapper
    async def process(self, value: str) -> str:
        """Synchronous wrapper for process_async."""
        return await self.process_async(value)


# Usage example
if __name__ == "__main__":
    obj = MyAsyncClass()

    # Call synchronously - works in regular Python
    result = obj.fetch_data(42)
    print(result)  # {'id': 42, 'data': 'Data for 42'}

    processed = obj.process("hello")
    print(processed)  # 'HELLO'


    # The async versions still work if you want them
    async def async_usage():
        result = await obj.fetch_data_async(99)
        print(result)


    asyncio.run(async_usage())


#  more
class MyClass:
    async def fetch_data_async(self, id: int) -> dict:
        await asyncio.sleep(0.1)
        return {"id": id, "data": f"Data for {id}"}

    def fetch_data(self, id: int) -> dict:
        """Sync version - delegates to async."""
        return sync_wrapper(self.fetch_data_async)(id)


# and more
import asyncio
from typing import Any

def _run_sync(coro):
    """
    Run an async coroutine from synchronous code in a way that works:
      - normally via asyncio.run
      - in environments with a running loop (e.g. Jupyter) via nest_asyncio
    """
    try:
        # Preferred, works when no loop is running
        return asyncio.run(coro)
    except RuntimeError:
        # Probably "asyncio.run() cannot be called from a running event loop"
        # Try to patch the running loop so we can run nested run_until_complete
        try:
            import nest_asyncio  # pip install nest_asyncio
        except ImportError as exc:
            raise RuntimeError(
                "An event loop is already running (e.g. in Jupyter). "
                "Install nest_asyncio (`pip install nest_asyncio`) or run the coroutine "
                "from async code. Original error: " + str(exc)
            ) from None

        nest_asyncio.apply()
        loop = asyncio.get_event_loop()
        return loop.run_until_complete(coro)


class AsyncClient:
    async def async_compute(self, x: int, y: int, delay: float = 0.1) -> int:
        # Example async work (I/O, network, etc.)
        await asyncio.sleep(delay)
        return x + y

    def compute(self, x: int, y: int, delay: float = 0.1) -> int:
        """
        Synchronous wrapper around `async_compute`.
        Call this from regular (non-async) code.
        """
        return _run_sync(self.async_compute(x, y, delay))


if __name__ == "__main__":
    c = AsyncClient()
    print("Result:", c.compute(2, 3))