# 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.
"""Utilities for creating high-performance keyword argument wrapper functions.

This module provides tools for wrapping positional-only callables with
keyword argument support using code generation techniques.
"""

from __future__ import annotations

import functools
import inspect
import keyword
from typing import Any, Callable, Iterable

from tvm_ffi.utils.unpack_dataclass import unpack_dataclass_to_tuple

# Sentinel object for missing arguments
MISSING = object()

# Internal variable names used in generated code to avoid user argument conflicts
_INTERNAL_TARGET_FUNC = "__i_target_func"
_INTERNAL_MISSING = "__i_MISSING"
_INTERNAL_DEFAULTS_DICT = "__i_arg_defaults"
_INTERNAL_UNPACK = "__i_unpack"
_INTERNAL_NAMES = {
    _INTERNAL_TARGET_FUNC,
    _INTERNAL_MISSING,
    _INTERNAL_DEFAULTS_DICT,
    _INTERNAL_UNPACK,
}


def _validate_argument_names(names: list[str], arg_type: str) -> None:
    """Validate that argument names are valid Python identifiers and unique.

    Parameters
    ----------
    names
        List of argument names to validate.
    arg_type
        Description of the argument type (e.g., "Argument", "Keyword-only argument").

    """
    # Check for duplicate names
    if len(set(names)) != len(names):
        raise ValueError(f"Duplicate {arg_type.lower()} names found in: {names}")

    # Validate each name is a valid identifier
    for name in names:
        if not isinstance(name, str):
            raise TypeError(
                f"{arg_type} name must be a string, got {type(name).__name__}: {name!r}"
            )
        if keyword.iskeyword(name):
            raise ValueError(
                f"Invalid {arg_type.lower()} name: {name!r} is a Python keyword and cannot be used as a parameter name"
            )
        if not name.isidentifier():
            raise ValueError(
                f"Invalid {arg_type.lower()} name: {name!r} is not a valid Python identifier"
            )


def _validate_wrapper_args(
    arg_names: list[str],
    arg_defaults: tuple,
    kwonly_names: list[str],
    kwonly_defaults: dict[str, Any],
    reserved_names: set[str],
) -> None:
    """Validate all input arguments for make_kwargs_wrapper.

    Parameters
    ----------
    arg_names
        List of positional argument names.
    arg_defaults
        Tuple of default values for positional arguments.
    kwonly_names
        List of keyword-only argument names.
    kwonly_defaults
        Dictionary of default values for keyword-only arguments.
    reserved_names
        Set of reserved internal names that cannot be used as argument names.

    """
    # Validate arg_names are valid Python identifiers and unique
    _validate_argument_names(arg_names, "Argument")

    # Validate arg_defaults is a tuple
    if not isinstance(arg_defaults, tuple):
        raise TypeError(f"arg_defaults must be a tuple, got {type(arg_defaults).__name__}")

    # Validate arg_defaults length doesn't exceed arg_names length
    if len(arg_defaults) > len(arg_names):
        raise ValueError(
            f"arg_defaults has {len(arg_defaults)} values but only "
            f"{len(arg_names)} positional arguments"
        )

    # Validate kwonly_names are valid identifiers and unique
    _validate_argument_names(kwonly_names, "Keyword-only argument")

    # Validate kwonly_defaults keys are in kwonly_names
    kwonly_names_set = set(kwonly_names)
    for key in kwonly_defaults:
        if key not in kwonly_names_set:
            raise ValueError(
                f"Default provided for '{key}' which is not in kwonly_names: {kwonly_names}"
            )

    # Validate no overlap between positional and keyword-only arguments
    arg_names_set = set(arg_names)
    overlap = arg_names_set & kwonly_names_set
    if overlap:
        raise ValueError(f"Arguments cannot be both positional and keyword-only: {overlap}")

    # Validate no conflict between user argument names and internal names
    all_user_names = arg_names_set | kwonly_names_set
    conflicts = all_user_names & reserved_names
    if conflicts:
        raise ValueError(
            f"Argument names conflict with internal names: {conflicts}. "
            f'Please avoid using names starting with "__i_"'
        )


