import argparse
import asyncio
import logging
from dataclasses import dataclass
from dataclasses import field
from pathlib import Path
from typing import Callable
from typing import Optional
from typing import Type
from typing import TypeVar

from gwproactor import Proactor
from gwproactor import ProactorSettings
from gwproactor import setup_logging
from gwproactor.config import DEFAULT_BASE_NAME
from gwproactor.config import LoggingSettings
from gwproactor.config import MQTTClient
from gwproactor.config import Paths
from gwproactor_test import copy_keys
from gwproactor_test.certs import uses_tls
from gwproactor_test.logger_guard import LoggerGuards
from gwproactor_test.proactor_recorder import ProactorT
from gwproactor_test.proactor_recorder import RecorderInterface
from gwproactor_test.proactor_recorder import make_recorder_class


@dataclass
class ProactorTestHelper:
    name: str
    path_name: str
    settings: ProactorSettings
    kwargs: dict = field(default_factory=dict)
    proactor: Optional[RecorderInterface] = None


ChildT = TypeVar("ChildT", bound=Proactor)
ParentT = TypeVar("ParentT", bound=Proactor)
ChildSettingsT = TypeVar("ChildSettingsT", bound=ProactorSettings)
ParentSettingsT = TypeVar("ParentSettingsT", bound=ProactorSettings)


