"""``mgf.common.progress.asyncio`` — async sibling of the sync progress module.

Per the sync-first / async-adapter rule
(``CLOSED_BOX_REDESIGN.md`` §14.1): every new module ships sync;
if async semantics are real, the async sibling lands at
``<area>.asyncio`` as a thin shim — never a full duplicate API.

This sibling adds :class:`AsyncCancellationToken` — async-native
cancellation backed by :class:`asyncio.Event` instead of
:class:`threading.Event`. The sync API (``cancel`` /
``is_cancelled``) is preserved so it satisfies
:class:`mgf.common.typing.CancellationProtocol`; ``await wait()``
is the async addition.

Threading model
---------------
``AsyncCancellationToken`` is an asyncio primitive. Cross-thread
use is NOT supported — :class:`asyncio.Event` is bound to a
single event loop. For cross-thread cancellation, use the sync
:class:`mgf.common.progress.CancellationToken` and bridge to
asyncio yourself (``asyncio.to_thread`` + a sentinel queue, or
:meth:`asyncio.Event.set` from a thread via
``loop.call_soon_threadsafe``).

Example::

    import asyncio
    from mgf.common.progress.asyncio import AsyncCancellationToken

    async def long_op(token: AsyncCancellationToken):
        for i in range(100):
            if token.is_cancelled():
                raise CancellationError(f"cancelled at iteration {i}")
            await do_async_work()
"""

from __future__ import annotations

import asyncio

from mgf.common.exceptions import CancellationError
from mgf.common.typing import CancellationProtocol  # noqa: TC001 — runtime ref

__all__ = ["AsyncCancellationToken"]


class AsyncCancellationToken:
    """asyncio-native cancellation token.

    Mirrors the sync :class:`mgf.common.progress.CancellationToken`
    surface but ``await wait(timeout)`` instead of blocking. Used
    by async I/O code (future ``mgf.common.ssh.asyncio``, async
    HTTP clients, etc.) so cancellation crosses the same
    abstraction whether the caller is sync or async.

    Implements :class:`mgf.common.typing.CancellationProtocol`.

    Threading: bound to a single asyncio event loop. NOT
    thread-safe across loops.
    """

    __slots__ = ("_event",)

    def __init__(self) -> None:
        self._event = asyncio.Event()

    def cancel(self) -> None:
        """Signal cancellation. Idempotent. Safe to call from a
        synchronous callback inside the event-loop thread."""
        self._event.set()

    def is_cancelled(self) -> bool:
        """Cheap read for inner-loop check (sync; no await needed)."""
        return self._event.is_set()

    async def wait(
        self,
        timeout: float | None = None,  # noqa: ASYNC109 — timeout matches sync sibling
    ) -> bool:
        """Async block until cancelled or timeout.

        Returns ``True`` if cancelled, ``False`` if the timeout
        expired without cancellation. ``timeout=None`` waits
        indefinitely.

        Implementation note: when ``timeout is None`` we use
        :meth:`asyncio.Event.wait` directly (cheap); when a
        timeout is given we wrap in :func:`asyncio.wait_for`
        which raises :class:`asyncio.TimeoutError` on expiry.
        We catch that and return ``False`` so the surface
        matches the sync token's ``wait`` shape.
        """
        if timeout is None:
            await self._event.wait()
            return True
        try:
            await asyncio.wait_for(self._event.wait(), timeout=timeout)
        except TimeoutError:
            return False
        return True

    def raise_if_cancelled(self, message: str | None = None) -> None:
        """Raise :class:`mgf.common.exceptions.CancellationError`
        if cancelled.

        Synchronous helper — does NOT need to be awaited. Use
        in inner loops between ``await`` points::

            async def long_op(token):
                for item in items:
                    token.raise_if_cancelled()
                    await do_async(item)
        """
        if self._event.is_set():
            raise CancellationError(message or "operation cancelled by user")

    def __repr__(self) -> str:
        state = "cancelled" if self._event.is_set() else "active"
        return f"AsyncCancellationToken({state})"


# Static check at import time: AsyncCancellationToken satisfies
# the protocol. If a future refactor accidentally drops a method,
# this fails at import (cheap CI signal).
_PROTOCOL_CHECK: type[CancellationProtocol] = AsyncCancellationToken
