"""Abstract Radio Protocol — multi-backend radio control interface.

Defines :class:`Radio` (core) and optional capability Protocols
(:class:`AudioCapable`, :class:`ScopeCapable`, :class:`DualReceiverCapable`)
so that the Web UI, rigctld server, and CLI can work with **any** radio
backend — Icom LAN, Icom serial, Yaesu CAT, etc.

Quick start::

    from icom_lan.radio_protocol import Radio, AudioCapable

    async def tune(radio: Radio) -> None:
        await radio.set_freq(14_074_000)
        mode, filt = await radio.get_mode()
        print(f"Mode: {mode}, filter: {filt}")

    # Check optional capabilities at runtime:
    if isinstance(radio, AudioCapable):
        await radio.start_audio_rx(callback)

Architecture::

    ┌──────────────────────────────────────────────┐
    │          Web UI  /  rigctld  /  CLI           │
    ├──────────────────────────────────────────────┤
    │          Radio Protocol (core)                │
    │  ┌──────────────┬─────────────┬────────────┐ │
    │  │ AudioCapable │ ScopeCapable│ DualRxCap. │ │
    │  └──────────────┴─────────────┴────────────┘ │
    ├────────┬──────────┬──────────┬───────────────┤
    │IcomLAN │IcomSerial│ YaesuCAT │  Future...    │
    └────────┴──────────┴──────────┴───────────────┘

Standard mode names (cross-vendor):
    USB, LSB, CW, CWR, AM, FM, RTTY, RTTYR, PSK, PSKR, DV, DD

Standard capability tags:
    audio, scope, dual_rx, meters, tx, cw,
    attenuator, preamp, rf_gain, af_level, squelch, nb, nr
"""

from __future__ import annotations

from typing import Any, Awaitable, Callable, Protocol, runtime_checkable

from .radio_state import RadioState
from .types import Mode

__all__ = [
    "Radio",
    "AudioCapable",
    "CivCommandCapable",
    "ModeInfoCapable",
    "ScopeCapable",
    "DualReceiverCapable",
    "StateCacheCapable",
    "RecoverableConnection",
    "AdvancedControlCapable",
    "LevelsCapable",
    "MetersCapable",
    "PowerControlCapable",
    "StateNotifyCapable",
    "TransceiverStatusCapable",
]


# ---------------------------------------------------------------------------
# Core Protocol — every backend MUST implement this
# ---------------------------------------------------------------------------


@runtime_checkable
class Radio(Protocol):
    """Core interface for a controllable radio transceiver.

    Every radio backend must implement this protocol.  Optional features
    (audio streaming, spectrum scope, dual receivers) are expressed as
    separate protocols that a backend may additionally implement.
    """

    # -- Lifecycle ---------------------------------------------------------

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

    async def disconnect(self) -> None:
        """Disconnect from the radio, releasing all resources."""
        ...

    async def __aenter__(self) -> "Radio": ...
    async def __aexit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None: ...

    @property
    def connected(self) -> bool:
        """Whether the radio is currently connected and healthy."""
        ...

    @property
    def radio_ready(self) -> bool:
        """Whether the backend considers the CI-V stream ready for clients."""
        ...

    # -- Frequency ---------------------------------------------------------

    async def get_freq(self, receiver: int = 0) -> int:
        """Get the current frequency in Hz.

        Args:
            receiver: 0 = main (default), 1 = sub.
        """
        ...

    async def set_freq(self, freq: int, receiver: int = 0) -> None:
        """Set the frequency in Hz.

        Args:
            freq: Frequency in Hz (e.g. 14_074_000).
            receiver: 0 = main (default), 1 = sub.
        """
        ...

    # -- Mode --------------------------------------------------------------

    async def get_mode(self, receiver: int = 0) -> tuple[str, int | None]:
        """Get the current operating mode.

        Returns:
            Tuple of (mode_name, filter_number_or_None).
            Mode names are standardised strings: USB, LSB, CW, AM, FM, etc.
        """
        ...

    async def set_mode(
        self,
        mode: str,
        filter_width: int | None = None,
        receiver: int = 0,
    ) -> None:
        """Set the operating mode.

        Args:
            mode: Mode name string (e.g. "USB", "CW", "FT8").
            filter_width: Optional filter number (1-3 on Icom, varies by vendor).
            receiver: 0 = main, 1 = sub.
        """
        ...

    async def get_data_mode(self) -> bool:
        """Whether DATA mode is active (e.g. USB-D for FT8/FT4)."""
        ...

    async def set_data_mode(self, on: int | bool, receiver: int = 0) -> None:
        """Set receiver DATA mode.

        Args:
            on: False/0 to disable, True/1 to enable DATA1, or an explicit
                DATA mode value 0-3 when the radio supports multiple DATA
                sub-modes.
            receiver: 0 = main, 1 = sub.
        """
        ...

    # -- TX ----------------------------------------------------------------

    async def set_ptt(self, on: bool) -> None:
        """Key or unkey the transmitter."""
        ...

    # -- State -------------------------------------------------------------

    @property
    def radio_state(self) -> RadioState:
        """Live radio state (freq, mode, meters for all receivers)."""
        ...

    @property
    def model(self) -> str:
        """Human-readable radio model name (e.g. 'IC-7610', 'FTX-1')."""
        ...

    @property
    def capabilities(self) -> set[str]:
        """Set of capability tags this radio supports.

        Standard tags: audio, scope, dual_rx, meters, tx, cw,
        attenuator, preamp, rf_gain, af_level, squelch, nb, nr.
        """
        ...


