import asyncio

from collections import defaultdict
from typing import Optional, Dict, List, AsyncIterable

from cached_property import cached_property
from dataclasses import dataclass

from saas.library.persqueue.common.python import installations
from saas.library.persqueue.configuration.proto.description_pb2 import TNamespaceDescription
from saas.library.persqueue.configuration.proto.types_pb2 import TServiceConfig
from saas.library.persqueue.configuration.python.namespace import NamespaceManager as PersqueueNamespaceManager
from saas.library.python.logbroker.common import LogbrokerEndpoint
from saas.library.python.zk import ZKClient


@dataclass
class SaaSLogbrokerServiceConfig:
    ns_name: str

    logbroker: LogbrokerEndpoint
    topics_path: str
    consumers_path: str

    logbroker_mirror: Optional[LogbrokerEndpoint] = None
    mirror_topics_path: Optional[str] = None
    mirror_consumers_path: Optional[str] = None


class SaaSLogbrokerService:
    def __init__(self, name: str, ctype: str, ns: 'SaaSNamespace'):
        self._name: str = name
        self._ctype: str = ctype
        self._ns: SaaSNamespace = ns

    @property
    def name(self) -> str:
        return self._name

    @property
    def ctype(self) -> str:
        return self._ctype

    async def get_config(self) -> SaaSLogbrokerServiceConfig:
        ns_config = await self._ns.get_config()
        config_path = f'{ns_config.service_configs_path}/{self._name}/{self._ctype}'

        async with self._ns.zk_client as client:
            content, _ = await client.get(config_path)

        proto_conf = TServiceConfig()
        proto_conf.ParseFromString(content)

        host, port = installations.get_endpoint_with_port(proto_conf.Logbroker).split(':')
        logbroker = LogbrokerEndpoint(host, int(port))

        if proto_conf.HasField('LogbrokerMirror'):
            host, port = installations.get_endpoint_with_port(proto_conf.LogbrokerMirror).split(':')
            logbroker_mirror = LogbrokerEndpoint(host, int(port))
        else:
            logbroker_mirror = None

        return SaaSLogbrokerServiceConfig(
            self._ns.name,

            logbroker,
            proto_conf.TopicsPath,
            proto_conf.ConsumersPath,

            logbroker_mirror=logbroker_mirror,
            mirror_topics_path=proto_conf.MirrorTopicsPath,
            mirror_consumers_path=proto_conf.MirrorConsumersPath
        )


@dataclass()
class SaaSNamespaceConfig:
    service_configs_path: str
    services_path: str


class SaaSNamespace:
    def __init__(self, name: str, zk_client: ZKClient):
        self._name: str = name
        self._zk_client: ZKClient = zk_client

    @property
    def name(self) -> str:
        return self._name

    @property
    def zk_client(self) -> ZKClient:
        return self._zk_client

    async def get_config(self) -> SaaSNamespaceConfig:
        desc = await self._desc
        return SaaSNamespaceConfig(
            service_configs_path=desc.Config.ServicesConfigsPath,
            services_path=desc.Config.LogbrokerServicesPath
        )

    @cached_property
    async def _desc(self) -> TNamespaceDescription:
        return await asyncio.get_event_loop().run_in_executor(None, PersqueueNamespaceManager(self._name).describe)

    @cached_property
    async def _ctype_to_name_to_service(self) -> Dict[str, Dict[str, SaaSLogbrokerService]]:
        result = defaultdict(dict)
        desc = await self._desc
        for service in desc.Services:
            for ctype in service.Ctypes:
                result[ctype][service.Name] = SaaSLogbrokerService(service.Name, ctype, self)
        return result

    async def get_services(self, ctype: Optional[str] = None) -> AsyncIterable[SaaSLogbrokerService]:
        data = await self._ctype_to_name_to_service
        if ctype is not None:
            data = {ctype: data[ctype]}

        for ctype, services in data.items():
            for _, service in services.items():
                yield service

    async def get_service(self, service: str, ctype: str) -> Optional[SaaSLogbrokerService]:
        data = await self._ctype_to_name_to_service
        return data[ctype].get(service)


class SaaSConfigurationManager:
    _NAMESPACES_PATH = '/logbroker/namespaces'

    def __init__(self) -> None:
        self._zk_client: ZKClient = ZKClient()

    async def get_namespace_names(self) -> List[str]:
        async with self._zk_client as client:
            return await client.list(self._NAMESPACES_PATH)

    def get_namespace(self, name: str) -> SaaSNamespace:
        return SaaSNamespace(name, self._zk_client)
