# -*- coding: utf-8 -*-

"""
    Регистратор методов для JSON-RPC.

    Используется для централизованного управления всеми методами приложения.

"""
# встроенные модули
from functools import partial
from inspect import signature
from types import FunctionType
from typing import Optional, List, Callable, Dict, Sequence, Union, Coroutine

# модули проекта
from trivial_tools.json_rpc.errors import (
    invalid_format, empty_request, invalid_version, no_method,
    unknown_method, no_arguments, wrong_arguments_type, no_secret_key,
    internal_error, not_found)
from trivial_tools.formatters.base import s_type
from trivial_tools.json_rpc.basic_tools import (
    Request, Error, form_error, Result, decide, authorize, result
)


class JSONRPCMethodMaster:
    """
    Регистратор методов для JSON-RPC.

    Используется для централизованного управления всеми методами приложения.
    """

    def __init__(
            self,
            config,
            auth_callback: Optional[Callable] = None,
            post_mortem_callback: Optional[Callable] = None,
            ignores: Sequence[str] = ('secret_key',),
    ):
        """
        Инициация.
        """
        self.config = config
        self.auth_callback = auth_callback
        self.post_mortem_callback = post_mortem_callback
        self._ignores = ['return'] + list(ignores)
        self._methods: Dict[str, Callable] = {}

    def __contains__(self, method_name: str):
        """
        Проверить, имеется ли этот метод в записях.
        """
        return method_name in self._methods

    def register_method(self, func: Union[FunctionType, Coroutine]) -> FunctionType:
        """
        Зарегистрировать метод.
        """
        self._methods[func.__name__] = func
        return func

    def unknown_method(self, method_name: str) -> bool:
        """
        Этот метод нам неизвестен?
        """
        return method_name not in self._methods

    def get_method(self, method_name: str) -> Optional[Callable]:
        """
        Получить метод.
        """
        return self._methods.get(method_name)

    def get_method_names(self) -> List[str]:
        """
        Получить имена доступных методов.
        """
        return sorted(self._methods.keys())

    @staticmethod
    def base_conveyor(requests: List[Request], errors: List[Error], func: Callable) -> None:
        """
        Базовая функция сортировки запросов.
        """
        filtered = []
        while requests:
            new_request = requests.pop()
            output = func(new_request)
            decide(output, filtered, errors)

        requests.extend(reversed(filtered))

    def check_request_conveyor(self, requests: List[Request], errors: List[Error]) -> None:
        """
        Проверить на предмет соответствия JSON RPC 2.0.
        """
        self.base_conveyor(requests, errors, func=self.check_dict)

    def check_signature_conveyor(self, requests: List[Request], errors: List[Error]) -> None:
        """
        Проверка полученных параметров на соответствие сигнатуре метода.
        """
        self.base_conveyor(requests, errors, func=self._check_signature)

    def check_auth_conveyor(self, requests: List[Request], errors: List[Error]) -> None:
        """
        Проверить авторизацию внешней функцией (если есть).
        """
        if self.auth_callback is None:
            self.base_conveyor(requests, errors,
                               func=lambda x: x)
        else:
            self.base_conveyor(requests, errors,
                               func=partial(authorize, check_func=self.auth_callback))

    def _check_signature(self, request: Request) -> Union[Request, Error]:
        """
        Проверка полученных параметров на соответствие сигнатуре метода.
        """
        method_name = request.get('method')
        params = request.get('params')
        msg_id = request.get('id')
        need_id = 'id' in request

        if isinstance(params, list):
            # мы не можем проверить аргументы, если они переданы по порядку
            return request

        params = params.keys()

        method: FunctionType = self._methods[method_name]
        sig = signature(method)

        if 'kwargs' in sig.parameters:
            # мы не можем проверить аргументы, если функция принимает их в запакованном виде
            return request

        valid_keys = {x for x in sig.parameters if x not in self._ignores}
        other_keys = {x for x in params if x not in self._ignores}

        message = 'Расхождение в аргументах метода: '

        if valid_keys != other_keys:
            insufficient = ', '.join(sorted(valid_keys - other_keys))
            excess = ', '.join(sorted(other_keys - valid_keys))

            if insufficient:
                message += 'не хватает аргументов ' + insufficient

                if excess:
                    message += ', '

            if excess:
                message += 'лишние аргументы ' + excess

            return form_error(-32602, message, need_id, msg_id)
        return request

    def check_dict(self, request: Request) -> Union[Request, Error]:
        """
        Проверить словарь запроса.
        """
        if not isinstance(request, dict):
            return form_error(*invalid_format(s_type(request)))

        method = request.get('method')
        params = request.get('params')
        msg_id = request.get('id')
        need_id = 'id' in request

        if not request:
            response = form_error(*empty_request())

        elif request.get('jsonrpc') != '2.0':
            response = form_error(*invalid_version(), need_id, msg_id)

        elif method is None:
            response = form_error(*no_method(), need_id, msg_id)

        elif method not in self._methods:
            response = form_error(*unknown_method(str(method)), need_id, msg_id)

        elif params is None:
            response = form_error(*no_arguments(), need_id, msg_id)

        elif not isinstance(params, (dict, list)):
            response = form_error(*wrong_arguments_type(), need_id, msg_id)

        elif isinstance(params, dict) and params.get('secret_key') is None:
            response = form_error(*no_secret_key(), need_id, msg_id)

        else:
            response = request

        return response

    def execute(self, request: Request) -> Result:
        """
        Исполнить запрос.
        """
        method_name = request.get('method', 'unknown method')
        params = request.get('params', {})
        method = self.get_method(method_name)

        # @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
        if isinstance(params, dict):
            # убираем игнорируемые параметры
            for argument in self._ignores:
                params.pop(argument, None)
            response = method(**params)
        else:
            response = method(*params)
        # @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@

        return response

    async def async_execute(self, request: Request) -> Result:
        """
        Исполнить запрос.
        """
        method_name = request.get('method', 'unknown method')
        params = request.get('params', {})
        method = self.get_method(method_name)

        # @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
        if isinstance(params, dict):
            # убираем игнорируемые параметры
            for argument in self._ignores:
                params.pop(argument, None)
            response = await method(**params)
        else:
            response = await method(*params)
        # @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@

        return response

    async def render_get(self):
        """
        Приветствие для пользователя
        """
        methods = ''.join([f'<li>{x}</li>' for x in sorted(self.get_method_names())])

        html = (
            f"<html><head><title>{self.config.SERVICE_NAME}</title></head>"
            f"<body><strong>{self.config.SERVICE_NAME}</strong><br>POST methods:<ul>"
            f"{methods}"
            "</ul></body></html>"
        )

        return html

    async def render_post(self, request: Union[list, dict]) -> Union[Result, Error]:
        """
        Обработка запроса
        """
        if isinstance(request, dict):
            requests = [request]

        elif isinstance(request, list):
            requests = request

        else:
            response = form_error(*invalid_format(f'{s_type(request)}'))
            return response

        errors = []
        results = []

        try:
            self.check_request_conveyor(requests, errors)
            self.check_signature_conveyor(requests, errors)
            self.check_auth_conveyor(requests, errors)

        except Exception as err:
            self.post_mortem_callback(err)
            errors.append(form_error(*internal_error()))

        else:
            for job in requests:
                need_id = 'id' in job
                msg_id = job.get('id')

                # noinspection PyBroadException
                try:
                    method_result = await self.async_execute(job)

                    if need_id:
                        results.append(result(method_result, msg_id=msg_id))
                    else:
                        # мы не отвечаем на нотификации без id
                        pass

                except FileNotFoundError:
                    errors.append(form_error(*not_found(), need_id=need_id, msg_id=msg_id))

                except Exception as err:
                    self.post_mortem_callback(err)
                    errors.append(form_error(*internal_error(), need_id=need_id, msg_id=msg_id))

        output = results + errors
        if len(output) == 1:
            output = output[0]

        return output