# ---------------------------------------------------------------------------
# Optional capability Protocols
# ---------------------------------------------------------------------------

# --- Capabilities split from Radio (P1 slim) -------------------------------


@runtime_checkable
class LevelsCapable(Protocol):
    """Radio supports setting receiver levels: AF, RF gain, squelch."""

    async def set_af_level(self, level: int, receiver: int = 0) -> None:
        """Set AF (audio) output level (0-255).

        Args:
            level: Level in 0-255 scale.
            receiver: 0 = main (default), 1 = sub.
        """
        ...

    async def set_rf_gain(self, level: int, receiver: int = 0) -> None:
        """Set RF gain level (0-255).

        Args:
            level: Level in 0-255 scale.
            receiver: 0 = main (default), 1 = sub.
        """
        ...

    async def set_squelch(self, level: int, receiver: int = 0) -> None:
        """Set squelch level (0-255).

        Args:
            level: Level in 0-255 scale.
            receiver: 0 = main (default), 1 = sub.
        """
        ...

    async def get_nr_level(self, receiver: int = 0) -> int:
        """Get noise reduction level (0-255)."""
        ...

    async def set_nr_level(self, level: int, receiver: int = 0) -> None:
        """Set noise reduction level (0-255)."""
        ...

    async def get_nb_level(self, receiver: int = 0) -> int:
        """Get noise blanker level (0-255)."""
        ...

    async def set_nb_level(self, level: int, receiver: int = 0) -> None:
        """Set noise blanker level (0-255)."""
        ...

    async def get_mic_gain(self) -> int:
        """Get microphone gain level (0-255)."""
        ...

    async def set_mic_gain(self, level: int) -> None:
        """Set microphone gain level (0-255)."""
        ...

    async def get_drive_gain(self) -> int:
        """Get drive gain / TX power adjust level (0-255)."""
        ...

    async def set_drive_gain(self, level: int) -> None:
        """Set drive gain / TX power adjust level (0-255)."""
        ...

    async def get_compressor_level(self) -> int:
        """Get speech compressor level (0-255)."""
        ...

    async def set_compressor_level(self, level: int) -> None:
        """Set speech compressor level (0-255)."""
        ...


@runtime_checkable
class MetersCapable(Protocol):
    """Radio supports read-only meters: S-meter, SWR, TX power."""

    async def get_s_meter(self, receiver: int = 0) -> int:
        """Get S-meter reading (raw value, vendor-specific scale).

        Args:
            receiver: 0 = main (default), 1 = sub.
        """
        ...

    async def get_swr(self) -> float:
        """Get SWR reading (1.0 = perfect match)."""
        ...

    async def get_rf_power(self) -> int:
        """Get TX power level (0-255 normalised scale)."""
        ...

    async def get_comp_meter(self) -> int:
        """Get compression meter reading (0-255)."""
        ...

    async def get_id_meter(self) -> int:
        """Get drive (Id) meter reading (0-255)."""
        ...

    async def get_vd_meter(self) -> int:
        """Get Vd meter reading (0-255)."""
        ...


