import json
import logging
import re
from typing import Iterator, List

import requests

from .exceptions import (
    StreamNotCreatedException,
    StreamNotDeletedException,
    StreamSubscriptionException,
)
from .sseclient import WicaSSEClient
from .utils import WicaChannel, WicaMessage, WicaStreamProperties


class WicaStream(object):
    """An WicaStream object!

    An object of this class represents a single WicaStream from an wica-http server.

    Usage:

    ```python
    >>> stream = wica_api.WicaStream(channels="EXAMPLE:CHANNEL:123")
    >>> for message in stream.subscribe():
    >>>     print(message)
    ```

    Parameters:
        channels (List[str]|List[WicaChannel]): The channel or channels the stream should include. Optional parameters can be given per channel with the `WicaChannel` object.
                                                If only one channel is needed just encapsulate it.
        base_url (str): The endpoint the request should be sent to. Usually it has the form `SERVER:PORT/ca/streams`.
        stream_properties (WicaStreamProperties): The properties the stream should have. See `WicaStreamProperties` class for more info.

    """

    def __init__(
        self,
        channels: List[str] | List[WicaChannel],
        stream_properties: WicaStreamProperties | None = None,
        base_url: str = "127.0.0.1/ca/streams",
    ) -> None:
        self.id: int = None
        self.base_url = base_url
        self.channels = [
            WicaChannel(name=channel) if isinstance(channel, str) else channel
            for channel in channels
        ]
        self.stream_properties = stream_properties
        self._logger = logging.getLogger(self.__class__.__module__)
        self._logger.debug(
            f"Initialized AsyncWicaStream for channels {channels} with base_url {base_url}"
        )

    def create(self):
        """Create a stream from the wica-http server.

        This method can be used to pre-create streams before subscribing. It sends a POST request to the server and gets an ID.
        This ID is than written to the id field of the object and returned.

        Usage:

        ```python
        >>> for _ in range(10):
        >>>     wica_stream = AsyncWicaStream(base_url="http://student08/ca/streams", channels=["MMAC3:STR:2"])
        >>>     streams.append(wica_stream)
        >>>     await wica_stream.create()
        >>>
        >>> print("Doing someting else before starting the stream...")
        >>> await asyncio.sleep(5)
        >>>
        >>> subscribed_streams = []
        >>>
        >>> for wica_stream in streams:
        >>>     print(f"Subscribing to stream {wica_stream.id}")
        >>>     subscribed_streams.append(wica_stream.subscribe())
        ```

        See examples for more detail!


        Retruns:
            int: The ID of the created stream

        """
        channels_payload = []
        if self.channels:
            for channel in self.channels:
                if channel.properties:
                    channels_payload.append(
                        {"name": channel.name, "props": channel.properties.__dict__}
                    )
                else:
                    channels_payload.append({"name": channel.name})

        if self.stream_properties:
            payload = {"channels": channels_payload, "props": WicaStreamProperties}
        else:
            payload = {"channels": channels_payload}
        try:
            response = requests.post(url=self.base_url, json=payload)
            if response.ok:
                self.id = int(response.json())
                self._logger.debug(f"successfully started stream with id: {self.id}")
                return self.id
            else:
                response.raise_for_status()

        except requests.ConnectionError as exc:
            self._logger.debug(
                f"A Connection error occurred while creating stream {exc.request.url!r}."
            )
            raise StreamNotCreatedException(
                f"A connection error occurred while creating stream {exc.request.url!r}."
            )
        except requests.HTTPError as exc:
            self._logger.debug(
                f"Error response {exc.response.status_code} while creating stream {exc.request.url!r}."
            )
            raise StreamNotCreatedException(
                f"An error occurred while creating stream {exc.request.url!r}."
            )

    def subscribe(self) -> Iterator[WicaMessage]:
        """Subscribe to a stream.

        If the stream doesn't exists this function will call create() to create a new stream.


        Yields:
            AsyncIterator[WicaMessage]: That returns the messages sent by the server.
        """
        if not self.id:
            self._logger.debug("Trying to create stream")
            self.create()

        self._logger.debug(f"Subscribing to stream {self.id}")

        # Compiling regex for segmenting the messages returned by the client
        regex_event = re.compile(r"(?<=\-)(\w+)$")
        regex_value_type = re.compile(r"(?<=\w\s)([a-z]+)")
        regex_time = re.compile(r"(.*)(?=\s\-\s\w)")

        # get the stream
        client = None
        try:
            response = requests.get(url=self.base_url + f"/{self.id}", stream=True)
            if response.ok:
                client = WicaSSEClient(response)
                for event in client.events():
                    message = WicaMessage()
                    message.stream_id = int(event.id)
                    message.event = re.search(regex_event, event.event).group(0)
                    message.value_type = re.search(
                        regex_value_type, event.msg_info
                    ).group(0)
                    message.data = (
                        event.data
                        if message.event == "heartbeat"
                        else json.loads(event.data)
                    )
                    message.time = re.search(regex_time, event.msg_info).group(0)
                    yield message
            else:
                response.raise_for_status()

        except requests.ConnectionError as exc:
            self._logger.debug(
                f"A Connection error occurred while subscribing to the stream {exc.request.url!r}."
            )
            raise StreamSubscriptionException(
                f"A connection error occurred while subscribing to the stream  {exc.request.url!r}."
            )
        except requests.HTTPError as exc:
            self._logger.debug(
                f"Error response {exc.response.status_code} while subscribing to the stream  {exc.request.url!r}."
            )
            raise StreamSubscriptionException(
                f"An error occurred while subscribing to the stream  {exc.request.url!r}."
            )
        finally:
            if client:
                client.close()

    def destroy(self) -> requests.Response:
        """Destroys the stream on the server.

        This method sends a delete request to the server and thereby destroys the stream.
        This is also the best way to stop a running subscription.

        Returns:
            Response: Returns the http response
        """
        if not self.id:
            self._logger.debug("Stream was no deleted because no ID was provided!")
            raise StreamNotDeletedException(
                "Stream was not deleted because no ID was provided!"
            )

        try:
            response = requests.delete(url=self.base_url + f"/{self.id}")
            if response.ok:
                return response
            else:
                response.raise_for_status()
        except requests.ConnectionError as exc:
            self._logger.debug(
                f"A Connection error occurred while deleting the stream {exc.request.url!r}."
            )
            raise StreamNotDeletedException(
                f"A connection error occurred while deleting the stream  {exc.request.url!r}."
            )
        except requests.HTTPError as exc:
            self._logger.debug(
                f"Error response {exc.response.status_code} while deleting the stream  {exc.request.url!r}."
            )
            raise StreamNotDeletedException(
                f"An error occurred while deleting the stream  {exc.request.url!r}. Probably not stream with the ID {self.id}"
            )
