from __future__ import annotations

import functools
import textwrap
from typing import Any
from typing import TYPE_CHECKING
from typing import TypeVar
import warnings

from optuna._warnings import optuna_warn
from optuna.exceptions import ExperimentalWarning


if TYPE_CHECKING:
    from collections.abc import Callable

    from typing_extensions import ParamSpec

    FT = TypeVar("FT")
    FP = ParamSpec("FP")
    CT = TypeVar("CT")


_EXPERIMENTAL_NOTE_TEMPLATE = """

.. note::
    Added in v{ver} as an experimental feature. The interface may change in newer versions
    without prior notice. See https://github.com/optuna/optuna/releases/tag/v{ver}.
"""


def warn_experimental_argument(option_name: str) -> None:
    optuna_warn(
        f"Argument ``{option_name}`` is an experimental feature."
        " The interface can change in the future.",
        ExperimentalWarning,
    )


def _validate_version(version: str) -> None:
    if not isinstance(version, str) or len(version.split(".")) != 3:
        raise ValueError(
            f"Invalid version specification. Must follow `x.y.z` format but `{version}` is given"
        )


def _get_docstring_indent(docstring: str) -> str:
    return docstring.split("\n")[-1] if "\n" in docstring else ""


def experimental_func(
    version: str,
    name: str | None = None,
) -> Callable[[Callable[FP, FT]], Callable[FP, FT]]:
    """Decorate function as experimental.

    Args:
        version: The first version that supports the target feature.
        name: The name of the feature. Defaults to fully qualified name of
        the function, i.e. `f"{func.__module__}.{func.__qualname__}"`. Optional.
    """

    _validate_version(version)

    def decorator(func: Callable[FP, FT]) -> Callable[FP, FT]:
        if func.__doc__ is None:
            func.__doc__ = ""

        note = _EXPERIMENTAL_NOTE_TEMPLATE.format(ver=version)
        indent = _get_docstring_indent(func.__doc__)
        func.__doc__ = func.__doc__.strip() + textwrap.indent(note, indent) + indent

        _name = name or f"{func.__module__}.{func.__qualname__}"

        @functools.wraps(func)
        def wrapper(*args: Any, **kwargs: Any) -> FT:
            warnings.warn(
                f"{_name} is experimental (supported from v{version}). "
                "The interface can change in the future.",
                ExperimentalWarning,
                stacklevel=2,
            )

            return func(*args, **kwargs)

        return wrapper

    return decorator


def experimental_class(
    version: str,
    name: str | None = None,
) -> Callable[[CT], CT]:
    """Decorate class as experimental.

    Args:
        version: The first version that supports the target feature.
        name: The name of the feature. Defaults to the class name. Optional.
    """

    _validate_version(version)

    def decorator(cls: CT) -> CT:
        def wrapper(cls: CT) -> CT:
            """Decorates a class as experimental.

            This decorator is supposed to be applied to the experimental class.
            """
            _original_init = getattr(cls, "__init__")
            _original_name = getattr(cls, "__name__")

            @functools.wraps(_original_init)
            def wrapped_init(self: Any, *args: Any, **kwargs: Any) -> None:
                warnings.warn(
                    f"{name if name is not None else _original_name} "
                    f"is experimental (supported from v{version}). "
                    "The interface can change in the future.",
                    ExperimentalWarning,
                    stacklevel=2,
                )

                _original_init(self, *args, **kwargs)

            setattr(cls, "__init__", wrapped_init)

            if cls.__doc__ is None:
                cls.__doc__ = ""

            note = _EXPERIMENTAL_NOTE_TEMPLATE.format(ver=version)
            indent = _get_docstring_indent(cls.__doc__)
            cls.__doc__ = cls.__doc__.strip() + textwrap.indent(note, indent) + indent

            return cls

        return wrapper(cls)

    return decorator
