from typing import Any, LiteralString, Protocol, Unpack

from django.conf import settings
from django.core.exceptions import ImproperlyConfigured
from django.http import HttpRequest
from django.template.loader import render_to_string
from django.utils.safestring import SafeString
from django.views import View
from django.views.generic.base import ContextMixin, TemplateResponseMixin


class TabedViewMixin(View):
    """Basic functions for displaying tabs in the template."""

    def get_tabs_title(self):
        if hasattr(self, "tabs_title"):
            return self.tabs_title
        raise ImproperlyConfigured("tabs_title is required")

    def get_current_tab(self):
        if hasattr(self, "current_tab"):
            return self.current_tab
        raise ImproperlyConfigured("current_tab is required")

    def get_list_of_tabs(self):
        if hasattr(self, "list_of_tabs"):
            return self.list_of_tabs
        raise ImproperlyConfigured("list_of_tabs is required")

    def get_context_data(self, **kwargs):
        kwargs = super().get_context_data(**kwargs)
        kwargs["tabs_title"] = self.get_tabs_title()
        kwargs["current_tab"] = self.get_current_tab()
        kwargs["list_of_tabs"] = self.get_list_of_tabs()
        return kwargs


class QuickNotifMixin:
    quick_notif_list = []

    def dispatch(self, request, *arg, **kwargs):
        # In some cases, the class can stay instanciated, so we need to reset the list
        self.quick_notif_list = []
        return super().dispatch(request, *arg, **kwargs)

    def get_success_url(self):
        ret = super().get_success_url()
        if hasattr(self, "quick_notif_url_arg"):
            if "?" in ret:
                ret += "&" + self.quick_notif_url_arg
            else:
                ret += "?" + self.quick_notif_url_arg
        return ret

    def get_context_data(self, **kwargs):
        """Add quick notifications to context."""
        kwargs = super().get_context_data(**kwargs)
        kwargs["quick_notifs"] = []
        for n in self.quick_notif_list:
            kwargs["quick_notifs"].append(settings.SITH_QUICK_NOTIF[n])
        for key, val in settings.SITH_QUICK_NOTIF.items():
            for gk in self.request.GET:
                if key == gk:
                    kwargs["quick_notifs"].append(val)
        return kwargs


class AllowFragment:
    """Add `is_fragment` to templates. It's only True if the request is emitted by htmx"""

    def get_context_data(self, **kwargs):
        kwargs["is_fragment"] = self.request.headers.get("HX-Request", False)
        return super().get_context_data(**kwargs)


class FragmentRenderer(Protocol):
    def __call__(
        self, request: HttpRequest, **kwargs: Unpack[dict[str, Any]]
    ) -> SafeString: ...


class FragmentMixin(TemplateResponseMixin, ContextMixin):
    """Make a view buildable as a fragment that can be embedded in a template.

    Most fragments are used in two different ways :
    - in the request/response cycle, like any regular view
    - in templates, where the rendering is done in another view

    This mixin aims to simplify the initial fragment rendering.
    The rendered fragment will then be able to re-render itself
    through the request/response cycle if it uses HTMX.

    !!!Example
        ```python
        class MyFragment(FragmentMixin, FormView):
            template_name = "app/fragment.jinja"
            form_class = MyForm
            success_url = reverse_lazy("foo:bar")

        # in another view :
        def some_view(request):
            fragment = MyFragment.as_fragment()
            return render(
                request,
                "app/template.jinja",
                context={"fragment": fragment(request)
            }

        # in urls.py
        urlpatterns = [
            path("foo/view", some_view),
            path("foo/fragment", MyFragment.as_view()),
        ]
        ```
    """

    @classmethod
    def as_fragment(cls, **initkwargs) -> FragmentRenderer:
        # the following code is heavily inspired from the base View.as_view method
        for key in initkwargs:
            if not hasattr(cls, key):
                raise TypeError(
                    "%s() received an invalid keyword %r. as_view "
                    "only accepts arguments that are already "
                    "attributes of the class." % (cls.__name__, key)
                )

        def fragment(request: HttpRequest, **kwargs) -> SafeString:
            self = cls(**initkwargs)
            self.request = request
            self.kwargs = kwargs
            return self.render_fragment(request, **kwargs)

        fragment.__doc__ = cls.__doc__
        fragment.__module__ = cls.__module__
        return fragment

    def render_fragment(self, request, **kwargs) -> SafeString:
        return render_to_string(
            self.get_template_names(),
            context=self.get_context_data(**kwargs),
            request=request,
        )


class UseFragmentsMixin(ContextMixin):
    """Mark a view as using fragments.

    This mixin is not mandatory
    (you may as well render manually your fragments in the `get_context_data` method).
    However, the interface of this class bring some distinction
    between fragments and other context data, which may
    reduce boilerplate.

    !!!Example
        ```python
        class FooFragment(FragmentMixin, FormView): ...

        class BarFragment(FragmentMixin, FormView): ...

        class AdminFragment(FragmentMixin, FormView): ...

        class MyView(UseFragmentsMixin, TemplateView)
            template_name = "app/view.jinja"
            fragments = {
                "foo": FooFragment
                "bar": BarFragment(template_name="some_template.jinja")
            }
            fragments_data = {
                "foo": {"some": "data"}  # this will be passed to the FooFragment renderer
            }

            def get_fragments(self):
                res = super().get_fragments()
                if self.request.user.is_superuser:
                    res["admin_fragment"] = AdminFragment
                return res
        ```
    """

    fragments: dict[LiteralString, type[FragmentMixin] | FragmentRenderer] | None = None
    fragment_data: dict[LiteralString, dict[LiteralString, Any]] | None = None

    def get_fragments(self) -> dict[str, type[FragmentMixin] | FragmentRenderer]:
        return self.fragments if self.fragments is not None else {}

    def get_fragment_data(self) -> dict[str, dict[str, Any]]:
        """Return eventual data used to initialize the fragments."""
        return self.fragment_data if self.fragment_data is not None else {}

    def get_fragment_context_data(self) -> dict[str, SafeString]:
        """Return the rendered fragments as context data."""
        res = {}
        data = self.get_fragment_data()
        for name, fragment in self.get_fragments().items():
            _fragment = (
                fragment.as_fragment() if fragment is FragmentMixin else fragment
            )
            fragment_data = data.get(name, {})
            res[name] = _fragment(self.request, **fragment_data)
        return res

    def get_context_data(self, **kwargs):
        kwargs = super().get_context_data(**kwargs)
        kwargs.update(self.get_fragment_context_data())
        return kwargs