"""
This file can generate a meta file for the given prompt template or a python file.
"""
import inspect
import types
from dataclasses import asdict

from utils.tool_utils import function_to_interface

from promptflow.contracts.tool import Tool, ToolType
# Avoid circular dependencies: Use import 'from promptflow._internal' instead of 'from promptflow'
# since the code here is in promptflow namespace as well
from promptflow._internal import ToolProvider
from promptflow.exceptions import ErrorTarget, UserErrorException


def asdict_without_none(obj):
    return asdict(obj, dict_factory=lambda x: {k: v for (k, v) in x if v})


def asdict_with_advanced_features_without_none(obj, **advanced_features):
    dict_without_none = asdict_without_none(obj)
    dict_without_none.update({k: v for k, v in advanced_features.items() if v})
    return dict_without_none


def is_tool(f):
    if not isinstance(f, types.FunctionType):
        return False
    if not hasattr(f, "__tool"):
        return False
    return True


def collect_tool_functions_in_module(m):
    tools = []
    for _, obj in inspect.getmembers(m):
        if is_tool(obj):
            # Note that the tool should be in defined in exec but not imported in exec,
            # so it should also have the same module with the current function.
            if getattr(obj, "__module__", "") != m.__name__:
                continue
            tools.append(obj)
    return tools


def collect_tool_methods_in_module(m):
    tools = []
    for _, obj in inspect.getmembers(m):
        if isinstance(obj, type) and issubclass(obj, ToolProvider) and obj.__module__ == m.__name__:
            for _, method in inspect.getmembers(obj):
                if is_tool(method):
                    initialize_inputs = obj.get_initialize_inputs()
                    tools.append((method, initialize_inputs))
    return tools


def _parse_tool_from_function(f, initialize_inputs=None, tool_type=ToolType.PYTHON, name=None, description=None):
    if hasattr(f, "__tool") and isinstance(f.__tool, Tool):
        return f.__tool
    if hasattr(f, "__original_function"):
        f = f.__original_function
    try:
        inputs, _, _ = function_to_interface(f, tool_type=tool_type, initialize_inputs=initialize_inputs)
    except Exception as e:
        raise BadFunctionInterface(f"Failed to parse interface for tool {f.__name__}, reason: {e}") from e
    class_name = None
    if "." in f.__qualname__:
        class_name = f.__qualname__.replace(f".{f.__name__}", "")
    # Construct the Tool structure
    return Tool(
        name=name or f.__qualname__,
        description=description or inspect.getdoc(f),
        inputs=inputs,
        type=tool_type,
        class_name=class_name,
        function=f.__name__,
        module=f.__module__,
    )


def generate_python_tools_in_module(module, name, description):
    tool_functions = collect_tool_functions_in_module(module)
    tool_methods = collect_tool_methods_in_module(module)
    return [_parse_tool_from_function(f, name=name, description=description) for f in tool_functions] + [
        _parse_tool_from_function(f, initialize_inputs, name=name, description=description)
        for (f, initialize_inputs) in tool_methods
    ]


def generate_python_tools_in_module_as_dict(module, name=None, description=None, **advanced_features):
    tools = generate_python_tools_in_module(module, name, description)
    return _construct_tool_dict(tools, **advanced_features)


def generate_custom_llm_tools_in_module(module, name, description):
    tool_functions = collect_tool_functions_in_module(module)
    tool_methods = collect_tool_methods_in_module(module)
    return [
        _parse_tool_from_function(f, tool_type=ToolType.CUSTOM_LLM, name=name, description=description)
        for f in tool_functions
    ] + [
        _parse_tool_from_function(
            f, initialize_inputs, tool_type=ToolType.CUSTOM_LLM, name=name, description=description
        )
        for (f, initialize_inputs) in tool_methods
    ]


def generate_custom_llm_tools_in_module_as_dict(module, name=None, description=None, **advanced_features):
    tools = generate_custom_llm_tools_in_module(module, name, description)
    return _construct_tool_dict(tools, **advanced_features)


def _construct_tool_dict(tools, **advanced_features):
    return {
        f"{t.module}.{t.class_name}.{t.function}"
        if t.class_name is not None
        else f"{t.module}.{t.function}": asdict_with_advanced_features_without_none(t, **advanced_features)
        for t in tools
    }


class ToolValidationError(UserErrorException):
    """Base exception raised when failed to validate tool."""

    def __init__(self, message):
        super().__init__(message, target=ErrorTarget.TOOL)


class PythonParsingError(ToolValidationError):
    pass


class BadFunctionInterface(PythonParsingError):
    pass
