import asyncio
import base64
import json
import os
import tempfile
import time
from typing import Optional, Dict, Set, Type

import aiohttp
from clarabot_testing.client.gql_ws import AbstractGraphQlWebSocketClient
from clarabot_testing.client.scenario import AbstractScenarioTestingMixin

from clarabot_nano.development.common import parse_development_server_config
from clarabot_nano.utility.network import create_session
from clarabot_nano.utility.serialize import msgpack_loads
from clarabot_nano_tests.testing.client.gql_ws import GraphQlWebSocketClient
from clarabot_nano_tests.testing.utility import alt_temp_dir


class DevServerManager(AbstractScenarioTestingMixin):
    """
    Usage information and control interface to a dev-server (debug) cluster.
    """

    @classmethod
    def create(cls, config_path: Optional[str] = None):
        if not config_path:
            for tmp_dir in [tempfile.gettempdir(), alt_temp_dir()]:
                config_path = os.path.join(tmp_dir, "__clarabot_running_debug_server_config.json")
                if os.path.isfile(config_path):
                    break
        # try to load config of server running on custom host (maybe localhost)
        with open(config_path, "r") as f:
            config = json.load(f)
        return cls(config_path, config)

    def __init__(self, config_path, config):
        AbstractScenarioTestingMixin.__init__(self)
        server_frontend_domain, server_frontend_path, ca_certificate = parse_development_server_config(config)
        self.config_path = config_path
        self.ca_certificate = ca_certificate
        self.server_frontend_domain = server_frontend_domain
        self.server_frontend_path = server_frontend_path

    # scenario overrides
    def get_gql_url(self) -> str:
        return self.url_for("h")

    def get_wsgql_url(self) -> str:
        return self.url_for("ws")

    def create_aiohttp_session(self, **kwargs) -> aiohttp.ClientSession:
        return create_session(cadata=self.ca_certificate)

    def server_url_for(self, path) -> str:
        return self.url_for(path)

    def _gql_ws_cls(self) -> Type[AbstractGraphQlWebSocketClient]:
        return GraphQlWebSocketClient

    def url_for(self, path=None, protocol="https"):
        if not path:
            return f"{protocol}://{self.server_frontend_domain}"
        return f"{protocol}://{self.server_frontend_domain}{self.server_frontend_path}{path}"

    async def get_nano_binds(self, account_id):
        s = await self.session()
        async with s.get(self.url_for(f"debug/nano/bind/{account_id}")) as resp:
            assert resp.status == 200, f"{resp.status}\n{resp}"
            result = msgpack_loads(await resp.read())
        for slot in result["slots"]:
            if slot["routing_info"]:
                slot["routing_info"] = base64.b64decode(slot["routing_info"])
        for serve in result["serves"]:
            nanos = dict()
            for rid, data in serve["nanos"].items():
                nanos[int(rid)] = base64.b64decode(data)
            serve["nanos"] = nanos
        return result

    async def wait_nano_bind(self, account_id, bind_serves: Dict[int, Set[int]], exact=False, timeout=25.0):
        """
        NOTE: exact is not very useful, different tests will bind to different nano slots and linger.
        """
        # bind_serves: session_id: Set[resource_id]
        timeout += time.perf_counter()
        while True:
            try:
                result = await self.get_nano_binds(account_id)
                current_bind_serves = dict()
                for slot in result["slots"]:
                    if slot["session_id"]:
                        current_bind_serves[slot["session_id"]] = set()
                for serve in result["serves"]:
                    for session_id in serve["nanos"].keys():
                        assert session_id in current_bind_serves
                        current_bind_serves[session_id].add(serve["resource_id"])
                if exact:
                    assert bind_serves == current_bind_serves, f"Binds mismatch\n{bind_serves}\n{current_bind_serves}"
                else:
                    for k, v in bind_serves.items():
                        assert k in current_bind_serves, f"Bind missing\n{k}\n{current_bind_serves}"
                        assert current_bind_serves[k].issuperset(v), f"Serves mismatch\n{k} {v}\n{current_bind_serves}"
            except AssertionError:
                if time.perf_counter() > timeout:
                    raise
                await asyncio.sleep(0.5)
                continue
            break

    async def safe_radix32(self, value, pack=True):
        s = await self.session()
        async with self.post_auto_csrf(
            s,
            self.url_for(f"debug/safe-radix32/{'' if pack else 'un'}pack"),
            json={"value": value},
        ) as resp:
            assert resp.status == 200, "%s %s" % (resp.status, await resp.read())
            return msgpack_loads(await resp.read())["value"]
