from __future__ import annotations

import functools
import typing as t

import markupsafe

try:
    from warnings import deprecated  # type: ignore[attr-defined,unused-ignore]
except ImportError:
    from typing_extensions import deprecated

if t.TYPE_CHECKING:
    from collections.abc import AsyncIterator, Callable, Iterator, Mapping

    from . import Node, Renderable
    from ._contexts import Context


C = t.TypeVar("C", bound="Node")
P = t.ParamSpec("P")
R = t.TypeVar("R", bound="Renderable")


class _WithChildrenUnbound(t.Generic[C, P, R]):
    wrapped: Callable[t.Concatenate[C | None, P], R]

    def __init__(self, func: Callable[t.Concatenate[C | None, P], R]) -> None:
        # This instance is created at import time when decorating the component.
        # It means that this object is global, and shared between all renderings
        # of the same component.
        self.wrapped = func

    def __repr__(self) -> str:
        return f"with_children({self.wrapped.__name__}, <unbound>)"

    def __call__(self, *args: P.args, **kwargs: P.kwargs) -> _WithChildrenBound[C, P, R]:
        # This is the first call to the component, where we get the
        # component's args and kwargs:
        #
        #     my_component(title="My title")
        #
        # It is important that we return a new instance bound to the args
        # and kwargs instead of mutating, so that state doesn't leak between
        # multiple renderings of the same component.
        #
        return _WithChildrenBound(self.wrapped, args, kwargs)

    def __getitem__(self, children: C | None) -> R:
        # This is the unbound component being used with children:
        #
        #     my_component["My content"]
        #
        return self.wrapped(children)  # type: ignore[call-arg]

    def __str__(self) -> markupsafe.Markup:
        # This is the unbound component being rendered to a string:
        #
        #     str(my_component)
        #
        return markupsafe.Markup(self.wrapped(None))  # type: ignore[call-arg]

    __html__ = __str__

    @deprecated(
        "Calling .encode() is deprecated and will be removed in a future release. "
        "Using Starlette? Use htpy.starlette.HtpyResponse for improved performance and convenience. "  # noqa: E501
        "More info: https://htpy.dev/starlette/"
    )
    def encode(self, encoding: str = "utf-8", errors: str = "strict") -> bytes:
        return str(self).encode(encoding, errors)

    def iter_chunks(
        self,
        context: Mapping[htpy.Context[t.Any], t.Any] | None = None,
    ) -> Iterator[str]:
        return self.wrapped(None).iter_chunks(context)  # type: ignore[call-arg]

    def aiter_chunks(
        self,
        context: Mapping[htpy.Context[t.Any], t.Any] | None = None,
    ) -> AsyncIterator[str]:
        return self.wrapped(None).aiter_chunks(context)  # type: ignore[call-arg]


class _WithChildrenBound(t.Generic[C, P, R]):
    _func: Callable[t.Concatenate[C | None, P], R]
    _args: tuple[t.Any, ...]
    _kwargs: Mapping[str, t.Any]

    def __init__(
        self,
        func: Callable[t.Concatenate[C | None, P], R],
        args: tuple[t.Any, ...],
        kwargs: Mapping[str, t.Any],
    ) -> None:
        # This is called at runtime when the component is being passed args and
        # kwargs. This instance is only used for the current rendering of the
        # component.
        self._func = func
        self._args = args
        self._kwargs = kwargs

    def __repr__(self) -> str:
        return f"with_children({self._func.__name__}, {self._args}, {self._kwargs})"

    def __getitem__(self, children: C | None) -> R:
        # This is a bound component being used with children:
        #
        #     my_component(title="My title")["My content"]
        #
        return self._func(children, *self._args, **self._kwargs)

    def __str__(self) -> markupsafe.Markup:
        # This is a bound component being rendered to a string:
        #
        #     str(my_component(title="My title"))
        #
        return markupsafe.Markup(self._func(None, *self._args, **self._kwargs))

    __html__ = __str__

    @deprecated(
        "Calling .encode() is deprecated and will be removed in a future release. "
        "Using Starlette? Use htpy.starlette.HtpyResponse for improved performance and convenience. "  # noqa: E501
        "More info: https://htpy.dev/starlette/"
    )
    def encode(self, encoding: str = "utf-8", errors: str = "strict") -> bytes:
        return str(self).encode(encoding, errors)

    def iter_chunks(
        self,
        context: Mapping[htpy.Context[t.Any], t.Any] | None = None,
    ) -> Iterator[str]:
        return self._func(None, *self._args, **self._kwargs).iter_chunks(context)

    def aiter_chunks(
        self,
        context: Mapping[htpy.Context[t.Any], t.Any] | None = None,
    ) -> AsyncIterator[str]:
        return self._func(None, *self._args, **self._kwargs).aiter_chunks(context)


def with_children(
    func: Callable[t.Concatenate[C | None, P], R],
) -> _WithChildrenUnbound[C | None, P, R]:
    """Decorator to make a component support children nodes.

    This decorator allows you to create components that can accept children nodes
    using the bracket notation, similar to native htpy components.

    Example:
        @with_children
        def my_component(children: Node, *, title: str) -> Renderable:
            return div(class_="container")[h1[title], children]

        # Usage:
        my_component(title="Hello")[span["World"]]
    """
    wrapper = _WithChildrenUnbound(func)
    # Ensure the wrapper has the same signature as the original function
    # This is crucial for LSP to recognize the function arguments
    functools.update_wrapper(wrapper, func)
    return wrapper
