"""Template tags for rendering the new side menu.

Documentation of the render logic
---------------------------------

The are 3 types of menu items:

- App entries: Generated by hooks from Django apps
- Link entries: Linking to external pages. User created.
- Folder: Grouping together several app or link entries. User created.

The MenuItem model holds the current list of all menu items.

App entries are linked to a `MenuItemHook` object in the respective Django app.
Those hook objects contain dynamic logic in a `render()` method,
which must be executed when rendering for the current request.

Since the same template must be used to render all items, link entries and folders
are converted to `MenuItemHookCustom` objects, a sub class of `MenuItemHook`.
This ensures the template only rendered objects of one specific type or sub-type.

The rendered menu items are finally collected in a list of RenderedMenuItem objects,
which is used to render the complete menu.
"""

from dataclasses import dataclass, field
from typing import Dict, List, Optional

from django import template
from django.db.models import QuerySet
from django.http import HttpRequest

from allianceauth.hooks import get_hooks
from allianceauth.menu.core import menu_item_hooks, smart_sync
from allianceauth.menu.models import MenuItem
from allianceauth.services.auth_hooks import MenuItemHook

register = template.Library()


@register.inclusion_tag("menu/menu-block.html", takes_context=True)
def menu_items(context: dict) -> dict:
    """Render menu items for new dashboards."""
    smart_sync.sync_menu()

    items = render_menu(context["request"])
    return {"menu_items": items}


@dataclass
class RenderedMenuItem:
    """A rendered menu item.

    These objects can be rendered with the menu-block template.
    """

    menu_item: MenuItem

    children: List["RenderedMenuItem"] = field(default_factory=list)
    count: Optional[int] = None
    html: str = ""
    html_id: str = ""

    @property
    def is_folder(self) -> bool:
        """Return True if this item is a folder."""
        return self.menu_item.is_folder

    def update_html(self, request: HttpRequest, template: str):
        """Render this menu item with defaults and set HTML ID."""
        hook_obj = self.menu_item.to_hook_obj()
        hook_obj.template = template
        hook_obj.count = self.count
        if self.is_folder:
            hook_obj.children = [child.html for child in self.children]

        self.html = hook_obj.render(request)
        self.html_id = hook_obj.html_id


def render_menu(request: HttpRequest) -> List[RenderedMenuItem]:
    """Return the rendered side menu for including in a template.

    This function is creating BS5 style menus.
    """
    hook_items = _gather_menu_items_from_hooks()

    # Menu items needs to be rendered with the new BS5 template
    bs5_template = "menu/menu-item-bs5.html"

    rendered_items: Dict[int, RenderedMenuItem] = {}
    menu_items: QuerySet[MenuItem] = MenuItem.objects.order_by(
        "parent", "order", "text"
    )
    for item in menu_items:
        if item.is_hidden:
            continue  # do not render hidden items

        if item.is_app_item:
            rendered_item = _render_app_item(request, hook_items, item, bs5_template)
            if rendered_item.html == "":
                # If there is no content dont render it.
                # This item has probably been hidden by permissions
                continue
        elif item.is_link_item:
            rendered_item = _render_link_item(request, item, bs5_template)
        elif item.is_folder:
            rendered_item = RenderedMenuItem(item)  # we render these items later
        else:
            raise NotImplementedError("Unknown menu item type")

        if item.is_child:
            try:
                parent = rendered_items[item.parent_id]
            except KeyError:
                continue  # do not render children of hidden folders

            parent.children.append(rendered_item)
            if rendered_item.count is not None:
                if parent.count is None:
                    parent.count = 0
                parent.count += rendered_item.count

        else:
            rendered_items[item.id] = rendered_item

    _remove_empty_folders(rendered_items)

    _render_folder_items(request, rendered_items, bs5_template)

    return list(rendered_items.values())


def _gather_menu_items_from_hooks() -> Dict[str, MenuItemHook]:
    hook_items = {}
    for hook in get_hooks("menu_item_hook"):
        f = hook()
        hook_items[menu_item_hooks.generate_hash(f)] = f
    return hook_items


def _render_app_item(
    request: HttpRequest, hook_items: dict, item: MenuItem, new_template: str
) -> RenderedMenuItem:
    # This is a module hook, so we need to render it as the developer intended
    # TODO add a new attribute for apps that want to override it in the new theme
    hook_item = hook_items[item.hook_hash]
    hook_item.template = new_template
    html = hook_item.render(request)
    count = hook_item.count
    rendered_item = RenderedMenuItem(menu_item=item, count=count, html=html)
    return rendered_item


def _render_link_item(
    request: HttpRequest, item: MenuItem, new_template: str
) -> RenderedMenuItem:
    rendered_item = RenderedMenuItem(menu_item=item)
    rendered_item.update_html(request, template=new_template)
    return rendered_item


def _render_folder_items(
    request: HttpRequest, rendered_items: Dict[int, RenderedMenuItem], new_template: str
):
    for item in rendered_items.values():
        if item.menu_item.is_folder:
            item.update_html(request=request, template=new_template)


def _remove_empty_folders(rendered_items: Dict[int, RenderedMenuItem]):
    ids_to_remove = []
    for item_id, item in rendered_items.items():
        if item.is_folder and not item.children:
            ids_to_remove.append(item_id)

    for item_id in ids_to_remove:
        del rendered_items[item_id]
