import asyncio
import threading
from typing import Any, Coroutine
import weakref
import contextlib
import atexit


class AsyncExecutor:
    """
    Thread-safe executor for running asyncio coroutines in any thread.

    Each thread gets its own event loop, managed independently.
    """

    def __init__(self):
        self._local = threading.local()
        self._cleaners = []
        self._lock = threading.Lock()

        atexit.register(self._cleanup_all)

    def _get_loop(self) -> asyncio.AbstractEventLoop:
        """Get or create an event loop for the current thread"""
        if not hasattr(self._local, "loop") or self._local.loop.is_closed():
            loop = asyncio.new_event_loop()
            asyncio.set_event_loop(loop)
            self._local.loop = loop

            # Register automatic cleanup when thread exits
            self._register_thread_cleanup(loop)

        return self._local.loop

    def _register_thread_cleanup(self, loop: asyncio.AbstractEventLoop):
        """Register cleanup to run when thread exits"""

        def cleanup():
            with contextlib.suppress(RuntimeError, Exception):
                if not loop.is_closed():
                    # Cancel all tasks
                    for task in asyncio.all_tasks(loop):
                        task.cancel()

                    # Run loop briefly to let tasks cancel
                    if not loop.is_running() and not loop.is_closed():
                        try:
                            loop.run_until_complete(asyncio.sleep(0))
                        except (RuntimeError, asyncio.CancelledError):
                            pass

                    loop.close()

        thread = threading.current_thread()
        finalizer = weakref.finalize(thread, cleanup)

        with self._lock:
            self._cleaners = [f for f in self._cleaners if f.alive]
            self._cleaners.append(finalizer)

    def run(self, coro: Coroutine) -> Any:
        """
        Run coroutine in the current thread's event loop.

        Args:
            coro: Coroutine to execute

        Returns:
            Coroutine result

        Raises:
            RuntimeError: If event loop is in invalid state
        """
        loop = self._get_loop()

        if loop.is_closed():
            raise RuntimeError("Event loop is closed")

        # If loop is already running, use thread-safe execution
        if loop.is_running():
            future = asyncio.run_coroutine_threadsafe(coro, loop)
            try:
                return future.result()
            finally:
                pass
        else:
            # Run in current thread
            try:
                return loop.run_until_complete(coro)
            finally:
                # Don't close the loop here - it might be reused
                pass

    def clear_current_thread(self):
        """Clean up the event loop for the current thread"""
        if hasattr(self._local, "loop"):
            loop = self._local.loop
            if not loop.is_closed():
                # Cancel all tasks
                for task in asyncio.all_tasks(loop):
                    task.cancel()

                # Let tasks cancel
                if not loop.is_running():
                    with contextlib.suppress(asyncio.CancelledError, RuntimeError):
                        loop.run_until_complete(asyncio.sleep(0))

                loop.close()

            delattr(self._local, "loop")

    def _cleanup_all(self):
        """Clean up all loops (called at program exit)"""
        if hasattr(self._local, "loop"):
            self.clear_current_thread()

        with self._lock:
            for finalizer in self._cleaners:
                if finalizer.alive:
                    finalizer()
            self._cleaners.clear()
