import asyncio
import logging

from dataclasses import dataclass
from contextlib import asynccontextmanager

from .amqp import AmqpConnection
from .mqtt import MqttConnection
from .model import (
    InformationResponse,
    CreateActorRequest,
    FetchMessage,
    FetchResponse,
)
from .exceptions import NoIncomingException
from .util import ConnectionParams


logger = logging.getLogger(__name__)


@dataclass
class Almabtrieb:
    """Implements the asynchronous API of the Cattle Drive Protocol"""

    connection: MqttConnection | AmqpConnection

    @property
    def connected(self):
        """Is one connected to the service"""
        return self.connection.connected

    @staticmethod
    def from_connection_string(
        connection_string: str, echo: bool = False, silent: bool = False
    ):
        """Creates instance for connection string

        ```python
        Almabtrieb.from_connection_string("ws://user:pass@host/ws")
        Almabtrieb.from_connection_string("amqp://user:pass@host/ws")
        ```

        :param connection_string: The connection string
        :param echo: Set to true to log all messages
        """

        params = ConnectionParams.from_string(connection_string)

        if params.protocol in ["ws", "wss", "mqtt"]:
            return Almabtrieb(
                connection=MqttConnection.from_connection_string(
                    connection_string, echo=echo, silent=silent
                )
            )
        elif params.protocol == "amqp":
            return Almabtrieb(
                connection=AmqpConnection(
                    connection_string=connection_string,
                    echo=echo,
                    username=params.username,
                    silent=silent,
                )
            )
        else:
            raise NotImplementedError("Protocol not implemented")

    async def run(self):
        """Starts the connection and queries for new received messages

        ```python
        task = asyncio.create_task(almabtrieb.run())
        ...
        task.cancel()
        try:
            await task
        except asyncio.CancelledError:
            pass
        ```
        """
        await self.connection.run()

    async def info(self) -> InformationResponse:
        """Returns the information about the connected
        Cattle Drive server

        :returns: The information about the server
        """
        result = await self.connection.send_with_reply("request/info", {})

        return InformationResponse.model_validate(result)

    async def create_actor(
        self,
        base_url: str,
        preferred_username: str | None = None,
        profile: dict = {},
        automatically_accept_followers: bool | None = None,
    ) -> dict:
        """Creates a new actor

        :param base_url: The base url of the actor
        :param preferred_username: The preferred username used as `acct:preferred_username@domain` where `domain` is from `base_url`
        :param profile: The profile of the actor
        :param automatically_accept_followers: If true, the server will automatically accept followers
        :returns: The created actor profile
        """
        request = CreateActorRequest(
            base_url=base_url,
            preferred_username=preferred_username,
            profile=profile,
            automatically_accept_followers=automatically_accept_followers,
        )

        return await self.connection.send_with_reply(
            "request/create_actor", request.model_dump()
        )

    async def fetch(self, actor: str, uri: str, timeout: float = 1) -> FetchResponse:
        """Fetches the object with uri `uri` as the actor with actor id `actor`.

        :param actor: The actor id must be part of actors from the info response
        :param uri: The uri of the object to fetch
        :param timeout: The timeout for the request
        :returns: The fetched object
        """
        async with asyncio.timeout(timeout):
            result = await self.connection.send_with_reply(
                "request/fetch", FetchMessage(actor=actor, uri=uri).model_dump()
            )

            return FetchResponse.model_validate(result)

    async def trigger(self, method: str, data: dict):
        end = "trigger"
        if method and len(method) > 0:
            end = f"{end}/{method}"
        return await self.connection.send(
            end,
            data,
        )

    async def incoming(self):
        """Generator for the incoming messages"""
        while msg := await self.connection.incoming_queue.get():
            yield msg

    async def outgoing(self):
        """Generator for the outgoing messages"""
        while msg := await self.connection.outgoing_queue.get():
            yield msg

    async def next_incoming(self, timeout: float = 1) -> dict:
        """Returns the next incoming message

        :param timeout: The timeout for the request
        :returns: The next incoming message
        """
        try:
            async with asyncio.timeout(timeout):
                return await self.connection.incoming_queue.get()
        except asyncio.TimeoutError:
            raise NoIncomingException("No incoming message")

    async def next_outgoing(self, timeout: float = 1) -> dict:
        """Returns the next outgoing message

        :param timeout: The timeout for the request
        :returns: The next outgoing message
        """
        try:
            async with asyncio.timeout(timeout):
                return await self.connection.outgoing_queue.get()
        except asyncio.TimeoutError:
            raise NoIncomingException("No outgoing message")

    async def next_error(self, timeout: float = 1) -> dict:
        """Returns the next error message

        :param timeout: The timeout for the request
        :returns: The next error message
        """
        try:
            async with asyncio.timeout(timeout):
                return await self.connection.error_queue.get()
        except asyncio.TimeoutError:
            raise NoIncomingException("No error message")

    async def clear_incoming(self) -> None:
        """Empties the incoming message queue"""
        while not self.connection.incoming_queue.empty():
            await self.connection.incoming_queue.get()


@asynccontextmanager
async def connect(connection_string: str, echo: bool = False) -> Almabtrieb:
    """Connects to the service

    Usage
    ```python
    async with connect("ws://user:pass@host/ws") as almabtrieb:
        print(await almabtrieb.info())
    ```
    """
    almabtrieb = Almabtrieb.from_connection_string(
        connection_string=connection_string,
        echo=echo,
    )

    task = asyncio.create_task(almabtrieb.run())
    while not almabtrieb.connected:
        logger.info("Waiting for connection")
        await asyncio.sleep(0.1)
    try:
        yield almabtrieb
    finally:
        task.cancel()
        try:
            await task
        except asyncio.CancelledError:
            pass