def _build_wrapper_code(
    arg_names: list[str],
    arg_defaults: tuple,
    kwonly_names: list[str],
    kwonly_defaults: dict[str, Any],
    dc_to_tuple_set: set[str],
) -> tuple[str, dict[str, Any]]:
    """Build the generated wrapper code string and runtime defaults dict.

    Returns
    -------
        A tuple of (code_str, runtime_defaults) where code_str is the generated
        wrapper function code and runtime_defaults maps arg names to their default values.

    """
    # Build positional defaults dictionary (right-aligned)
    arg_defaults_dict = (
        dict(zip(arg_names[-len(arg_defaults) :], arg_defaults)) if arg_defaults else {}
    )

    arg_parts: list[str] = []
    call_parts: list[str] = []
    runtime_defaults: dict[str, Any] = {}

    def _transform_expr(name: str, expr: str) -> str:
        if name in dc_to_tuple_set:
            return f"{_INTERNAL_UNPACK}({expr})"
        return expr

    def _add_param_with_default(name: str, default_value: Any) -> None:
        # Directly embed None and bool defaults; use MISSING sentinel for others.
        if default_value is None:
            arg_parts.append(f"{name}=None")
            call_parts.append(_transform_expr(name, name))
        elif type(default_value) is bool:
            default_value_str = "True" if default_value else "False"
            arg_parts.append(f"{name}={default_value_str}")
            call_parts.append(_transform_expr(name, name))
        else:
            arg_parts.append(f"{name}={_INTERNAL_MISSING}")
            runtime_defaults[name] = default_value
            base_expr = (
                f'{_INTERNAL_DEFAULTS_DICT}["{name}"] if {name} is {_INTERNAL_MISSING} else {name}'
            )
            call_parts.append(_transform_expr(name, base_expr))

    for name in arg_names:
        if name in arg_defaults_dict:
            _add_param_with_default(name, arg_defaults_dict[name])
        else:
            arg_parts.append(name)
            call_parts.append(_transform_expr(name, name))

    if kwonly_names:
        arg_parts.append("*")
        for name in kwonly_names:
            if name in kwonly_defaults:
                _add_param_with_default(name, kwonly_defaults[name])
            else:
                arg_parts.append(name)
                call_parts.append(_transform_expr(name, name))

    arg_list = ", ".join(arg_parts)
    call_list = ", ".join(call_parts)
    code_str = f"""
def wrapper({arg_list}):
    return {_INTERNAL_TARGET_FUNC}({call_list})
"""
    return code_str, runtime_defaults


def make_kwargs_wrapper(
    target_func: Callable,
    arg_names: list[str],
    arg_defaults: tuple = (),
    kwonly_names: list[str] | None = None,
    kwonly_defaults: dict[str, Any] | None = None,
    prototype: Callable | None = None,
    map_dataclass_to_tuple: list[str] | None = None,
) -> Callable:
    """Create a wrapper with kwargs support for a function that only accepts positional arguments.

    This function dynamically generates a wrapper using code generation to minimize overhead.

    Parameters
    ----------
    target_func
        The underlying function to be called by the wrapper. This function must only
        accept positional arguments.
    arg_names
        A list of ALL positional argument names in order. These define the positional
        parameters that the wrapper will accept. Must not overlap with kwonly_names.
    arg_defaults
        A tuple of default values for positional arguments, right-aligned to arg_names
        (matching Python's __defaults__ behavior). The length of this tuple determines
        how many trailing arguments have defaults.
        Example: (10, 20) with arg_names=['a', 'b', 'c', 'd'] means c=10, d=20.
        Empty tuple () means no defaults.
    kwonly_names
        A list of keyword-only argument names. These arguments can only be passed by name,
        not positionally, and appear after a '*' separator in the signature. Can include both
        required and optional keyword-only arguments. Must not overlap with arg_names.
        Example: ['debug', 'timeout'] creates wrapper(..., *, debug, timeout).
    kwonly_defaults
        Optional dictionary of default values for keyword-only arguments (matching Python's
        __kwdefaults__ behavior). Keys must be a subset of kwonly_names. Keyword-only
        arguments not in this dict are required.
        Example: {'timeout': 30} with kwonly_names=['debug', 'timeout'] means 'debug'
        is required and 'timeout' is optional.
    prototype
        Optional prototype function to copy metadata (__name__, __doc__, __module__,
        __qualname__, __annotations__) from. If None, no metadata is copied.
    map_dataclass_to_tuple
        Optional list of argument names whose values should be converted from
        dataclass instances to tuples via ``unpack_dataclass_to_tuple``.
        Dataclass fields are unpacked to tuple and leaf fields are shallow copied.

        Nested dataclass fields are recursed automatically based on type annotations.
        Lists/tuples are recursed element-wise. Dicts are recursed on values.
        Non-dataclass leaves are passed through unchanged.

    Returns
    -------
        A dynamically generated wrapper function with the specified signature

    Notes
    -----
    The generated wrapper will directly embed default values for None and bool types
    and use a MISSING sentinel object to distinguish between explicitly
    passed arguments and those that should use default values for other types to ensure
    the generated code does not contain unexpected str repr.

    """
    # Normalize inputs
    if kwonly_names is None:
        kwonly_names = []
    if kwonly_defaults is None:
        kwonly_defaults = {}
    dc_to_tuple_set = set(map_dataclass_to_tuple) if map_dataclass_to_tuple else set()

    # Validate all input arguments
    _validate_wrapper_args(arg_names, arg_defaults, kwonly_names, kwonly_defaults, _INTERNAL_NAMES)

    # Build the generated wrapper code
    code_str, runtime_defaults = _build_wrapper_code(
        arg_names, arg_defaults, kwonly_names, kwonly_defaults, dc_to_tuple_set
    )

    # Execute the generated code
    # Note: this is a limited use of exec that is safe.
    # We ensure generated code does not contain any untrusted input.
    # The argument names are validated and the default values are not part of generated code.
    # Instead default values are set to MISSING sentinel object and explicitly passed as exec_globals.
    # This is a practice adopted by `dataclasses` and `pydantic`
    exec_globals: dict[str, Any] = {
        _INTERNAL_TARGET_FUNC: target_func,
        _INTERNAL_MISSING: MISSING,
        _INTERNAL_DEFAULTS_DICT: runtime_defaults,
    }
    if dc_to_tuple_set:
        exec_globals[_INTERNAL_UNPACK] = unpack_dataclass_to_tuple
    namespace: dict[str, Any] = {}
    exec(code_str, exec_globals, namespace)
    new_func = namespace["wrapper"]

    # Copy metadata from prototype if provided
    if prototype is not None:
        functools.update_wrapper(new_func, prototype, updated=())

    return new_func