class CommTestHelper:
    parent_t: Type[ProactorT]
    child_t: Type[Proactor]
    parent_settings_t: Type[ParentSettingsT]
    child_settings_t: Type[ProactorSettings]

    parent_recorder_t: Callable[..., RecorderInterface] = None
    child_recorder_t: Callable[..., RecorderInterface] = None

    parent_helper: ProactorTestHelper
    child_helper: ProactorTestHelper
    verbose: bool
    child_verbose: bool
    parent_verbose: bool
    parent_on_screen: bool
    lifecycle_logging: bool
    logger_guards: LoggerGuards

    warn_if_multi_subscription_tests_skipped: bool = True

    @classmethod
    def setup_class(cls):
        if cls.parent_recorder_t is None:
            cls.parent_recorder_t = make_recorder_class(cls.parent_t)
        if cls.child_recorder_t is None:
            cls.child_recorder_t = make_recorder_class(cls.child_t)

    def __init__(
        self,
        child_settings: Optional[ChildSettingsT] = None,
        parent_settings: Optional[ParentSettingsT] = None,
        verbose: bool = False,
        child_verbose: bool = False,
        parent_verbose: bool = False,
        lifecycle_logging: bool = False,
        add_child: bool = False,
        add_parent: bool = False,
        start_child: bool = False,
        start_parent: bool = False,
        parent_on_screen: bool = False,
        child_name: str = "",
        parent_name: str = "",
        child_path_name: str = "child",
        parent_path_name: str = "parent",
        child_kwargs: Optional[dict] = None,
        parent_kwargs: Optional[dict] = None,
    ):
        self.setup_class()
        self.child_helper = ProactorTestHelper(
            child_name,
            child_path_name,
            (
                self.child_settings_t(paths=Paths(name=Path(child_path_name)))
                if child_settings is None
                else child_settings
            ),
            dict() if child_kwargs is None else child_kwargs,
        )
        self.parent_helper = ProactorTestHelper(
            parent_name,
            parent_path_name,
            (
                self.parent_settings_t(
                    logging=LoggingSettings(
                        base_log_name=f"parent_{DEFAULT_BASE_NAME}"
                    ),
                    paths=Paths(name=Path(parent_path_name)),
                )
                if parent_settings is None
                else parent_settings
            ),
            dict() if parent_kwargs is None else parent_kwargs,
        )
        self.verbose = verbose
        self.child_verbose = child_verbose
        self.parent_verbose = parent_verbose
        self.parent_on_screen = parent_on_screen
        self.lifecycle_logging = lifecycle_logging
        self.setup_logging()
        if add_child or start_child:
            self.add_child()
            if start_child:
                self.start_child()
        if add_parent or start_parent:
            self.add_parent()
            if start_parent:
                self.start_parent()

    @classmethod
    def _make(
        cls, recorder_t: Callable[..., RecorderInterface], helper: ProactorTestHelper
    ) -> RecorderInterface:
        if uses_tls(helper.settings):
            copy_keys(helper.path_name, helper.settings)
        return recorder_t(helper.name, helper.settings, **helper.kwargs)

    def make_parent(self) -> RecorderInterface:
        return self._make(self.parent_recorder_t, self.parent_helper)

    def make_child(self) -> RecorderInterface:
        return self._make(self.child_recorder_t, self.child_helper)

    @property
    def parent(self) -> Optional[ProactorT]:
        return self.parent_helper.proactor

    @property
    def child(self) -> Optional[ProactorT]:
        return self.child_helper.proactor

    def start_child(self) -> "CommTestHelper":
        if self.child is not None:
            self.start_proactor(self.child)
        return self

    def start_parent(self) -> "CommTestHelper":
        if self.parent is not None:
            self.start_proactor(self.parent)
        return self

    def start_proactor(self, proactor: Proactor) -> "CommTestHelper":
        asyncio.create_task(proactor.run_forever(), name=f"{proactor.name}_run_forever")
        return self

    def start(self) -> "CommTestHelper":
        return self

    def add_child(self) -> "CommTestHelper":
        self.child_helper.proactor = self.make_child()
        return self

    def add_parent(self) -> "CommTestHelper":
        self.parent_helper.proactor = self.make_parent()
        return self

    def remove_child(self) -> "CommTestHelper":
        self.child_helper.proactor = None
        return self

    def remove_parent(self) -> "CommTestHelper":
        self.parent_helper.proactor = None
        return self

    @classmethod
    def _get_clients_supporting_tls(
        cls, settings: ProactorSettings
    ) -> list[MQTTClient]:
        clients = []
        for _, v in settings._iter():  # noqa
            if isinstance(v, MQTTClient):
                clients.append(v)
        return clients

    def _get_child_clients_supporting_tls(self) -> list[MQTTClient]:
        """Overide to filter which MQTT clients of ChildSettingsT are treated as supporting TLS"""
        return self._get_clients_supporting_tls(self.child_helper.settings)

    def _get_parent_clients_supporting_tls(self) -> list[MQTTClient]:
        """Overide to filter which MQTT clients of ParentSettingsT are treated as supporting TLS"""
        return self._get_clients_supporting_tls(self.parent_helper.settings)

    @classmethod
    def _set_settings_use_tls(cls, use_tls: bool, clients: list[MQTTClient]) -> None:
        for client in clients:
            client.tls.use_tls = use_tls

    def set_use_tls(self, use_tls: bool) -> None:
        """Set MQTTClients which support TLS in parent and child settings to use TLS per use_tls. Clients supporting TLS
        is determined by _get_child_clients_supporting_tls() and _get_parent_clients_supporting_tls() which may be
        overriden in derived class.
        """
        self._set_settings_use_tls(use_tls, self._get_child_clients_supporting_tls())
        self._set_settings_use_tls(use_tls, self._get_parent_clients_supporting_tls())

    def setup_logging(self):
        self.child_helper.settings.paths.mkdirs(parents=True)
        self.parent_helper.settings.paths.mkdirs(parents=True)
        errors = []
        if not self.lifecycle_logging:
            self.child_helper.settings.logging.levels.lifecycle = logging.WARNING
            self.parent_helper.settings.logging.levels.lifecycle = logging.WARNING
        self.logger_guards = LoggerGuards(
            list(self.child_helper.settings.logging.qualified_logger_names().values())
            + list(
                self.parent_helper.settings.logging.qualified_logger_names().values()
            )
        )
        setup_logging(
            argparse.Namespace(verbose=self.verbose or self.child_verbose),
            self.child_helper.settings,
            errors,
            add_screen_handler=True,
            root_gets_handlers=False,
        )
        assert not errors
        setup_logging(
            argparse.Namespace(verbose=self.verbose or self.parent_verbose),
            self.parent_helper.settings,
            errors,
            add_screen_handler=self.parent_on_screen,
            root_gets_handlers=False,
        )
        assert not errors

    async def stop_and_join(self):
        proactors = [
            helper.proactor
            for helper in [self.child_helper, self.parent_helper]
            if helper.proactor is not None
        ]
        for proactor in proactors:
            # noinspection PyBroadException
            try:
                proactor.stop()
            except:
                pass
        for proactor in proactors:
            # noinspection PyBroadException
            try:
                await proactor.join()
            except:
                pass

    async def __aenter__(self) -> "CommTestHelper":
        return self

    async def __aexit__(self, exc_type, exc, tb):
        await self.stop_and_join()
        if exc is not None:
            logging.getLogger("gridworks").error(
                "CommTestHelper caught error %s.\nWorking log dirs:\n\t[%s]\n\t[%s]",
                exc,
                self.child_helper.settings.paths.log_dir,
                self.parent_helper.settings.paths.log_dir,
            )
        self.logger_guards.restore()

    def summary_str(self) -> str:
        return (
            "CHILD:\n"
            f"{self.child.summary_str()}\n"
            "PARENT:\n"
            f"{self.parent.summary_str()}"
        )
