# #region Core.AsyncJobRunner [C:4] [TYPE Module] [SEMANTICS async,scheduler,bridge,event-loop]
# @defgroup Core Async/sync bridge for scheduled jobs.
# @BRIEF Centralized bridge between APScheduler (sync thread pool) and async coroutines.
# @LAYER Core
# @PRE Event loop is running (FastAPI lifespan).
# @POST All scheduled async jobs execute through this runner.
# @SIDE_EFFECT Submits coroutines to the event loop from sync threads.
# @RATIONALE APScheduler BackgroundScheduler runs callbacks in a sync thread pool.
#            Async coroutines must be dispatched to the FastAPI event loop via
#            asyncio.run_coroutine_threadsafe(). This runner centralizes that pattern.
# @REJECTED asyncio.run() — creates a new event loop each call; breaks when called
#            from a context with an existing loop; incompatible with AsyncIOScheduler migration.
# @REJECTED Direct async def callbacks in BackgroundScheduler — coroutine is created
#            but never awaited, causing silent no-ops (the root cause of scheduled translation failures).

import asyncio
from typing import Any


# #region AsyncJobRunner [C:4] [TYPE Class] [SEMANTICS async,runner,scheduler]
# @ingroup Core
# @BRIEF Centralized async/sync bridge for dispatching coroutines from sync threads.
# @PRE Event loop is captured at construction time.
# @POST run() executes coroutines synchronously (blocking until complete).
# @POST run_later() schedules delayed coroutine execution.
# @SIDE_EFFECT Submits coroutines to the captured event loop.
# @DATA_CONTRACT Input[coroutine] -> Output[result]
class AsyncJobRunner:
    """Bridge between APScheduler sync threads and the FastAPI async event loop.

    Usage:
        runner = AsyncJobRunner(asyncio.get_running_loop())
        result = runner.run(some_async_function())
    """

    # #region __init__ [TYPE Function]
    # @BRIEF Capture the event loop for later coroutine dispatch.
    # @PRE loop is a running asyncio event loop.
    # @POST self.loop is set and ready for run()/run_later() calls.
    def __init__(self, loop: asyncio.AbstractEventLoop) -> None:
        self.loop = loop
        # Track pending tasks from run_later() to prevent silent exception loss
        self._pending_tasks: set[asyncio.Task] = set()
    # #endregion __init__

    # #region run [TYPE Function]
    # @BRIEF Execute a coroutine from a sync thread and wait for the result.
    # @PRE coro is an awaitable (coroutine or Future).
    # @POST Coroutine has been executed to completion; result or exception is returned.
    # @SIDE_EFFECT Blocks the calling thread until the coroutine completes.
    def run(self, coro: Any) -> Any:
        """Dispatch a coroutine to the event loop and block until it completes.

        Args:
            coro: An awaitable (coroutine, Future, or Task).

        Returns:
            The result of the coroutine.

        Raises:
            Exception: Any exception raised by the coroutine is re-raised.
        """
        future = asyncio.run_coroutine_threadsafe(coro, self.loop)
        return future.result()  # timeout=None — wait indefinitely
    # #endregion run

    # #region run_later [TYPE Function]
    # @BRIEF Schedule a coroutine to execute after a delay (for windowed validation).
    # @PRE coro is an awaitable; delay_seconds >= 0.
    # @POST Coroutine is scheduled on the event loop's call_later queue.
    # @SIDE_EFFECT Non-blocking — returns immediately; coroutine executes later.
    def run_later(self, coro: Any, delay_seconds: float) -> None:
        """Schedule a coroutine to execute after a delay.

        Used for windowed validation: tasks are distributed across a time window
        using call_later() instead of running them all at once.

        Args:
            coro: An awaitable to execute after the delay.
            delay_seconds: Seconds to wait before scheduling the coroutine.
        """
        def _schedule() -> None:
            task = asyncio.ensure_future(coro, loop=self.loop)
            # Store task for exception tracking; remove when done
            self._pending_tasks.add(task)
            task.add_done_callback(self._pending_tasks.discard)
        # Use call_soon_threadsafe to schedule from any thread
        self.loop.call_soon_threadsafe(
            lambda: self.loop.call_later(delay_seconds, _schedule)
        )
    # #endregion run_later
# #endregion AsyncJobRunner
# #endregion Core.AsyncJobRunner
