import json
from importlib import import_module
from logging import getLogger, Logger
from typing import Any, Callable, Dict, List, Optional

from inspect import signature

import openbb_terminal.config_terminal as cfg

from openbb_terminal.core.library.metadata import Metadata
from openbb_terminal.core.library.trail_map import TrailMap

# pylint: disable=import-outside-toplevel


class MetadataBuilder:
    def __init__(
        self,
        method: Callable,
        trail: str,
        trail_map: TrailMap,
    ) -> None:
        self.__method = method
        self.__trail = trail
        self.__trail_map = trail_map

    @staticmethod
    def build_docstring(
        method: Callable,
        trail: str,
        trail_map: TrailMap,
    ) -> str:
        doc = ""

        view_trail = f"{trail}_chart"
        if trail_map.get_view_function(view_trail):
            doc += f"Use '{view_trail}' to access the view.\n"

        if method.__doc__:
            doc += method.__doc__

        return doc

    def build(self) -> Metadata:
        method = self.__method
        trail = self.__trail
        trail_map = self.__trail_map

        dir_list: List[str] = []
        docstring = self.build_docstring(
            method=method,
            trail=trail,
            trail_map=trail_map,
        )
        metadata = Metadata(dir_list=dir_list, docstring=docstring)

        return metadata


class Operation:
    @staticmethod
    def __get_method(method_path: str) -> Callable:
        module_path, function_name = method_path.rsplit(".", 1)
        module = import_module(module_path)
        func = getattr(module, function_name)

        return func

    def __init__(
        self,
        trail: str,
        trail_map: Optional[TrailMap] = None,
        metadata: Optional[Metadata] = None,
    ) -> None:
        trail_map = trail_map or TrailMap()

        method_path = trail_map.get_model_or_view(trail=trail)
        method = self.__get_method(method_path=method_path)
        metadata = (
            metadata
            or MetadataBuilder(method=method, trail=trail, trail_map=trail_map).build()
        )

        self._trail = trail
        self._trail_map = trail_map
        self._method = method
        self.__doc__ = metadata.docstring
        self.__signature__ = signature(method)

    def __call__(self, *args: Any, **kwargs: Any) -> Any:
        method = self._method

        operation_logger = OperationLogger(
            trail=self._trail,
            method_chosen=method,
            args=args,
            kwargs=kwargs,
        )

        operation_logger.log_before_call()

        method_result = method(*args, **kwargs)

        operation_logger.log_after_call(method_result=method_result)

        return method_result

    def about(self):
        import webbrowser

        trail = self._trail
        url = "https://docs.openbb.co/sdk/reference/"
        url += "/".join(trail.split("."))
        webbrowser.open(url)


class OperationLogger:

    last_method: Dict[Any, Any] = {}

    def __init__(
        self,
        trail: str,
        method_chosen: Callable,
        args: Any,
        kwargs: Any,
        logger: Optional[Logger] = None,
    ) -> None:
        self.__trail = trail
        self.__method_chosen = method_chosen
        self.__logger = logger or getLogger(self.__method_chosen.__module__)
        self.__args = args
        self.__kwargs = kwargs

    def log_before_call(
        self,
    ):
        if self.__check_logging_conditions():
            logger = self.__logger
            self.__log_start(logger=logger, method_chosen=self.__method_chosen)
            self.__log_method_info(
                logger=logger,
                trail=self.__trail,
                method_chosen=self.__method_chosen,
                args=self.__args,
                kwargs=self.__kwargs,
            )

    @staticmethod
    def __log_start(logger: Logger, method_chosen: Callable):
        logger.info(
            "START",
            extra={"func_name_override": method_chosen.__name__},
        )

    def __log_method_info(
        self,
        logger: Logger,
        trail: str,
        method_chosen: Callable,
        args: Any,
        kwargs: Any,
    ):
        merged_args = self.__merge_function_args(method_chosen, args, kwargs)
        merged_args = self.__remove_key_and_log_state(
            method_chosen.__module__, merged_args
        )

        logging_info: Dict[str, Any] = {}
        logging_info["INPUT"] = {
            key: str(value)[:100] for key, value in merged_args.items()
        }
        logging_info["VIRTUAL_PATH"] = trail
        logging_info["CHART"] = "chart" in kwargs and kwargs["chart"] is True

        logger.info(
            f"{json.dumps(logging_info)}",
            extra={"func_name_override": method_chosen.__name__},
        )

    @staticmethod
    def __merge_function_args(func: Callable, args: tuple, kwargs: dict) -> dict:
        """
        Merge user input args and kwargs with signature defaults into a dictionary.

        Parameters
        ----------

        func : Callable
            Function to get the args from
        args : tuple
            Positional args
        kwargs : dict
            Keyword args

        Returns
        ----------
        dict
            Merged user args and signature defaults
        """
        import inspect  # pylint: disable=C0415

        sig = inspect.signature(func)
        sig_args = {
            param.name: param.default
            for param in sig.parameters.values()
            if param.default is not inspect.Parameter.empty
        }
        # merge args with sig_args
        sig_args.update(dict(zip(sig.parameters, args)))
        # add kwargs elements to sig_args
        sig_args.update(kwargs)
        return sig_args

    @staticmethod
    def __remove_key_and_log_state(func_module: str, function_args: dict) -> dict:
        """
        Remove API key from the function args and log state of keys.

        Parameters
        ----------
        func_module : str
            Module of the function
        function_args : dict
            Function args

        Returns
        ----------
        dict
            Function args with API key removed
        """

        if func_module == "openbb_terminal.keys_model":

            from openbb_terminal.core.log.generation.settings_logger import (  # pylint: disable=C0415
                log_keys,
            )

            # remove key if defined
            function_args.pop("key", None)
            log_keys()
        return function_args

    def log_after_call(
        self,
        method_result: Any,
    ):
        if self.__check_logging_conditions():
            logger = self.__logger
            self.__log_exception_if_any(
                logger=logger,
                method_result=method_result,
                method_chosen=self.__method_chosen,
            )
            self.__log_end(
                logger=logger,
                method_chosen=self.__method_chosen,
            )
            OperationLogger.last_method = {
                f"{self.__method_chosen.__module__}.{self.__method_chosen.__name__}": {
                    "args": str(self.__args)[:100],
                    "kwargs": str(self.__kwargs)[:100],
                }
            }

    @staticmethod
    def __log_exception_if_any(
        logger: Logger,
        method_chosen: Callable,
        method_result: Any,
    ):
        if isinstance(method_result, Exception):
            logger.exception(
                f"Exception: {method_result}",
                extra={"func_name_override": method_chosen.__name__},
            )

    @staticmethod
    def __log_end(logger: Logger, method_chosen: Callable):
        logger.info(
            "END",
            extra={"func_name_override": method_chosen.__name__},
        )

    def __check_logging_conditions(self) -> bool:
        return not cfg.LOGGING_SUPPRESS and not self.__check_last_method()

    def __check_last_method(self) -> bool:
        current_method = {
            f"{self.__method_chosen.__module__}.{self.__method_chosen.__name__}": {
                "args": str(self.__args)[:100],
                "kwargs": str(self.__kwargs)[:100],
            }
        }
        return OperationLogger.last_method == current_method