@runtime_checkable
class PowerControlCapable(Protocol):
    """Radio supports power on/off and TX power level control."""

    async def get_powerstat(self) -> bool:
        """Get the current radio power state.

        Returns:
            True if radio is powered on, False if powered off.
        """
        ...

    async def set_powerstat(self, on: bool) -> None:
        """Power the radio on or off.

        Args:
            on: True to power on, False to power off.
        """
        ...

    async def set_rf_power(self, level: int) -> None:
        """Set TX power level (0-255 normalised scale).

        Args:
            level: Power level in 0-255 scale.
        """
        ...


@runtime_checkable
class StateNotifyCapable(Protocol):
    """Radio supports server integration callbacks for state and reconnect."""

    def set_state_change_callback(
        self,
        callback: Callable[..., Any] | None,
    ) -> None:
        """Register a callback for radio state change notifications.

        Called by the web server to receive real-time updates.
        Pass ``None`` to unregister.
        """
        ...

    def set_reconnect_callback(
        self,
        callback: Callable[..., Any] | None,
    ) -> None:
        """Register a callback invoked after successful reconnection.

        Used by the web server to re-enable scope/audio after transport
        recovery.  Pass ``None`` to unregister.
        """
        ...


# ---------------------------------------------------------------------------
# Other optional capability Protocols
# ---------------------------------------------------------------------------


@runtime_checkable
class AudioCapable(Protocol):
    """Radio supports real-time audio streaming (LAN, USB, or virtual).

    Audio format is Opus-encoded.  For USB audio devices, the backend
    handles capture/playback internally and exposes the same interface.

    Preferred consumer pattern: use :attr:`audio_bus` for pub/sub access
    so multiple consumers can receive audio simultaneously::

        async with radio.audio_bus.subscribe(name="recorder") as sub:
            async for packet in sub:
                save(packet)
    """

    @property
    def audio_bus(self) -> Any:
        """AudioBus instance for pub/sub audio distribution.

        Returns an :class:`~icom_lan.audio_bus.AudioBus` that manages
        subscriptions and automatically starts/stops the radio's audio
        stream based on subscriber count.
        """
        ...

    async def start_audio_rx_opus(
        self,
        callback: Callable[..., Awaitable[None]],
    ) -> None:
        """Start receiving audio.  Decoded frames are passed to *callback*.

        Note: prefer :attr:`audio_bus` for multi-consumer scenarios.
        Direct callback usage is single-consumer only.
        """
        ...

    async def stop_audio_rx_opus(self) -> None:
        """Stop receiving audio."""
        ...

    async def push_audio_tx_opus(self, data: bytes) -> None:
        """Send Opus-encoded audio data for transmission."""
        ...

    async def start_audio_rx_pcm(
        self,
        callback: Callable[..., Any],
        *,
        sample_rate: int | None = None,
        channels: int | None = None,
        frame_ms: int | None = None,
    ) -> None: ...

    async def stop_audio_rx_pcm(self) -> None: ...

    async def start_audio_tx_pcm(
        self,
        *,
        sample_rate: int | None = None,
        channels: int | None = None,
        frame_ms: int | None = None,
    ) -> None: ...

    async def push_audio_tx_pcm(self, data: bytes) -> None: ...
    async def stop_audio_tx_pcm(self) -> None: ...
    async def get_audio_stats(self) -> dict[str, Any]: ...
    async def start_audio_tx_opus(self) -> None: ...
    async def stop_audio_tx(self) -> None: ...


@runtime_checkable
class CivCommandCapable(Protocol):
    """Radio exposes low-level CI-V command injection for background pollers."""

    async def send_civ(
        self,
        command: int,
        sub: int | None = None,
        data: bytes | None = None,
        *,
        wait_response: bool = True,
    ) -> Any:
        """Send a CI-V command through the active backend transport."""
        ...


