"""
This is a tightly coupled spawn process implementation with debugging features.
It is used for the testing of the core features and for other various tests.
"""

import argparse
import asyncio
import os
import signal
import time
from functools import partial

import psutil
from aiohttp import web
from clarabot_nano_common.common.config.constants import CommonConfigConstants
from clarabot_nano_common.utility.path.resolve import resolve_path
from clarabot_nano_common.utility.serialize import msgpack_loads, msgpack_dumps

from clarabot_nano.common.config.client import AbstractAioConfiguredProcess
from clarabot_nano.common.core.message import (
    MSG_TYPE_GATEWAY_LOGIN,
    MSG_TYPE_GATEWAY_LOGOUT,
    MSG_TYPE_REQUEST_CONFIG_UPDATE,
    MSG_TYPE_REQUEST_CONFIG_RESET,
    MSG_TYPE_CONTROL_REQ_STATS,
    MSG_TYPE_CONTROL_INDEXER_WORK,
    MSG_TYPE_CONTROL_SERVICE_ERROR,
    MSG_TYPE_REQUEST_INDEXER_LAST_WORK,
)
from clarabot_nano.development.spawn.common import write_spawn_debug_info
from clarabot_nano.utility.network import get_free_port_for_dynamic_service


class _TesterProcess(AbstractAioConfiguredProcess):
    def __init__(self, process_name):
        AbstractAioConfiguredProcess.__init__(self, process_name, True)
        self.startup_done = asyncio.Event()
        self._main_wait_future = asyncio.get_event_loop().create_future()
        self._hang_indefinitely = False
        # extra control handlers to simulate correct communication of processes
        self._core_handler.add_handler(MSG_TYPE_CONTROL_REQ_STATS, self._handle_noop)
        self._core_handler.add_handler(MSG_TYPE_CONTROL_INDEXER_WORK, self._handle_noop)
        self._core_handler.add_handler(MSG_TYPE_CONTROL_SERVICE_ERROR, self._handle_noop)

    def _handle_noop(self, routed_message):
        pass

    async def run(self):
        # stop dbg server and exit loop after this task is done
        asyncio.current_task().add_done_callback(lambda f: os.kill(os.getpid(), signal.SIGTERM))
        try:
            await AbstractAioConfiguredProcess.run(self)
        finally:
            if self._hang_indefinitely:
                while True:  # simulate permanent hanging error
                    time.sleep(5)

    async def _main_wrapper(self):
        self._core_requester = self.create_core_requester()
        with self._core_requester:
            await AbstractAioConfiguredProcess._main_wrapper(self)

    async def _main(self):
        self.startup_done.set()
        await self._main_wait_future

    def _msgpack_response(self, data):
        return web.Response(body=msgpack_dumps(data), content_type="application/msgpack")

    async def _parse_post_data(self, request: web.Request):
        if request.content_type in ("text/plain", "application/json"):
            return await request.json()
        return msgpack_loads(await request.read())

    async def handle_get_identity(self, request):
        return self._msgpack_response(
            {
                "pid": os.getpid(),
                "create_time": psutil.Process(os.getpid()).create_time(),
            }
        )

    async def handle_get_config(self, request):
        return self._msgpack_response(self.get_config())

    async def handle_set_hang(self, request):
        self._hang_indefinitely = True
        return web.Response()

    async def handle_exit_normal(self, request):
        asyncio.get_event_loop().call_later(0.333, partial(self._main_wait_future.set_result, True))
        return web.Response()

    async def handle_exit_error(self, request):
        asyncio.get_event_loop().call_later(
            0.333, partial(self._main_wait_future.set_exception, Exception("Error-test"))
        )
        return web.Response()

    async def _handle_core_request(self, command, request):
        reply = await self._core_requester.request([command, request], timeout=60)  # timeout for debug
        if len(reply) == 1:
            return self._msgpack_response(msgpack_loads(reply[0]))
        return self._msgpack_response({"ok": None})

    async def handle_core_request(self, request: web.Request):
        data = await self._parse_post_data(request)
        return await self._handle_core_request(data["command"], data["message"])

    async def handle_core_notify(self, request: web.Request):
        data = await self._parse_post_data(request)
        self._core_requester.notify([data["command"], data["message"]])
        return self._msgpack_response({"ok": None})

    # convenience functions
    async def _gateway_request(self, request_frames):
        gcrn = self.get_config()["state"].get("gateway_command_result_number")
        self._core_requester.notify(request_frames)
        # mega-simple wait for request completion; this is sufficient for debugging...
        while gcrn == self.get_config()["state"].get("gateway_command_result_number"):
            await asyncio.sleep(0.1)
        return {"result": self.get_config()["state"].get("gateway_command_result")}

    async def handle_do_login(self, request: web.Request):
        data = await self._parse_post_data(request)
        result = await self._gateway_request(
            [MSG_TYPE_GATEWAY_LOGIN, msgpack_dumps({"email": data["email"], "password": data["password"]})]
        )
        return self._msgpack_response(result)

    async def handle_do_logout(self, request: web.Request):
        result = await self._gateway_request([MSG_TYPE_GATEWAY_LOGOUT, b""])
        return self._msgpack_response(result)

    async def handle_do_config_reset(self, request: web.Request):
        return await self._handle_core_request(MSG_TYPE_REQUEST_CONFIG_RESET, msgpack_dumps({}))

    async def handle_do_drive_create(self, request: web.Request):
        data = await self._parse_post_data(request)
        return await self._handle_core_request(
            MSG_TYPE_REQUEST_CONFIG_UPDATE, msgpack_dumps({"_handler_key": "drive:create", "path": data["path"]})
        )

    async def handle_do_drive_delete(self, request: web.Request):
        data = await self._parse_post_data(request)
        return await self._handle_core_request(
            MSG_TYPE_REQUEST_CONFIG_UPDATE, msgpack_dumps({"_handler_key": "drive:delete", "path": data["path"]})
        )

    async def handle_do_drive_attach_room(self, request: web.Request):
        data = await self._parse_post_data(request)
        return await self._handle_core_request(
            MSG_TYPE_REQUEST_CONFIG_UPDATE,
            msgpack_dumps(
                {
                    "_handler_key": "drive:attach-room",
                    "path": data["path"],
                    "room_id": data["room_id"],
                    "room_blocks": data["room_blocks"],
                }
            ),
        )

    async def handle_do_drive_detach_room(self, request: web.Request):
        data = await self._parse_post_data(request)
        return await self._handle_core_request(
            MSG_TYPE_REQUEST_CONFIG_UPDATE,
            msgpack_dumps({"_handler_key": "drive:detach-room", "path": data["path"], "room_id": data["room_id"]}),
        )

    async def handle_wait_idle_indexer(self, request: web.Request):
        data = await self._parse_post_data(request)
        idle_after = data["idle_after"]
        poll_delay = max(min(idle_after / 100, 0.1), 0.001)
        t = time.perf_counter() + data["timeout"]
        idle = False
        while True:
            reply = await self._core_requester.request(
                [MSG_TYPE_REQUEST_INDEXER_LAST_WORK, msgpack_dumps({})], timeout=10
            )
            assert len(reply) == 1, str(reply)
            elapsed = msgpack_loads(reply[0])["elapsed"]
            if elapsed > idle_after:
                idle = True
                break
            if time.perf_counter() > t:
                break
            await asyncio.sleep(poll_delay)
        return self._msgpack_response({"idle": idle})


