"""
Object oriented abstraction over the H2B API protocol and workflow.
"""

import json
from enum import Enum
from typing import Any, Dict, Union

from .events import (
    Closed,
    Event,
    Opened,
    RecognitionComplete,
    RecognitionInProgress,
    Stopped,
    deserialize,
)


def serialize(cmd: Dict[str, Any]) -> str:
    return json.dumps(cmd, ensure_ascii=False, indent=None, separators=(",", ":"))


class ProtocolError(RuntimeError):
    """Exception raised when a [Recognizer][uhlive.stream.recognition.Recognizer] method is not available in the current state."""

    pass


class State(Enum):
    """Protocol state."""

    NoSession = "Out of Session State"
    IdleSession = "Idle Session State"
    Recognition = "On-going Recognition State"


class Recognizer:
    """The connection state machine.

    Use this class to decode received frames as `Event`s or to
    make command frames by calling the appropriate methods.
    If you call a method that is not appropriate in the current protocol
    state, a `ProtocolError` is raised.
    """

    def __init__(self) -> None:
        self._state: State = State.NoSession
        self._request_id = 0
        self._channel_id = ""

    # Workflow methods

    def open(
        self,
        custom_id: str = "",
        channel_id: str = "",
        session_id: str = "",
        audio_codec: str = "linear",
    ) -> str:
        """Open a new H2B session.

        Args:
            custom_id: is any reference of yours that you want to appear in the logs
                       and invoice reports; for example your client id.
            channel_id: when provided, it'll be used as a prefix for
                        the actual channel ID generated by the server.
            session_id: you can set an alternate channel identifier to appear along side the channel_id in
                        the logs and in the Dev Console, to make it easier to reconcile your client logs with
                        the server logs.
            audio_codec: the speech audio codec of the audio data:

                - `"linear"`: (default) linear 16 bit SLE raw PCM audio at 8khz;
                - `"g711a"`: G711 a-law audio at 8khz;
                - `"g711u"`: G711 μ-law audio at 8khz.

        Returns:
            A websocket text message to send to the server.

        Raises:
            ProtocolError: if a session is already open.
        """
        if self._state != State.NoSession:
            raise ProtocolError("Session already opened!")
        return serialize(
            {
                "command": "OPEN",
                "request_id": self.request_id,
                "channel_id": channel_id,
                "headers": {
                    "custom_id": custom_id,
                    "audio_codec": audio_codec,
                    "session_id": session_id,
                },
                "body": "",
            }
        )

    def send_audio_chunk(self, chunk: bytes) -> bytes:
        """Build an audio chunk frame for streaming.

        Returns:
            A websocket binary message to send to the server.

        Raises:
            ProtocolError: if no session opened.
        """
        if self._state == State.NoSession:
            raise ProtocolError("You must open a session first!")
        return chunk

    def set_params(self, **params: Any) -> str:
        """Set default ASR parameters for the session.

        See [the parameter list](https://docs.allo-media.net/stream-h2b/protocols/websocket/#set-session-defaults)
        and [the parameter visual explanations](https://docs.allo-media.net/stream-h2b/input/#resource-headers)
        for an explanation of the different parameters available.

        Returns:
            A websocket text message to send to the server.

        Raises:
            ProtocolError: if no session opened.
        """
        if self._state != State.IdleSession:
            raise ProtocolError(f"Method not available in this state ({self._state})!")
        return self.command("SET-PARAMS", params)

    def get_params(self) -> str:
        """Retrieve the default values for the ASR parameters.

        Returns:
            A websocket text message to send to the server.

        Raises:
            ProtocolError: if no session opened.
        """
        if self._state != State.IdleSession:
            raise ProtocolError(f"Method not available in this state ({self._state})!")
        return self.command("GET-PARAMS")

    def define_grammar(self, builtin: str, alias: str) -> str:
        """Define a grammar alias for a parameterized builtin.

        Args:
            builtin: the builtin URI to alias, including the query string, but without the "builtin:" prefix
            alias: the alias, without the "session:" prefix.

        Returns:
            A websocket text message to send to the server.

        Raises:
            ProtocolError: if no session opened.
        """
        if self._state != State.IdleSession:
            raise ProtocolError(f"Method not available in this state ({self._state})!")
        return self.command(
            "DEFINE-GRAMMAR",
            {"content_id": alias, "content_type": "text/uri-list"},
            f"builtin:{builtin}",
        )

    def recognize(
        self,
        *grammars: str,
        start_timers: bool = True,
        recognition_mode: str = "normal",
        **params: Any,
    ) -> str:
        """Start a recognition process.

        This method takes grammar URIs as positional arguments, including the `builtin:` or
        `session:` prefixes to make the difference between builtin grammars and custom aliases.

        Keyword Args:
          start_timers: default True
          recognition_mode: default is "normal"
          **params: any other [ASR parameter](https://docs.allo-media.net/stream-h2b/protocols/websocket/#start-recognition) (no client side defaults).

        Returns:
            A websocket text message to send to the server.

        Raises:
            ProtocolError: if no session opened.
        """
        if self._state != State.IdleSession:
            raise ProtocolError(f"Method not available in this state ({self._state})!")
        return self.command(
            "RECOGNIZE",
            headers={
                "recognition_mode": recognition_mode,
                "content_type": "text/uri-list",
                "start_input_timers": start_timers,
                **params,
            },
            body="\n".join(grammars),
        )

    def close(self) -> str:
        """Close the current session.

        Returns:
            A websocket text message to send to the server.

        Raises:
            ProtocolError: if no session opened.
        """
        if self._state != State.IdleSession:
            raise ProtocolError(f"Method not available in this state ({self._state})!")
        return self.command("CLOSE")

    def start_input_timers(self) -> str:
        """If the input timers were not started by the RECOGNIZE command,
        starts them now.

        Returns:
            A websocket text message to send to the server.

        Raises:
            ProtocolError: if no on-going recognition process
        """
        if self._state != State.Recognition:
            raise ProtocolError("Command is only valid during recognition!")
        return self.command("START-INPUT-TIMERS")

    def stop(self) -> str:
        """Stop ongoing recognition process

        Returns:
            A websocket text message to send to the server.

        Raises:
            ProtocolError: if no on-going recognition process
        """
        if self._state != State.Recognition:
            raise ProtocolError("Command is only valid during recognition!")
        return self.command("STOP")

    ##

    @property
    def request_id(self) -> int:
        self._request_id += 1
        return self._request_id

    @property
    def channel_id(self) -> str:
        """The current session ID."""
        return self._channel_id

    def receive(self, data: Union[str, bytes]) -> Event:
        """Decode received text frame.

        The server always replies with text frames.

        Returns:
            The appropriate `Event` subclass.
        """
        assert type(data) is str  # to please mypy
        event = deserialize(data)
        if isinstance(event, RecognitionInProgress):
            self._state = State.Recognition
        elif isinstance(event, (RecognitionComplete, Stopped)):
            self._state = State.IdleSession
        elif isinstance(event, Opened):
            self._channel_id = event.channel_id
            self._state = State.IdleSession
        elif isinstance(event, Closed):
            self._state = State.NoSession
        return event

    def command(self, name: str, headers: Dict[str, Any] = {}, body: str = "") -> str:
        return serialize(
            {
                "command": name,
                "request_id": self.request_id,
                "channel_id": self.channel_id,
                "headers": headers,
                "body": body,
            }
        )