@runtime_checkable
class ModeInfoCapable(Protocol):
    """Radio exposes backend-native mode metadata for rigctld consumers."""

    async def get_mode_info(self, receiver: int = 0) -> tuple[Mode, int | None]:
        """Return backend-native mode enum and filter selector."""
        ...


@runtime_checkable
class ScopeCapable(Protocol):
    """Radio supports a spectrum/panadapter scope data stream."""

    async def enable_scope(self, **kwargs: Any) -> None:
        """Enable the spectrum scope.

        Keyword arguments are backend-specific (e.g. span, center_freq).
        """
        ...

    async def disable_scope(self) -> None:
        """Disable the spectrum scope."""
        ...

    def on_scope_data(self, callback: Callable[..., Any] | None) -> None:
        """Register scope-frame callback; pass ``None`` to unregister."""
        ...

    async def set_scope_during_tx(self, on: bool) -> None: ...
    async def set_scope_center_type(self, center_type: int) -> None: ...
    async def set_scope_fixed_edge(
        self, *, edge: int, start_hz: int, end_hz: int
    ) -> None: ...
    async def capture_scope_frame(self, *, timeout: float = 10.0) -> Any: ...
    async def capture_scope_frames(
        self, *, count: int, timeout: float = 15.0
    ) -> list[Any]: ...


@runtime_checkable
class DualReceiverCapable(Protocol):
    """Radio has two independent receivers (e.g. IC-7610 Main/Sub)."""

    async def vfo_exchange(self) -> None:
        """Swap Main and Sub VFO frequencies."""
        ...

    async def vfo_equalize(self) -> None:
        """Set Sub VFO frequency equal to Main."""
        ...


@runtime_checkable
class StateCacheCapable(Protocol):
    """Radio exposes a shared state cache for server-side snapshots."""

    @property
    def state_cache(self) -> Any:
        """Shared state cache object."""
        ...


@runtime_checkable
class RecoverableConnection(Protocol):
    """Radio supports soft reconnect/disconnect operations."""

    async def soft_reconnect(self) -> None:
        """Attempt in-place reconnect without full teardown."""
        ...

    async def soft_disconnect(self) -> None:
        """Gracefully disconnect from the radio."""
        ...


