#  Licensed to the Apache Software Foundation (ASF) under one
#  or more contributor license agreements.  See the NOTICE file
#  distributed with this work for additional information
#  regarding copyright ownership.  The ASF licenses this file
#  to you under the Apache License, Version 2.0 (the
#  "License"); you may not use this file except in compliance
#  with the License.  You may obtain a copy of the License at
#
#  http://www.apache.org/licenses/LICENSE-2.0
#
#  Unless required by applicable law or agreed to in writing,
#  software distributed under the License is distributed on an
#  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
#  KIND, either express or implied.  See the License for the
#  specific language governing permissions and limitations
#  under the License.
from __future__ import annotations

import logging
from functools import wraps
from inspect import (
    getcallargs,
    getmembers,
    getmodule,
    isclass,
    isfunction,
    ismethod,
    signature,
    Signature,
)
from logging import Logger
from typing import Any, Callable, cast, Optional, Type, Union

_DEFAULT_ENTER_MSG_PREFIX = "enter to "
_DEFAULT_ENTER_MSG_SUFFIX = ""
_DEFAULT_WITH_ARGUMENTS_MSG_PART = " with: "
_DEFAULT_EXIT_MSG_PREFIX = "exit from "
_DEFAULT_EXIT_MSG_SUFFIX = ""
_DEFAULT_RETURN_VALUE_MSG_PART = " with return value: "

_CLS_PARAM = "cls"
_SELF_PARAM = "self"
_PRIVATE_PREFIX_SYMBOL = "_"
_FIXTURE_ATTRIBUTE = "_pytestfixturefunction"
_LOGGER_VAR_NAME = "logger"

empty_and_none = {Signature.empty, "None"}


Function = Callable[..., Any]
Decorated = Union[Type[Any], Function]


def log(
    decorated: Optional[Decorated] = None,
    *,
    prefix_enter_msg: str = _DEFAULT_ENTER_MSG_PREFIX,
    suffix_enter_msg: str = _DEFAULT_ENTER_MSG_SUFFIX,
    with_arguments_msg_part=_DEFAULT_WITH_ARGUMENTS_MSG_PART,
    prefix_exit_msg: str = _DEFAULT_EXIT_MSG_PREFIX,
    suffix_exit_msg: str = _DEFAULT_EXIT_MSG_SUFFIX,
    return_value_msg_part=_DEFAULT_RETURN_VALUE_MSG_PART,
) -> Decorated:

    decorator: Decorated = _make_decorator(
        prefix_enter_msg,
        suffix_enter_msg,
        with_arguments_msg_part,
        prefix_exit_msg,
        suffix_exit_msg,
        return_value_msg_part,
    )
    if decorated is None:
        return decorator
    return decorator(decorated)


def _make_decorator(
    prefix_enter_msg: str,
    suffix_enter_msg: str,
    with_arguments_msg_part,
    prefix_out_msg: str,
    suffix_out_msg: str,
    return_value_msg_part,
) -> Decorated:
    def decorator(decorated: Decorated):
        decorated_logger = _get_logger(decorated)

        def decorator_class(clazz: Type[Any]) -> Type[Any]:
            _decorate_class_members_with_logs(clazz)
            return clazz

        def _decorate_class_members_with_logs(clazz: Type[Any]) -> None:
            members = getmembers(
                clazz, predicate=lambda val: ismethod(val) or isfunction(val)
            )
            for member_name, member in members:
                setattr(clazz, member_name, decorator_func(member, f"{clazz.__name__}"))

        def decorator_func(func: Function, prefix_name: str = "") -> Function:
            func_name = func.__name__
            func_signature: Signature = signature(func)
            is_fixture = hasattr(func, _FIXTURE_ATTRIBUTE)
            has_return_value = func_signature.return_annotation not in empty_and_none
            is_private = func_name.startswith(_PRIVATE_PREFIX_SYMBOL)
            full_func_name = f"{prefix_name}.{func_name}"
            under_info = None
            debug_enable = None

            @wraps(func)
            def _wrapper_func(*args, **kwargs) -> Any:
                _log_enter_to_function(*args, **kwargs)
                val = func(*args, **kwargs)
                _log_exit_of_function(val)
                return val

            def _log_enter_to_function(*args, **kwargs) -> None:
                if _is_log_info():
                    decorated_logger.info(
                        f"{prefix_enter_msg}'{full_func_name}'{suffix_enter_msg}"
                    )
                elif _is_debug_enable():
                    _log_debug(*args, **kwargs)

            def _is_log_info() -> bool:
                return not (_is_under_info() or is_private or is_fixture)

            def _is_under_info() -> bool:
                nonlocal under_info
                if under_info is None:
                    under_info = decorated_logger.getEffectiveLevel() < logging.INFO
                return under_info

            def _is_debug_enable() -> bool:
                nonlocal debug_enable
                if debug_enable is None:
                    debug_enable = decorated_logger.isEnabledFor(logging.DEBUG)
                return debug_enable

            def _log_debug(*args, **kwargs) -> None:
                used_parameters = getcallargs(func, *args, **kwargs)
                _SELF_PARAM in used_parameters and used_parameters.pop(_SELF_PARAM)
                _CLS_PARAM in used_parameters and used_parameters.pop(_CLS_PARAM)
                if used_parameters:
                    decorated_logger.debug(
                        f"{prefix_enter_msg}'{full_func_name}'{with_arguments_msg_part}"
                        f"{used_parameters}{suffix_enter_msg}"
                    )
                else:
                    decorated_logger.debug(
                        f"{prefix_enter_msg}'{full_func_name}'{suffix_enter_msg}"
                    )

            def _log_exit_of_function(return_value: Any) -> None:
                if _is_debug_enable() and has_return_value:
                    decorated_logger.debug(
                        f"{prefix_out_msg}'{full_func_name}'{return_value_msg_part}"
                        f"'{return_value}'{suffix_out_msg}"
                    )

            return _wrapper_func

        if isclass(decorated):
            return decorator_class(cast(Type[Any], decorated))
        return decorator_func(cast(Function, decorated))

    return decorator


def _get_logger(decorated: Decorated) -> Logger:
    module = getmodule(decorated)
    return module.__dict__.get(
        _LOGGER_VAR_NAME, logging.getLogger(module.__name__)  # type: ignore
    )
