"""Cancellation primitive per ``contracts.md`` §9 + ``mgf-common`` DP-09.

Every long-running adapter method takes a ``CancellationToken``. The
token is created by the caller (the SDK or the CP reconciler);
adapters check it at every reasonable yield point and abort cleanly
on cancellation.

Across the network, the token maps to gRPC ``context`` cancellation
(``contracts.md`` §9, D-D7). The Protocol below is what adapter
methods see locally; the SDK ``GrpcTransport`` is what bridges the
network ``context`` cancel into a local token.

A simple default implementation (``CancellationEvent``) ships
alongside the Protocol for tests, fakes, and consumers that don't
need a custom token type.
"""

from __future__ import annotations

import threading
from typing import Protocol, runtime_checkable


@runtime_checkable
class CancellationToken(Protocol):
    """Per ``contracts.md`` §9.

    Implementations MUST be thread-safe — multiple worker threads
    may inspect the same token concurrently.
    """

    @property
    def cancelled(self) -> bool:
        """``True`` once cancellation has been signalled. One-way; never
        un-set."""
        ...

    def raise_if_cancelled(self) -> None:
        """Convenience: raise ``OperationCancelled`` if ``cancelled``.

        Adapter methods call this between sub-operations so they don't
        have to write the ``if token.cancelled: raise OperationCancelled``
        scaffold everywhere.
        """
        ...

    def wait(self, timeout: float | None = None) -> bool:
        """Block until cancelled or ``timeout`` elapses.

        Returns ``True`` if cancelled, ``False`` on timeout. Useful for
        poll-loops with a bounded wait between checks.
        """
        ...


class CancellationEvent:
    """Default ``CancellationToken`` implementation backed by ``threading.Event``.

    Sufficient for tests, fakes, and any caller that doesn't need a
    custom token. Implements the ``CancellationToken`` Protocol
    structurally.
    """

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

    @property
    def cancelled(self) -> bool:
        return self._event.is_set()

    def cancel(self) -> None:
        """Signal cancellation. One-way; idempotent."""
        self._event.set()

    def raise_if_cancelled(self) -> None:
        if self._event.is_set():
            # Local import avoids the circular import — exceptions
            # might import from cancellation in the future.
            from mgf.vm.core.exceptions import OperationCancelled

            raise OperationCancelled("operation cancelled by caller")

    def wait(self, timeout: float | None = None) -> bool:
        return self._event.wait(timeout=timeout)


__all__ = ["CancellationEvent", "CancellationToken"]