@runtime_checkable
class AdvancedControlCapable(Protocol):
    """Radio supports extended control surface used by the web layer."""

    async def set_filter(self, filter_num: int, receiver: int = 0) -> None: ...

    async def set_filter_shape(self, shape: int, receiver: int = 0) -> None: ...

    async def set_nb(self, on: bool, receiver: int = 0) -> None: ...

    async def set_nr(self, on: bool, receiver: int = 0) -> None: ...

    async def set_digisel(self, on: bool, receiver: int = 0) -> None: ...

    async def set_ip_plus(self, on: bool, receiver: int = 0) -> None: ...

    async def set_attenuator_level(self, db: int, receiver: int = 0) -> None: ...

    async def set_preamp(self, level: int, receiver: int = 0) -> None: ...

    async def set_agc(self, mode: int, receiver: int = 0) -> None: ...

    async def send_cw_text(self, text: str) -> None: ...
    async def set_attenuator(self, on: bool, receiver: int = 0) -> None: ...
    async def get_attenuator_level(self, receiver: int = 0) -> int: ...
    async def get_preamp(self, receiver: int = 0) -> int: ...
    async def set_antenna_1(self, on: bool) -> None: ...
    async def set_antenna_2(self, on: bool) -> None: ...
    async def set_rx_antenna_ant1(self, on: bool) -> None: ...
    async def set_rx_antenna_ant2(self, on: bool) -> None: ...
    async def get_antenna_1(self) -> bool: ...
    async def get_antenna_2(self) -> bool: ...
    async def get_rx_antenna_ant1(self) -> bool: ...
    async def get_rx_antenna_ant2(self) -> bool: ...
    async def set_system_date(self, year: int, month: int, day: int) -> None: ...
    async def get_system_date(self) -> tuple[int, int, int]: ...
    async def set_system_time(self, hour: int, minute: int) -> None: ...
    async def get_system_time(self) -> tuple[int, int]: ...
    async def set_dual_watch(self, on: bool) -> None: ...
    async def get_dual_watch(self) -> bool: ...
    async def set_tuner_status(self, value: int) -> None: ...
    async def get_tuner_status(self) -> int: ...
    async def set_acc1_mod_level(self, level: int) -> None: ...
    async def set_usb_mod_level(self, level: int) -> None: ...
    async def set_lan_mod_level(self, level: int) -> None: ...
    async def set_compressor(self, on: bool) -> None: ...

    # DSP toggles (M4 operator_toggles family)
    async def get_auto_notch(self, receiver: int = 0) -> bool: ...
    async def set_auto_notch(self, on: bool, receiver: int = 0) -> None: ...
    async def get_manual_notch(self, receiver: int = 0) -> bool: ...
    async def set_manual_notch(self, on: bool, receiver: int = 0) -> None: ...
    async def get_vox(self) -> bool: ...
    async def set_vox(self, on: bool) -> None: ...
    async def get_dial_lock(self) -> bool: ...
    async def set_dial_lock(self, on: bool) -> None: ...
    async def get_monitor(self) -> bool: ...
    async def set_monitor(self, on: bool) -> None: ...

    # Advanced DSP levels (M4 dsp_levels family)
    async def get_pbt_inner(self, receiver: int = 0) -> int: ...
    async def set_pbt_inner(self, level: int, receiver: int = 0) -> None: ...
    async def get_pbt_outer(self, receiver: int = 0) -> int: ...
    async def set_pbt_outer(self, level: int, receiver: int = 0) -> None: ...
    async def get_cw_pitch(self) -> int: ...
    async def set_cw_pitch(self, freq: int) -> None: ...
    async def get_vox_gain(self) -> int: ...
    async def set_vox_gain(self, level: int) -> None: ...
    async def get_anti_vox_gain(self) -> int: ...
    async def set_anti_vox_gain(self, level: int) -> None: ...
    async def get_monitor_gain(self) -> int: ...
    async def set_monitor_gain(self, level: int) -> None: ...

    # Scope controls (M4 advanced_scope family)
    async def get_scope_receiver(self) -> int: ...
    async def set_scope_receiver(self, receiver: int) -> None: ...
    async def get_scope_dual(self) -> bool: ...
    async def set_scope_dual(self, dual: bool) -> None: ...
    async def get_scope_mode(self) -> int: ...
    async def set_scope_mode(self, mode: int) -> None: ...
    async def get_scope_span(self) -> int: ...
    async def set_scope_span(self, span: int) -> None: ...
    async def get_scope_speed(self) -> int: ...
    async def set_scope_speed(self, speed: int) -> None: ...
    async def get_scope_ref(self) -> int: ...
    async def set_scope_ref(self, ref: int) -> None: ...
    async def get_scope_hold(self) -> bool: ...
    async def set_scope_hold(self, on: bool) -> None: ...


@runtime_checkable
class TransceiverStatusCapable(Protocol):
    """Radio supports RIT/XIT and transceiver status queries (M4 transceiver_status family)."""

    async def get_rit_frequency(self) -> int:
        """Get RIT frequency offset in Hz."""
        ...

    async def set_rit_frequency(self, freq_hz: int) -> None:
        """Set RIT frequency offset in Hz."""
        ...

    async def get_rit_status(self) -> bool:
        """Get RIT on/off status."""
        ...

    async def set_rit_status(self, on: bool) -> None:
        """Set RIT on/off status."""
        ...

    async def get_rit_tx_status(self) -> bool:
        """Get RIT TX (XIT) on/off status."""
        ...

    async def set_rit_tx_status(self, on: bool) -> None:
        """Set RIT TX (XIT) on/off status."""
        ...

    async def get_tx_freq_monitor(self) -> bool:
        """Get TX frequency monitor on/off status."""
        ...

    async def set_tx_freq_monitor(self, on: bool) -> None:
        """Set TX frequency monitor on/off status."""
        ...
