"""``mgf.common.typing`` — :class:`Protocol` types for stable abstractions.

See: ``docs/reference/lifecycle.md`` §"`mgf.common.typing` — the
four Protocols" (consumers meet these through the lifecycle path).

Closes FEEDBACK API-05. Every core concept that a consumer
might want to mock, swap, or duck-type lives here as a
:class:`typing.Protocol`. The library's own concrete classes
(:class:`mgf.common.progress.CancellationToken`, future
``KeyringVault``, etc.) implement these protocols; consumer code
that only needs the contract (test doubles, third-party
implementations) can pass any duck-typed object that satisfies
the protocol.

The protocols are :func:`runtime_checkable` so ``isinstance(obj,
SomeProtocol)`` works in tests + audit code. Note that
``runtime_checkable`` only verifies attribute presence — not
signatures — so it is a smoke-level check, not a contract proof.

Why a single ``mgf.common.typing`` module instead of
per-domain protocols (e.g., ``mgf.common.progress.ProgressReporter``):

  - One canonical definition site avoids drift between
    "what the protocol means" and "what the protocol declares".
  - One import path for consumers who type-annotate against
    multiple abstractions (``def f(token: CancellationProtocol,
    reporter: ReporterProtocol) -> ...``).
  - The per-domain modules re-export ergonomic aliases (e.g.,
    ``mgf.common.progress.ProgressReporter`` IS
    ``mgf.common.typing.ReporterProtocol``).

See ``docs/CLOSED_BOX_REDESIGN.md`` §4.9 for the full design.
"""

from __future__ import annotations

from typing import Protocol, runtime_checkable


@runtime_checkable
class CancellationProtocol(Protocol):
    """Cooperative cancellation primitive — duck-typed contract.

    Concrete implementation: :class:`mgf.common.progress.CancellationToken`
    (sync) and :class:`mgf.common.progress.asyncio.AsyncCancellationToken`
    (async). Consumers who want to inject their own cancellation
    mechanism (a custom callback chain, an integration with an
    external scheduler) implement this protocol and pass the
    instance everywhere ``CancellationProtocol`` is expected.

    Surface kept minimal so this protocol works for both sync and
    async implementations + for callback-only impls that can't
    block. Concrete classes ALSO carry a ``wait()`` method (sync
    blocking / async ``await``); ``wait()`` is intentionally NOT
    on the protocol because not every implementation can offer it.

    Threading: implementations MUST be thread-safe (or document
    otherwise). The standard sync ``CancellationToken`` uses
    :class:`threading.Event`.
    """

    def cancel(self) -> None:
        """Signal cancellation. Idempotent."""

    def is_cancelled(self) -> bool:
        """Cheap read for inner-loop check."""


@runtime_checkable
class ReporterProtocol(Protocol):
    """Progress reporter — duck-typed contract.

    Long-running operations accept a ``ReporterProtocol`` so the
    same operation can drive a CLI progress bar, a Qt progress
    dialog, a unit-test recorder, or a no-op (when progress
    isn't relevant).

    Concrete implementations:
      - :class:`mgf.common.progress.NullReporter` — no-op for
        callers that don't care about progress.
      - :class:`mgf.common.progress.RecordingReporter` — test
        double; accumulates report() calls for assertions.

    The protocol bundles cancellation polling (``is_cancelled``)
    because every reporter call site is also a natural cancellation
    check point — the inner loop calls ``reporter.report(...)``
    and ``if reporter.is_cancelled(): break`` together. Consumers
    that don't combine the two can implement
    :class:`CancellationProtocol` separately and pass both.
    """

    def report(self, message: str, current: int = 0, total: int = 0) -> None:
        """Emit a progress update.

        ``current`` / ``total`` are optional — consumers that
        report by message only pass the defaults. When both are
        provided, the reporter MAY render a percentage bar.
        """

    def is_cancelled(self) -> bool:
        """Forward to the underlying cancellation token (if any)."""


@runtime_checkable
class LogSinkProtocol(Protocol):
    """Append-style log sink — duck-typed contract.

    The shape every GUI dock / log-panel widget implements so
    cross-thread workers can post log records into the UI without
    caring whether it's a Qt ``QTextEdit``, a Tkinter ``Text``,
    or a Flet widget.

    The future ``mgf.common.gui.qt`` helpers (Phase beyond
    current scope) provide a Qt-native concrete implementation;
    consumers in non-Qt frameworks implement this protocol
    against their own widget.
    """

    def append_info(self, message: str) -> None:
        """Append a normal-level log message."""

    def append_error(self, message: str) -> None:
        """Append an error-level log message."""


@runtime_checkable
class VaultProtocol(Protocol):
    """Credential vault — duck-typed contract.

    Concrete implementations land in ``mgf.common.vault`` (Phase 5):

      - ``KeyringVault`` — system keyring (libsecret / Keychain /
        Credential Manager via ``keyring`` or ``secretstorage``).
      - ``EncryptedFileVault`` — Fernet-encrypted JSON file
        fallback.
      - ``EnvVault`` — read from ``<APP>_VAULT_<KEY>`` env vars
        (CI-friendly).

    Consumers who already have a credential store (HashiCorp Vault,
    AWS Secrets Manager, custom HSM) implement this protocol
    against their store and pass the instance through; the rest of
    the ``mgf-common`` surface (future SSH client, etc.) accepts
    ``VaultProtocol``.

    Threading: implementations MUST be thread-safe (the standard
    backends are lock-protected at the file / keyring level).
    """

    def get(self, key: str) -> str | None:
        """Return the secret for ``key``, or ``None`` if absent."""

    def set(self, key: str, value: str) -> None:
        """Store ``value`` under ``key``. Idempotent."""

    def delete(self, key: str) -> None:
        """Remove the entry for ``key``. No-op if absent."""

    def list_keys(self) -> list[str]:
        """Return every stored key. SHOULD be sorted for determinism."""

    @property
    def backend(self) -> str:
        """Backend identifier (``"keyring"`` / ``"file"`` / ``"env"`` /
        consumer-provided name). Logged at startup per rule DP-10
        (no silent fallback)."""


__all__ = [
    "CancellationProtocol",
    "LogSinkProtocol",
    "ReporterProtocol",
    "VaultProtocol",
]