# -*- coding: utf-8 -*-

"""
    Регистратор методов для JSON-RPC.

    Используется для централизованного управления всеми методами приложения.

"""
# встроенные модули
from functools import partial
from inspect import signature
from types import FunctionType
from typing import Optional, List, Callable, Dict, Sequence, Union, Coroutine

# модули проекта
from trivial_tools.json_rpc.errors import (
    invalid_format, empty_request, invalid_version, no_method,
    unknown_method, no_arguments, wrong_arguments_type, no_secret_key,
    internal_error, not_found)
from trivial_tools.formatters.base import s_type
from trivial_tools.json_rpc.basic_tools import (
    Request, Error, form_error, Result, decide, authorize, result
)


class JSONRPCMethodMaster:
    """
    Регистратор методов для JSON-RPC.

    Используется для централизованного управления всеми методами приложения.
    """

    def __init__(
            self,
            config,
            auth_callback: Optional[Callable] = None,
            post_mortem_callback: Optional[Callable] = None,
            ignores: Sequence[str] = ('secret_key',),
    ):
        """
        Инициация.
        """
        self.config = config
        self.auth_callback = auth_callback
        self.post_mortem_callback = post_mortem_callback
        self._ignores = ['return'] + list(ignores)
        self._methods: Dict[str, Callable] = {}

    def __contains__(self, method_name: str):
        """
        Проверить, имеется ли этот метод в записях.
        """
        return method_name in self._methods

    def register_method(self, func: Union[FunctionType, Coroutine]) -> FunctionType:
        """
        Зарегистрировать метод.
        """
        self._methods[func.__name__] = func
        return func

    def unknown_method(self, method_name: str) -> bool:
        """
        Этот метод нам неизвестен?
        """
        return method_name not in self._methods

    def get_method(self, method_name: str) -> Optional[Callable]:
        """
        Получить метод.
        """
        return self._methods.get(method_name)

    def get_method_names(self) -> List[str]:
        """
        Получить имена доступных методов.
        """
        return sorted(self._methods.keys())

    @staticmethod
    def base_conveyor(requests: List[Request], errors: List[Error], func: Callable) -> None:
        """
        Базовая функция сортировки запросов.
        """
        filtered = []
        while requests:
            new_request = requests.pop()
            output = func(new_request)
            decide(output, filtered, errors)

        requests.extend(reversed(filtered))

    def check_request_conveyor(self, requests: List[Request], errors: List[Error]) -> None:
        """
        Проверить на предмет соответствия JSON RPC 2.0.
        """
        self.base_conveyor(requests, errors, func=self.check_dict)

    def check_signature_conveyor(self, requests: List[Request], errors: List[Error]) -> None:
        """
        Проверка полученных параметров на соответствие сигнатуре метода.
        """
        self.base_conveyor(requests, errors, func=self._check_signature)

    def check_auth_conveyor(self, requests: List[Request], errors: List[Error]) -> None:
        """
        Проверить авторизацию внешней функцией (если есть).
        """
        if self.auth_callback is None:
            self.base_conveyor(requests, errors,
                               func=lambda x: x)
        else:
            self.base_conveyor(requests, errors,
                               func=partial(authorize, check_func=self.auth_callback))

    def _check_signature(self, request: Request) -> Union[Request, Error]:
        """
        Проверка полученных параметров на соответствие сигнатуре метода.
        """
        method_name = request.get('method')
        params = request.get('params')
        msg_id = request.get('id')
        need_id = 'id' in request

        if isinstance(params, list):
            # мы не можем проверить аргументы, если они переданы по порядку
            return request

        params = params.keys()

        method: FunctionType = self._methods[method_name]
        sig = signature(method)

        if 'kwargs' in sig.parameters:
            # мы не можем проверить аргументы, если функция принимает их в запакованном виде
            return request

        valid_keys = {x for x in sig.parameters if x not in self._ignores}
        other_keys = {x for x in params if x not in self._ignores}

        message = 'Расхождение в аргументах метода: '

        if valid_keys != other_keys:
            insufficient = ', '.join(sorted(valid_keys - other_keys))
            excess = ', '.join(sorted(other_keys - valid_keys))

            if insufficient:
                message += 'не хватает аргументов ' + insufficient

                if excess:
                    message += ', '

            if excess:
                message += 'лишние аргументы ' + excess

            return form_error(-32602, message, need_id, msg_id)
        return request

    def check_dict(self, request: Request) -> Union[Request, Error]:
        """
        Проверить словарь запроса.
        """
        if not isinstance(request, dict):
            return form_error(*invalid_format(s_type(request)))

        method = request.get('method')
        params = request.get('params')
        msg_id = request.get('id')
        need_id = 'id' in request

        if not request:
            response = form_error(*empty_request())

        elif request.get('jsonrpc') != '2.0':
            response = form_error(*invalid_version(), need_id, msg_id)

        elif method is None:
            response = form_error(*no_method(), need_id, msg_id)

        elif method not in self._methods:
            response = form_error(*unknown_method(str(method)), need_id, msg_id)

        elif params is None:
            response = form_error(*no_arguments(), need_id, msg_id)

        elif not isinstance(params, (dict, list)):
            response = form_error(*wrong_arguments_type(), need_id, msg_id)

        elif isinstance(params, dict) and params.get('secret_key') is None:
            response = form_error(*no_secret_key(), need_id, msg_id)

        else:
            response = request

        return response

    def execute(self, request: Request) -> Result:
        """
        Исполнить запрос.
        """
        method_name = request.get('method', 'unknown method')
        params = request.get('params', {})
        method = self.get_method(method_name)

        # @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
        if isinstance(params, dict):
            # убираем игнорируемые параметры
            for argument in self._ignores:
                params.pop(argument, None)
            response = method(**params)
        else:
            response = method(*params)
        # @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@

        return response

    async def async_execute(self, request: Request) -> Result:
        """
        Исполнить запрос.
        """
        method_name = request.get('method', 'unknown method')
        params = request.get('params', {})
        method = self.get_method(method_name)

        # @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
        if isinstance(params, dict):
            # убираем игнорируемые параметры
            for argument in self._ignores:
                params.pop(argument, None)
            response = await method(**params)
        else:
            response = await method(*params)
        # @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@

        return response

    async def render_get(self):
        """
        Приветствие для пользователя
        """
        methods = ''.join([f'<li>{x}</li>' for x in sorted(self.get_method_names())])

        html = (
            f"<html><head><title>{self.config.SERVICE_NAME}</title></head>"
            f"<body><strong>{self.config.SERVICE_NAME}</strong><br>POST methods:<ul>"
            f"{methods}"
            "</ul></body></html>"
        )

        return html

    async def render_post(self, request: Union[list, dict]) -> Union[Result, Error]:
        """
        Обработка запроса
        """
        if isinstance(request, dict):
            requests = [request]

        elif isinstance(request, list):
            requests = request

        else:
            response = form_error(*invalid_format(f'{s_type(request)}'))
            return response

        errors = []
        results = []

        try:
            self.check_request_conveyor(requests, errors)
            self.check_signature_conveyor(requests, errors)
            self.check_auth_conveyor(requests, errors)

        except Exception as err:
            self.post_mortem_callback(err)
            errors.append(form_error(*internal_error()))

        else:
            for job in requests:
                need_id = 'id' in job
                msg_id = job.get('id')

                # noinspection PyBroadException
                try:
                    method_result = await self.async_execute(job)

                    if need_id:
                        results.append(result(method_result, msg_id=msg_id))
                    else:
                        # мы не отвечаем на нотификации без id
                        pass

                except FileNotFoundError:
                    errors.append(form_error(*not_found(), need_id=need_id, msg_id=msg_id))

                except Exception as err:
                    self.post_mortem_callback(err)
                    errors.append(form_error(*internal_error(), need_id=need_id, msg_id=msg_id))

        output = results + errors
        if len(output) == 1:
            output = output[0]

        return output

# -*- coding: utf-8 -*- Регистратор методов для JSON-RPC.      Используется для централизованного управления всеми методами приложения. # встроенные модули # модули проекта Регистратор методов для JSON-RPC.      Используется для централизованного управления всеми методами приложения. Инициация. Проверить, имеется ли этот метод в записях. Зарегистрировать метод. Этот метод нам неизвестен? Получить метод. Получить имена доступных методов. Базовая функция сортировки запросов. Проверить на предмет соответствия JSON RPC 2.0. Проверка полученных параметров на соответствие сигнатуре метода. Проверить авторизацию внешней функцией (если есть). Проверка полученных параметров на соответствие сигнатуре метода. # мы не можем проверить аргументы, если они переданы по порядку # мы не можем проверить аргументы, если функция принимает их в запакованном виде Проверить словарь запроса. Исполнить запрос. # @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ # убираем игнорируемые параметры # @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ Исполнить запрос. # @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ # убираем игнорируемые параметры # @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ Приветствие для пользователя Обработка запроса # noinspection PyBroadException # мы не отвечаем на нотификации без id