"""OpenBB Terminal SDK Helpers."""

import json
from inspect import signature
from logging import Logger, getLogger
from typing import Any, Callable, Dict, Optional

from openbb_terminal.core.config.paths import SETTINGS_ENV_FILE
from openbb_terminal.core.sdk.sdk_init import (
    FORECASTING_TOOLKIT_ENABLED,
    FORECASTING_TOOLKIT_WARNING,
    OPTIMIZATION_TOOLKIT_ENABLED,
    OPTIMIZATION_TOOLKIT_WARNING,
)
from openbb_terminal.core.session.current_system import (
    get_current_system,
    set_system_variable,
)
from openbb_terminal.rich_config import console

SETTINGS_ENV_FILE.parent.mkdir(parents=True, exist_ok=True)


if (
    not FORECASTING_TOOLKIT_ENABLED
    and not get_current_system().DISABLE_FORECASTING_WARNING
):
    set_system_variable("DISABLE_FORECASTING_WARNING", True)

    console.print(FORECASTING_TOOLKIT_WARNING)


if (
    not OPTIMIZATION_TOOLKIT_ENABLED
    and not get_current_system().DISABLE_OPTIMIZATION_WARNING
):
    set_system_variable("DISABLE_OPTIMIZATION_WARNING", True)

    console.print(OPTIMIZATION_TOOLKIT_WARNING)


def clean_attr_desc(attr: Optional[Any] = None) -> Optional[str]:
    """Clean the attribute description."""

    if attr.__doc__ is None:
        return None

    return (
        attr.__doc__.splitlines()[1].lstrip()
        if not attr.__doc__.splitlines()[0]
        else attr.__doc__.splitlines()[0].lstrip()
        if attr.__doc__
        else ""
    )


def class_repr(cls_dict: Dict[str, Any]) -> list:
    """Return the representation of the class."""

    return [
        f"    {k}: {clean_attr_desc(v)}\n"
        for k, v in cls_dict.items()
        if v.__doc__ and not k.startswith("_")
    ]


class Operation:
    def __init__(self, name: str, trail: str, func: Callable) -> None:
        self._trail = trail

        self._method = func

        self._name = name

        for attr in [
            "__doc__",
            "__name__",
            "__annotations__",
            "__defaults__",
            "__kwdefaults__",
            "__module__",
        ]:
            setattr(self.__class__, attr, getattr(func, attr))

            setattr(self, attr, getattr(func, attr))

        self.__signature__ = signature(func)

        self.__class__.__signature__ = signature(func)

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

        # We make a copy of the kwargs to avoid modifying the original

        log_kwargs = kwargs.copy()

        log_kwargs["chart"] = "chart" in self._name

        operation_logger = OperationLogger(
            trail=self._trail, method_chosen=method, args=args, kwargs=log_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):
        # pylint: disable=C0415

        import webbrowser

        trail = "/".join(self._trail.split(".")).replace("_chart", "")

        url = f"https://docs.openbb.co/sdk/reference/{trail}"

        webbrowser.open(url)


class Category:

    """The base class that all categories must inherit from."""

    _location_path: str = ""

    def __init__(self, *args, **kwargs):
        """Initialize the class"""

        super().__init__(*args, **kwargs)

    def __repr__(self):
        """Return the representation of the class."""

        repr_docs = []

        if submodules := class_repr(self.__class__.__dict__):
            repr_docs += ["\nSubmodules:\n"] + submodules

        if attributes := class_repr(self.__dict__):
            repr_docs += ["\nAttributes:\n"] + attributes

        return f"{self.__class__.__name__}(\n{''.join(repr_docs)}\n)"

    def __getattribute__(self, name: str):
        """We override the __getattribute__ method and wrap all callable
        attributes with a wrapper that logs the call and the result.
        """

        attr = super().__getattribute__(name)

        if isinstance(attr, Operation) or name.startswith("__"):
            return attr

        trail = f"{self.__class__._location_path}.{name}"

        if callable(attr) and not isinstance(attr, Operation):
            # We set the attribute to the wrapped function so that we don't

            # have to wrap it when called again.

            setattr(self, name, Operation(name=name, trail=trail, func=attr))

            return getattr(self, name)

        return attr

    def about(self):
        # pylint: disable=C0415

        import webbrowser

        trail = "/".join(self._location_path.split("."))

        url = f"https://docs.openbb.co/sdk/reference/{trail}"

        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"] = kwargs.get("chart", False)

        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":
            # pylint: disable=C0415

            # from openbb_terminal.core.log.generation.settings_logger import (
            #     log_credentials,
            # )

            # remove key if defined

            function_args.pop("key", None)

            # log_credentials()

        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 get_current_system().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


def get_sdk_imports_text() -> str:
    """Return the text for the SDK imports."""

    sdk_imports = """\"\"\"OpenBB Terminal SDK.\"\"\"

# ######### THIS FILE IS AUTO GENERATED - ANY CHANGES WILL BE VOID ######### #

# flake8: noqa

# pylint: disable=unused-import,wrong-import-order

# pylint: disable=C0302,W0611,R0902,R0903,C0412,C0301,not-callable

import logging

import openbb_terminal.config_terminal as cfg

from openbb_terminal import helper_funcs as helper  # noqa: F401

from openbb_terminal.core.plots.plotly_helper import theme  # noqa: F401

from openbb_terminal.cryptocurrency.due_diligence.pycoingecko_model import Coin

from openbb_terminal.dashboards.dashboards_controller import DashboardsController

from openbb_terminal.helper_classes import TerminalStyle  # noqa: F401

from openbb_terminal.reports import widget_helpers as widgets  # noqa: F401

from openbb_terminal.reports.reports_controller import ReportController

import openbb_terminal.core.sdk.sdk_init as lib

from openbb_terminal.core.sdk import (
    controllers as ctrl,

    models as model,
)

from openbb_terminal.core.session.current_system import get_current_system

from openbb_terminal.core.session.current_user import is_local

from openbb_terminal.terminal_helper import is_auth_enabled

cfg.setup_config_terminal(is_sdk=True)

logger = logging.getLogger(__name__)

cfg.theme.applyMPLstyle()

\r\r\r

"""

    return "\r".join(sdk_imports.splitlines())
