from typing import Dict, List, Optional

import shortuuid
from django import template
from django.conf import settings
from django.template.base import FilterExpression

from django_unicorn.call_method_parser import InvalidKwargError, parse_kwarg
from django_unicorn.errors import ComponentNotValidError
from django_unicorn.settings import get_morpher_settings

register = template.Library()


MINIMUM_ARGUMENT_COUNT = 2


@register.inclusion_tag("unicorn/scripts.html")
def unicorn_scripts():
    # Import here to prevent the potential of this loading before Django settings
    from django_unicorn.settings import get_setting

    csrf_header_name = settings.CSRF_HEADER_NAME

    if csrf_header_name.startswith("HTTP_"):
        csrf_header_name = settings.CSRF_HEADER_NAME[5:]

    csrf_header_name = csrf_header_name.replace("_", "-")

    csrf_cookie_name = settings.CSRF_COOKIE_NAME

    return {
        "MINIFIED": get_setting("MINIFIED", not settings.DEBUG),
        "CSRF_HEADER_NAME": csrf_header_name,
        "CSRF_COOKIE_NAME": csrf_cookie_name,
        "MORPHER": get_morpher_settings(),
    }


@register.inclusion_tag("unicorn/errors.html", takes_context=True)
def unicorn_errors(context):
    return {"unicorn": {"errors": context.get("unicorn", {}).get("errors", {})}}


def unicorn(parser, token):
    contents = token.split_contents()

    if len(contents) < MINIMUM_ARGUMENT_COUNT:
        first_arg = token.contents.split()[0]
        raise template.TemplateSyntaxError(f"{first_arg} tag requires at least a single argument")

    component_name = parser.compile_filter(contents[1])

    args = []
    kwargs = {}
    unparseable_kwargs = {}

    for arg in contents[2:]:
        try:
            parsed_kwarg = parse_kwarg(arg, raise_if_unparseable=True)
            kwargs.update(parsed_kwarg)
        except InvalidKwargError:
            # Assume it's an arg if invalid kwarg and kwargs is empty
            if not kwargs:
                args.append(arg)
        except ValueError:
            parsed_kwarg = parse_kwarg(arg, raise_if_unparseable=False)
            unparseable_kwargs.update(parsed_kwarg)

    return UnicornNode(component_name, args, kwargs, unparseable_kwargs)


class UnicornNode(template.Node):
    def __init__(
        self,
        component_name: FilterExpression,
        args: Optional[List] = None,
        kwargs: Optional[Dict] = None,
        unparseable_kwargs: Optional[Dict] = None,
    ):
        self.component_name = component_name
        self.args = args if args is not None else []
        self.kwargs = kwargs if kwargs is not None else {}
        self.unparseable_kwargs = unparseable_kwargs if unparseable_kwargs is not None else {}
        self.component_key = ""
        self.parent = None

    def render(self, context):
        request = None

        if hasattr(context, "request"):
            request = context.request

        from django_unicorn.components import UnicornView

        resolved_args = []

        for value in self.args:
            resolved_arg = template.Variable(value).resolve(context)
            resolved_args.append(resolved_arg)

        resolved_kwargs = self.kwargs.copy()

        for key, value in self.unparseable_kwargs.items():
            try:
                resolved_value = template.Variable(value).resolve(context)

                if key == "parent" and value == "view" and not isinstance(resolved_value, UnicornView):
                    # Handle rendering a parent component from a template that is called from
                    # a `TemplateView`; for some reason `view` is clobbered in this instance, but
                    # the `unicorn` dictionary has enough data to instantiate a `UnicornView`
                    parent_component_data = template.Variable("unicorn").resolve(context)

                    resolved_value = UnicornView(
                        component_name=parent_component_data.get("component_name"),
                        component_id=parent_component_data.get("component_id"),
                    )

                resolved_kwargs.update({key: resolved_value})
            except TypeError:
                resolved_kwargs.update({key: value})
            except template.VariableDoesNotExist:
                if value.endswith(".id"):
                    pk_val = value.replace(".id", ".pk")

                    try:
                        resolved_kwargs.update({key: template.Variable(pk_val).resolve(context)})
                    except TypeError:
                        resolved_kwargs.update({key: value})
                    except template.VariableDoesNotExist:
                        pass

        if "key" in resolved_kwargs:
            self.component_key = resolved_kwargs.pop("key")

        if "parent" in resolved_kwargs:
            self.parent = resolved_kwargs.pop("parent")
        else:
            # if there is no explicit parent, but this node is rendering under an existing
            # unicorn template, set that as the parent
            try:
                implicit_parent = template.Variable("unicorn.component").resolve(context)
                if implicit_parent:
                    self.parent = implicit_parent
            except template.VariableDoesNotExist:
                pass  # no implicit parent present

        component_id = None

        try:
            component_name = self.component_name.resolve(context)
        except AttributeError as e:
            raise ComponentNotValidError(f"Component template is not valid: {self.component_name}.") from e

        if self.parent:
            # Child components use the parent for part of the `component_id`
            component_id = f"{self.parent.component_id}:{component_name}"

            if self.component_key:
                component_id = f"{component_id}:{self.component_key}"
            elif "id" in resolved_kwargs:
                kwarg_id = resolved_kwargs["id"]
                component_id = f"{component_id}:{kwarg_id}"
            elif "pk" in resolved_kwargs:
                kwarg_pk = resolved_kwargs["pk"]
                component_id = f"{component_id}:{kwarg_pk}"
            elif "model" in resolved_kwargs:
                model = resolved_kwargs["model"]

                if hasattr(model, "pk"):
                    component_id = f"{component_id}:{model.pk}"
                elif hasattr(model, "id"):
                    component_id = f"{component_id}:{model.id}"

        if component_id:
            if not settings.DEBUG:
                component_id = shortuuid.uuid(name=component_id)[:8]
        else:
            component_id = shortuuid.uuid()[:8]

        # Useful for unit test
        self.component_id = component_id

        self.view = UnicornView.create(
            component_id=component_id,
            component_name=component_name,
            component_key=self.component_key,
            parent=self.parent,
            request=request,
            component_args=resolved_args,
            kwargs=resolved_kwargs,
        )

        extra_context = {}
        for c in context:
            extra_context.update(c)

        rendered_component = self.view.render(init_js=True, extra_context=extra_context)

        return rendered_component


register.tag("unicorn", unicorn)