def make_kwargs_wrapper_from_signature(
    target_func: Callable,
    signature: inspect.Signature,
    prototype: Callable | None = None,
    exclude_arg_names: Iterable[str] | None = None,
    map_dataclass_to_tuple: list[str] | None = None,
) -> Callable:
    """Create a wrapper with kwargs support for a function that only accepts positional arguments.

    This is a convenience function that extracts parameter information from a signature
    object and calls make_kwargs_wrapper with the appropriate arguments. Supports both
    required and optional keyword-only arguments.

    Parameters
    ----------
    target_func
        The underlying function to be called by the wrapper.
    signature
        An inspect.Signature object describing the desired wrapper signature.
    prototype
        Optional prototype function to copy metadata (__name__, __doc__, __module__,
        __qualname__, __annotations__) from. If None, no metadata is copied.
    exclude_arg_names
        Optional iterable of argument names to ignore when extracting parameters from the signature.
        These arguments will not be included in the generated wrapper. If a name in this iterable
        does not exist in the signature, it is silently ignored.
    map_dataclass_to_tuple
        Optional list of argument names to unpack via ``unpack_dataclass_to_tuple``.
        See ``make_kwargs_wrapper`` for details.

    Returns
    -------
        A dynamically generated wrapper function with the specified signature.

    Raises
    ------
    ValueError
        If the signature contains *args or **kwargs.

    """
    # Normalize exclude_arg_names to a set for efficient lookup
    skip_set = set(exclude_arg_names) if exclude_arg_names is not None else set()

    # Extract positional and keyword-only parameters
    arg_names = []
    arg_defaults_list = []
    kwonly_names = []
    kwonly_defaults = {}

    # Track when we start seeing defaults for positional args
    has_seen_positional_default = False

    for param_name, param in signature.parameters.items():
        # Skip arguments that are in the skip list
        if param_name in skip_set:
            continue

        if param.kind == inspect.Parameter.VAR_POSITIONAL:
            raise ValueError("*args not supported in wrapper generation")
        elif param.kind == inspect.Parameter.VAR_KEYWORD:
            raise ValueError("**kwargs not supported in wrapper generation")
        elif param.kind in (
            inspect.Parameter.POSITIONAL_ONLY,
            inspect.Parameter.POSITIONAL_OR_KEYWORD,
        ):
            arg_names.append(param_name)
            if param.default is not inspect.Parameter.empty:
                has_seen_positional_default = True
                arg_defaults_list.append(param.default)
            elif has_seen_positional_default:
                # Required arg after optional arg (invalid in Python)
                raise ValueError(
                    f"Required positional parameter '{param_name}' cannot follow "
                    f"parameters with defaults"
                )
        elif param.kind == inspect.Parameter.KEYWORD_ONLY:
            kwonly_names.append(param_name)
            if param.default is not inspect.Parameter.empty:
                kwonly_defaults[param_name] = param.default

    # Convert defaults list to tuple (right-aligned to arg_names)
    arg_defaults = tuple(arg_defaults_list)

    return make_kwargs_wrapper(
        target_func,
        arg_names,
        arg_defaults,
        kwonly_names,
        kwonly_defaults,
        prototype,
        map_dataclass_to_tuple,
    )