async def _aio_main(process_name):
    loop = asyncio.get_running_loop()

    proc = _TesterProcess(process_name)
    proc_task = loop.create_task(proc.run())
    await proc.startup_done.wait()
    # run test app
    app = web.Application()
    app.add_routes(
        [
            web.get("/get-identity", proc.handle_get_identity),
            web.get("/get-config", proc.handle_get_config),
            web.get("/set-hang", proc.handle_set_hang),
            web.get("/exit-normal", proc.handle_exit_normal),
            web.get("/exit-error", proc.handle_exit_error),
            web.post("/core-request", proc.handle_core_request),
            web.post("/core-notify", proc.handle_core_notify),
            # convenience for development build
            web.post("/do-login", proc.handle_do_login),
            web.post("/do-logout", proc.handle_do_logout),
            web.post("/do-config-reset", proc.handle_do_config_reset),
            web.post("/do-drive-create", proc.handle_do_drive_create),
            web.post("/do-drive-delete", proc.handle_do_drive_delete),
            web.post("/do-drive-attach-room", proc.handle_do_drive_attach_room),
            web.post("/do-drive-detach-room", proc.handle_do_drive_detach_room),
            web.post("/wait-idle-indexer", proc.handle_wait_idle_indexer),
        ]
    )
    # bind to debug port and make it public info
    port = get_free_port_for_dynamic_service()
    config_dir = resolve_path(proc.get_config()["local"]["config_dir"], strict=True)
    write_spawn_debug_info(config_dir, os.getpid(), process_name, port=port)
    # run aiohttp app in current loop and exit with the normal process
    runner = web.AppRunner(app)
    await runner.setup()
    site = web.TCPSite(runner, "127.0.0.1", port)
    await site.start()
    await proc_task
    await runner.cleanup()


def main():
    CommonConfigConstants.DEVELOPMENT = True
    parser = argparse.ArgumentParser("Clarabot Nano - Tester process")
    parser.add_argument("process_name")
    args = parser.parse_args()
    asyncio.run(_aio_main(args.process_name))


if __name__ == "__main__":
    main()
