"""Provider Protocol interfaces for EasyCat.

All providers are defined as typing.Protocol classes so that implementations
use structural subtyping (duck typing) rather than requiring inheritance.

Providers produce provider-scoped events (STTEvent, TTSEvent) via async
iterators. The Session is the single place that maps these to EasyCat events.
"""

from __future__ import annotations

from collections.abc import AsyncIterator
from typing import TYPE_CHECKING, Protocol, runtime_checkable

from easycat.audio_format import AudioChunk
from easycat.events import Event, EventBus, STTEvent, TTSEvent

if TYPE_CHECKING:
    from easycat.tts.input import TTSInput, TTSInputPolicy

# ── Versioned Provider ─────────────────────────────────────────────


@runtime_checkable
class VersionedProvider(Protocol):
    """Shared contract for providers that report their version info.

    The framework's journal records a ``provider_versions`` event by calling
    ``version_info()`` on every wired provider. Implementing this method is a
    de-facto part of the provider contract; mixing this Protocol into each
    provider interface makes that dependency explicit and type-checkable.

    Field conventions
    -----------------
    The returned mapping may include extra fields, but the installable
    provider contract suite requires the following four non-empty keys so
    postmortem diagnosis stays legible across providers. Use ``"unknown"``
    when a field does not apply:

    - ``provider``: stable short name of the provider (e.g. ``"openai"``).
    - ``model``: the model that actually produced the output for this
      session. For providers that connect with a separate "connection"
      model distinct from the transcription/generation model, also report
      that under ``connection_model`` rather than dropping it.
    - ``api_version``: the provider HTTP/WS API version (e.g. ``"v1"``).
    - ``sdk_version``: the version of the transport library the *active
      mode* actually uses — ``"websockets"`` when the provider streams over
      a WebSocket, ``"httpx"`` when it issues HTTP requests. Providers with
      a runtime mode switch must mirror the branch they will actually take.
    """

    def version_info(self) -> dict[str, str]:
        """Return a mapping of version metadata for this provider."""
        ...


# ── Optional provider capabilities ─────────────────────────────────


@runtime_checkable
class EventBusBindable(Protocol):
    """Optional capability for a live provider instance to receive the session bus.

    Session calls this synchronous hook during construction before any provider
    work starts. Implementations should retain an explicitly configured bus and
    otherwise store the supplied session bus.
    """

    def set_event_bus(self, event_bus: EventBus) -> None:
        """Attach the session event bus used for provider-scoped events."""
        ...


# ── STT Provider ───────────────────────────────────────────────────


@runtime_checkable
class STTProvider(VersionedProvider, Protocol):
    """Speech-to-text provider interface.

    Providers stream audio in via `send_audio` and produce `STTEvent` objects
    via the `events()` async iterator. Session consumes these and emits
    EasyCat-level STTPartial/STTFinal events. Providers never emit EasyCat
    events directly.

    Optional teardown hook
    ----------------------
    Session invokes an ``async def aclose(self) -> None`` (or sync/async
    ``close``) on the provider during ``stop()``/``close`` via
    ``runtime.capabilities.close_if_supported``. It is *not* a required
    member of this Protocol (so stubs like ``NoopSTT`` stay conformant and
    ``isinstance`` checks keep working), but providers that hold a socket
    or an ``httpx`` client should implement it to release those resources
    on session teardown rather than relying on garbage collection.
    """

    async def start_stream(self) -> None:
        """Begin a new STT stream session."""
        ...

    async def send_audio(self, chunk: AudioChunk) -> None:
        """Send an audio chunk to the active STT stream."""
        ...

    async def commit_segment(self) -> bool:
        """Finalize the current STT segment without ending the stream.

        Returns ``True`` when the provider accepted a segment commit request.
        Acceptance does not guarantee a subsequent ``FINAL``: providers may
        suppress empty/silent segments or receive an empty backend response.
        Consumers must use the :meth:`events` stream as the source of
        transcript completion. Providers that do not support segmented
        commits return ``False``.
        """
        ...

    async def end_stream(self) -> None:
        """Signal that no more audio will be sent for the current stream."""
        ...

    def events(self) -> AsyncIterator[STTEvent]:
        """Return a fresh async iterator of provider-scoped STT events.

        Every call must return a new iterator for the current stream. It must
        terminate after :meth:`end_stream`; Session calls ``events()`` again
        on the next turn and an exhausted cached iterator would silently lose
        all later transcripts.
        """
        ...


@runtime_checkable
class PendingCommitReporter(Protocol):
    """Optional STT-provider capability: report uncommitted audio size.

    Providers that can measure how much audio is buffered but not yet
    committed implement ``pending_commit_bytes()`` so the journal can
    record *why* a segment commit was accepted or skipped. This is an
    opt-in capability, not part of the core :class:`STTProvider` contract;
    providers that cannot report it are simply not instances of this
    Protocol and the committer records ``None``.
    """

    def pending_commit_bytes(self) -> int | None:
        """Return bytes buffered since the last successful commit, if known."""
        ...


# ── TTS Provider ───────────────────────────────────────────────────


