import inspect
from typing import Any, Dict, List, Optional
from pathlib import Path


def _create_dynamic_function(
    func_manifest: dict,
    appname: str,
    version: str,
    run_sh: str = "",
    app_files=None,
    app_artifacts=None,
):
    """
    Create a dynamic function from a PlayMolecule manifest function entry.

    Parameters
    ----------
    func_manifest : dict
        A function entry from manifest["functions"]
    appname : str
        Name of the app
    version : str
        Version of the app
    run_sh : str
        Shell script content for running the function
    app_files : optional
        App files to use for creating tests
    app_artifacts : optional
        App artifacts to use for creating tests

    Returns
    -------
    callable
        A function with the appropriate signature and behavior
    """
    # Extract function name
    function_name = func_manifest["function"].split(".")[-1]
    if function_name == "main":
        function_name = appname

    # Build function docs from manifest
    function_docs = _build_docs(func_manifest, appname)

    # Get metadata
    function = func_manifest["function"]
    function_resources = func_manifest.get("resources", None)
    module_path = f"playmolecule.apps.{appname}.{version}"
    slpm_path = f"{appname}.{version}.{function_name}"

    # Parse arguments to create signature
    params = []
    for param_info in func_manifest["params"]:
        param = _parse_param(param_info)
        params.append(param)

    signature = inspect.Signature(params)

    # Create the dynamic function
    def dynamic_function(*args, **kwargs):
        """Execute the function with validated arguments."""
        # Validate and bind arguments
        try:
            bound = signature.bind(*args, **kwargs)
            bound.apply_defaults()
        except TypeError as e:
            raise TypeError(f"{function_name}() {str(e)}")

        # Import and execute
        import importlib
        from playmolecule.apps import _inner_function

        module = importlib.import_module(module_path)

        return _inner_function(
            args=bound.arguments,
            function_name=function,
            function_resources=function_resources,
            func_manifest=func_manifest,
            slpm_path=slpm_path,
            files=module.files,
            run_sh=dynamic_function.__runsh,
        )

    # Set function attributes
    dynamic_function.__name__ = function_name
    dynamic_function.__signature__ = signature
    dynamic_function.__doc__ = "\n".join(function_docs)
    dynamic_function.__runsh = run_sh

    # Set additional metadata
    dynamic_function._name = function_name
    # Make a copy to avoid mutations
    dynamic_function.__manifest__ = func_manifest.copy()

    # Create tests from manifest
    # Import here to avoid circular imports
    from playmolecule._tests import _Tests

    dynamic_function.tests = _Tests(
        func_manifest["tests"], dynamic_function, app_files, app_artifacts
    )

    return dynamic_function


def _parse_param(param_info: dict) -> inspect.Parameter:
    """Parse a manifest parameter into an inspect.Parameter."""
    name = param_info["name"]
    param_type = param_info["type"]
    nargs = param_info.get("nargs")

    # Map manifest types to Python types
    type_map = {
        "str": str,
        "int": int,
        "float": float,
        "bool": bool,
        "Path": Path,
        "list": list,
        "dict": dict,
    }

    annotation = type_map.get(param_type, param_type)

    # If nargs is specified, convert to list type
    if nargs is not None:
        # Import typing.List for proper type annotation
        from typing import List as ListType

        annotation = ListType[annotation]

    # Handle default values
    if not param_info.get("mandatory", True) and "value" in param_info:
        default = param_info["value"]
        # Convert None string to actual None
        if default == "null" or default is None:
            default = None
        # If nargs is specified and default is not None, ensure it's a list/tuple
        elif nargs is not None and not isinstance(default, (list, tuple)):
            default = (default,) if default is not None else ()
    else:
        default = inspect.Parameter.empty

    return inspect.Parameter(
        name,
        inspect.Parameter.POSITIONAL_OR_KEYWORD,
        default=default,
        annotation=annotation,
    )


def _build_docs(func_manifest: dict, appname: str) -> List[str]:
    """Build documentation from manifest."""
    docs = []

    # Add description
    if "description" in func_manifest:
        docs.append(func_manifest["description"])
        docs.append("")

    # Add parameters section
    if "params" in func_manifest:
        docs.append("Parameters")
        docs.append("----------")

        for param in func_manifest["params"]:
            param_type = param["type"]
            nargs = param.get("nargs")

            # If nargs is specified, show as list type
            if nargs is not None:
                param_line = f"{param['name']} : list[{param_type}]"
                # Add nargs info if it's a specific number or special value
                if isinstance(nargs, int):
                    param_line += f", nargs={nargs}"
                elif nargs == "+":
                    param_line += ", nargs='+'"
            else:
                param_line = f"{param['name']} : {param_type}"

            if "choices" in param and param["choices"]:
                choices = '", "'.join(param["choices"])
                param_line += f', choices=("{choices}")'

            docs.append(param_line)

            if "description" in param:
                docs.append(f"    {param['description']}")

    # Add outputs section
    if "outputs" in func_manifest:
        docs.append("")
        docs.append("Outputs")
        docs.append("-------")
        for output, desc in func_manifest["outputs"].items():
            docs.append(f"{output}")
            docs.append(f"    {desc}")

    # Add examples
    if "examples" in func_manifest:
        docs.append("")
        docs.append("Examples")
        docs.append("--------")
        for example in func_manifest["examples"]:
            docs.append(f">>> {example}")

    # Add returns section
    docs.append("")
    docs.append("Returns")
    docs.append("-------")
    docs.append("exec_dir : ExecutableDirectory")
    docs.append("    Returns an executable directory with the setup for the run")

    return docs
