import asyncio
import logging

from .protocol import Protocol

logger = logging.getLogger(__name__)
logger.setLevel(logging.DEBUG)


class Serf:
    PROTOCOL = Protocol
    TIMEOUT = 10

    def __init__(self, host="localhost", port=7373, auth_key=None):
        self.host = host
        self.port = port
        self.auth_key = auth_key

        self.protocol = None

    async def connect(self):
        self.protocol = await self.PROTOCOL.connect(self.host, self.port, self.auth_key)
        return self

    async def close(self):
        await self.protocol.close()

    async def __aenter__(self):
        await self.connect()
        return self

    async def __aexit__(self, exc_type, exc, tb):
        await self.close()

    async def event(self, name, payload=None, coalesce=False):
        req = await self.protocol.send(
            {
                "command": "event",
                "body": {
                    "Name": name,
                    "Payload": payload,
                    "Coalesce": coalesce,
                },
            }
        )

        async with self.protocol.recv(req) as stream:
            async with asyncio.timeout(self.TIMEOUT):
                return (await anext(stream))[0]

    async def force_leave(self, node):
        req = await self.protocol.send(
            {
                "command": "force-leave",
                "body": {
                    "Node": node,
                },
            }
        )

        async with self.protocol.recv(req) as stream:
            async with asyncio.timeout(self.TIMEOUT):
                return await anext(stream)

    async def join(self, addresses, replay=False):
        req = await self.protocol.send(
            {
                "command": "join",
                "body": {
                    "Existing": addresses,
                    "Replay": replay,
                },
            }
        )

        async with self.protocol.recv(req) as stream:
            async with asyncio.timeout(self.TIMEOUT):
                return await anext(stream)

    async def members(self):
        req = await self.protocol.send(
            {
                "command": "members",
            }
        )

        async with self.protocol.recv(req) as stream:
            async with asyncio.timeout(self.TIMEOUT):
                return await anext(stream)

    async def members_filtered(self, name=None, status=None, tags=None):
        msg = {
            "command": "members-filtered",
            "body": {},
        }

        if name:
            if not isinstance(name, str):
                raise TypeError("name must be a str")
            msg["body"]["Name"] = name

        if status:
            if not isinstance(status, str):
                raise TypeError("status must be a str")
            msg["body"]["Status"] = status

        if tags:
            if not isinstance(tags, dict):
                raise TypeError("tags must be a dict")
            msg["body"]["Tags"] = tags

        req = await self.protocol.send(msg)
        async with self.protocol.recv(req) as stream:
            async with asyncio.timeout(self.TIMEOUT):
                return await anext(stream)

    async def tags(self, tags=None, delete_tags=None):
        msg = {
            "command": "tags",
            "body": {},
        }

        if tags:
            if not isinstance(tags, dict):
                raise TypeError("tags must be a dict")
            msg["body"]["Tags"] = tags

        if delete_tags:
            if not isinstance(delete_tags, list):
                raise TypeError("delete_tags must be a list")
            msg["body"]["DeleteTags"] = tags

        req = await self.protocol.send(msg)
        async with self.protocol.recv(req) as stream:
            async with asyncio.timeout(self.TIMEOUT):
                return (await anext(stream))[0]

    async def stream(self, event_type="*"):
        req = await self.protocol.send(
            {
                "command": "stream",
                "body": {
                    "Type": event_type,
                },
            }
        )

        async with self.protocol.recv(req) as stream:
            await anext(stream)  # skip header
            async for event in stream:
                logger.debug("serf.stream: %s", event)
                yield event

    async def monitor(self, log_level="DEBUG"):
        req = await self.protocol.send(
            {
                "command": "monitor",
                "body": {
                    "LogLevel": log_level,
                },
            }
        )

        async with self.protocol.recv(req) as stream:
            await anext(stream)  # skip header
            async for event in stream:
                logger.debug("serf.monitor: %s", event)
                yield event

    async def stop(self, seq):
        req = await self.protocol.send(
            {
                "command": "stop",
                "body": {
                    "Seq": seq,
                },
            }
        )

        async with self.protocol.recv(req) as stream:
            async with asyncio.timeout(self.TIMEOUT):
                return await anext(stream)

    async def leave(self):
        req = await self.protocol.send(
            {
                "command": "leave",
            }
        )

        async with self.protocol.recv(req) as stream:
            async with asyncio.timeout(self.TIMEOUT):
                return await anext(stream)

    async def query(
        self,
        name,
        payload,
        filter_nodes=None,
        filter_tags=None,
        timeout=0,
        request_ack=True,
    ):
        msg = {
            "command": "query",
            "body": {
                "Name": name,
                "Payload": payload,
                "RequestAck": request_ack,
                "Timeout": timeout,
            },
        }

        if filter_nodes:
            msg["body"]["FilterNodes"] = filter_nodes

        if filter_tags:
            msg["body"]["FilterTags"] = filter_tags

        req = await self.protocol.send(msg)
        async with self.protocol.recv(req) as stream:
            async with asyncio.timeout(self.TIMEOUT):
                await anext(stream)  # skip header
                async for event in stream:
                    yield event
                    if event[1]["Type"] == "done":
                        break

    async def respond(self, id_, payload=None):
        msg = {
            "command": "respond",
            "body": {
                "ID": id_,
                "Payload": payload,
            },
        }

        req = await self.protocol.send(msg)
        async with self.protocol.recv(req) as stream:
            async with asyncio.timeout(self.TIMEOUT):
                return await anext(stream)

    async def install_key(self, key):
        req = await self.protocol.send(
            {
                "command": "install-key",
                "body": {
                    "Key": key,
                },
            }
        )

        async with self.protocol.recv(req) as stream:
            async with asyncio.timeout(self.TIMEOUT):
                return await anext(stream)

    async def use_key(self, key):
        req = await self.protocol.send(
            {
                "command": "use-key",
                "body": {
                    "Key": key,
                },
            }
        )

        async with self.protocol.recv(req) as stream:
            async with asyncio.timeout(self.TIMEOUT):
                return await anext(stream)

    async def remove_key(self, key):
        req = await self.protocol.send(
            {
                "command": "remove-key",
                "body": {
                    "Key": key,
                },
            }
        )

        async with self.protocol.recv(req) as stream:
            async with asyncio.timeout(self.TIMEOUT):
                return await anext(stream)

    async def list_keys(self):
        req = await self.protocol.send(
            {
                "command": "list-keys",
            }
        )

        async with self.protocol.recv(req) as stream:
            async with asyncio.timeout(self.TIMEOUT):
                return await anext(stream)

    async def stats(self):
        req = await self.protocol.send(
            {
                "command": "stats",
            }
        )

        async with self.protocol.recv(req) as stream:
            async with asyncio.timeout(self.TIMEOUT):
                return await anext(stream)

    async def get_coordinate(self, node):
        req = await self.protocol.send(
            {
                "command": "get-coordinate",
                "body": {
                    "Node": node,
                },
            }
        )

        async with self.protocol.recv(req) as stream:
            async with asyncio.timeout(self.TIMEOUT):
                return await anext(stream)