@runtime_checkable
class TTSProvider(VersionedProvider, Protocol):
    """Text-to-speech provider interface.

    Call `synthesize` with text to get an async iterator of TTSEvent objects.
    Session maps these to EasyCat-level TTSAudio/TTSMarkers events. Note that
    ``TTSMarkers`` is a best-effort, debug-only event carrying the provider's
    *native* alignment shape (not a normalized cross-provider schema); see
    :class:`~easycat.events.TTSMarkers` for the contract.

    Optional teardown hook
    ----------------------
    Session invokes an ``async def aclose(self) -> None`` (or sync/async
    ``close``) on the provider during ``stop()``/``close`` via
    ``runtime.capabilities.close_if_supported``. It is *not* a required
    member of this Protocol (so stubs like ``NoopTTS`` stay conformant and
    ``isinstance`` checks keep working), but providers that hold a socket
    or an ``httpx`` client (e.g. ``OpenAITTS``, ``ElevenLabsTTS``) should
    implement it to release those resources on session teardown rather
    than relying on garbage collection.
    """

    def synthesize(self, payload: TTSInput | str) -> AsyncIterator[TTSEvent]:
        """Synthesize text into streaming TTSEvent objects."""
        ...

    async def stop(self) -> None:
        """Gracefully stop the current synthesis."""
        ...

    async def cancel(self) -> None:
        """Immediately cancel synthesis and discard pending output."""
        ...


@runtime_checkable
class TTSInputPolicyProvider(Protocol):
    """Optional TTS-provider capability: report typed input handling."""

    @property
    def input_policy(self) -> TTSInputPolicy:
        """Provider-facing input policy for TTS payload preparation."""
        ...


# ── VAD Provider ───────────────────────────────────────────────────


@runtime_checkable
class VADProvider(VersionedProvider, Protocol):
    """Voice activity detection provider interface.

    Process audio chunks and yield speech start/stop events.
    """

    def process(self, chunk: AudioChunk) -> AsyncIterator[Event]:
        """Process an audio chunk and yield any VAD events (start/stop speaking)."""
        ...

    def configure(
        self,
        *,
        min_speech_duration_ms: int = 250,
        min_silence_duration_ms: int = 150,
        sensitivity: float = 0.5,
    ) -> None:
        """Configure VAD thresholds."""
        ...


# ── Noise Reducer ──────────────────────────────────────────────────


@runtime_checkable
class NoiseReducer(VersionedProvider, Protocol):
    """Noise reduction provider interface.

    Processes an audio chunk and returns a cleaned version.
    """

    async def process(self, chunk: AudioChunk) -> AudioChunk:
        """Process an audio chunk and return a noise-reduced version."""
        ...


# ── Echo Canceller ────────────────────────────────────────────────


@runtime_checkable
class EchoCanceller(VersionedProvider, Protocol):
    """Echo cancellation provider interface.

    Processes near-end (mic) audio and accepts far-end (speaker) reference.
    """

    async def process(self, chunk: AudioChunk) -> AudioChunk:
        """Process a near-end audio chunk and return an echo-cancelled version."""
        ...

    def feed_reference(self, chunk: AudioChunk) -> None:
        """Feed a far-end audio chunk as the AEC reference signal."""
        ...


# ── Transport ──────────────────────────────────────────────────────


@runtime_checkable
class TransportLike(Protocol):
    """Narrow structural contract for an already-constructed transport.

    This mirrors :class:`Transport`'s audio/connection surface but deliberately
    omits :meth:`VersionedProvider.version_info`. It exists so that the identity
    discrimination in ``_create_transport`` (distinguishing a pre-built transport
    instance from a transport *config*) does not silently reject third-party
    transports that satisfy the audio contract but predate ``version_info()``.
    """

    async def connect(self) -> None:
        """Establish the transport connection."""
        ...

    async def disconnect(self) -> None:
        """Close the transport connection."""
        ...

    def receive_audio(self) -> AsyncIterator[AudioChunk]:
        """Return an async iterator that yields incoming audio chunks."""
        ...

    async def send_audio(self, chunk: AudioChunk) -> bool:
        """Send an audio chunk to the remote end."""
        ...


@runtime_checkable
class Transport(VersionedProvider, Protocol):
    """Audio transport interface for sending/receiving audio.

    Handles connection lifecycle and bidirectional audio streaming.
    """

    async def connect(self) -> None:
        """Establish the transport connection."""
        ...

    async def disconnect(self) -> None:
        """Close the transport connection."""
        ...

    def receive_audio(self) -> AsyncIterator[AudioChunk]:
        """Return an async iterator that yields incoming audio chunks."""
        ...

    async def send_audio(self, chunk: AudioChunk) -> bool:
        """Send an audio chunk to the remote end.

        Returns ``True`` when the chunk was accepted for delivery and
        ``False`` when it was silently dropped (transport disconnected,
        no active peer, etc.).
        """
        ...

    # Optional *playback-time* capabilities are intentionally not declared as
    # methods on this ``runtime_checkable`` protocol: only a few transports can
    # provide them, so requiring stubs everywhere would make
    # ``isinstance(x, Transport)`` reject otherwise-valid minimal transports
    # (including custom out-of-tree ones). The Session discovers them
    # structurally via :mod:`easycat.runtime.capabilities`, which treats a
    # missing hook as a strict no-op:
    #
    # - ``clear_audio() -> None`` — discard queued outbound audio during
    #   barge-in; transports without outbound buffering omit it entirely.
    # - ``pending_playout_ms() -> float`` — milliseconds of audio still queued
    #   for local speaker playout, so ``await_drain`` waits for real playout.
    # - ``drain_aec_reference_frames() -> list[AudioChunk]`` — typed far-end
    #   (speaker) reference frames captured at playback time and fed to the echo
    #   canceller before the matching near-end mic frame (LocalTransport +
    #   WebRTC). The runtime still accepts legacy raw-byte frames.
    # - ``send_playback_mark(...)`` — see :class:`PlaybackAcknowledgements`.
    # - ``send_audio_is_nonblocking: bool`` — opt in only when every
    #   ``send_audio`` path completes without suspending. AudioRouter combines
    #   this with deferred delivery reporting before preserving first-frame
    #   ownership in the caller task; missing/false stays conservative.
