import copy
import itertools
import json
import logging
import urllib.parse
from typing import List, Dict, Union, Any

from django.conf import settings
from django.contrib.admin import BooleanFieldListFilter
from django.contrib.admin.helpers import AdminForm
from django.contrib.admin.models import LogEntry
from django.contrib.admin.views.main import PAGE_VAR, ChangeList
from django.contrib.auth import get_user_model
from django.contrib.auth.context_processors import PermWrapper
from django.contrib.auth.models import AbstractUser
from django.db.models.base import ModelBase
from django.http import HttpRequest
from django.template import Library, Context
from django.template.loader import get_template
from django.templatetags.static import static
from django.utils import translation
from django.utils.html import format_html
from django.utils.safestring import mark_safe, SafeText
from django.utils.text import get_text_list, slugify
from django.utils.translation import gettext

from .. import version
from ..settings import get_settings, get_ui_tweaks, CHANGEFORM_TEMPLATES
from ..utils import (
    order_with_respect_to,
    get_filter_id,
    get_admin_url,
    make_menu,
    has_fieldsets_check,
)

User = get_user_model()
register = Library()
logger = logging.getLogger(__name__)


@register.simple_tag(takes_context=True)
def get_side_menu(context: Context, using: str = "available_apps") -> List[Dict]:
    """
    Get the list of apps and models to render out in the side menu and on the dashboard page

    N.B - Permissions are not checked here, as context["available_apps"] has already been filtered by django
    """
    user = context.get("user")
    if not user:
        return []

    options = get_settings()
    ordering = options.get("order_with_respect_to", [])
    ordering = [x.lower() for x in ordering]

    menu = []
    available_apps = copy.deepcopy(context.get(using, []))

    custom_links = {
        app_name: make_menu(user, links, options, allow_appmenus=False)
        for app_name, links in options.get("custom_links", {}).items()
    }

    for app in available_apps:
        app_label = app["app_label"].lower()
        app_custom_links = custom_links.get(app_label, [])
        app["icon"] = options["icons"].get(app_label, options["default_icon_parents"])
        if app_label in options["hide_apps"]:
            continue

        menu_items = []
        for model in app.get("models", []):
            model_str = "{app_label}.{model}".format(app_label=app_label, model=model["object_name"]).lower()
            if model_str in options.get("hide_models", []):
                continue

            model["url"] = model["admin_url"]
            model["model_str"] = model_str
            model["icon"] = options["icons"].get(model_str, options["default_icon_children"])
            menu_items.append(model)

        menu_items.extend(app_custom_links)

        custom_link_names = [x.get("name", "").lower() for x in app_custom_links]
        model_ordering = list(
            filter(lambda x: x.lower().startswith("{}.".format(app_label)) or x.lower() in custom_link_names, ordering)
        )

        if len(menu_items):
            if model_ordering:
                menu_items = order_with_respect_to(
                    menu_items, model_ordering, getter=lambda x: x.get("model_str", x.get("name", "").lower())
                )
            app["models"] = menu_items
            menu.append(app)

    if ordering:
        apps_order = list(filter(lambda x: "." not in x, ordering))
        menu = order_with_respect_to(menu, apps_order, getter=lambda x: x["app_label"].lower())

    return menu


@register.simple_tag
def get_top_menu(user: AbstractUser, admin_site: str = "admin") -> List[Dict]:
    """
    Produce the menu for the top nav bar
    """
    options = get_settings()
    return make_menu(user, options.get("topmenu_links", []), options, allow_appmenus=True, admin_site=admin_site)


@register.simple_tag
def get_user_menu(user: AbstractUser, admin_site: str = "admin") -> List[Dict]:
    """
    Produce the menu for the user dropdown
    """
    options = get_settings()
    return make_menu(user, options.get("usermenu_links", []), options, allow_appmenus=False, admin_site=admin_site)


@register.simple_tag
def get_jazzmin_settings() -> Dict:
    """
    Return Jazzmin settings
    """
    return get_settings()


@register.simple_tag
def get_jazzmin_ui_tweaks() -> Dict:
    """
    Return Jazzmin ui tweaks
    """
    return get_ui_tweaks()


@register.simple_tag
def get_jazzmin_version() -> str:
    """
    Get the version for this package
    """
    return version


@register.simple_tag
def get_user_avatar(user: AbstractUser) -> str:
    """
    For the given user, try to get the avatar image
    """
    no_avatar = static("adminlte/img/user2-160x160.jpg")
    options = get_settings()

    if not options.get("user_avatar"):
        return no_avatar

    avatar_field = getattr(user, options["user_avatar"], None)
    if avatar_field:
        return avatar_field.url

    return no_avatar


@register.simple_tag
def jazzmin_paginator_number(cl: ChangeList, i: int) -> SafeText:
    """
    Generate an individual page index link in a paginated list.
    """
    if i == ".":
        html_str = """
            <li class="page-item">
            <a class="page-link" href="javascript:void(0);" data-dt-idx="3" tabindex="0">… </a>
            </li>
        """

    elif i == cl.page_num:
        html_str = """
            <li class="page-item active">
            <a class="page-link" href="javascript:void(0);" data-dt-idx="3" tabindex="0">{num}
            </a>
            </li>
        """.format(
            num=i + 1
        )

    else:
        query_string = cl.get_query_string({PAGE_VAR: i})
        end = mark_safe("end" if i == cl.paginator.num_pages - 1 else "")
        html_str = """
            <li class="page-item">
            <a href="{query_string}" class="page-link {end}" data-dt-idx="3" tabindex="0">{num}</a>
            </li>
        """.format(
            num=i + 1, query_string=query_string, end=end
        )

    return format_html(html_str)


@register.simple_tag
def admin_extra_filters(cl: ChangeList) -> Dict:
    """
    Return the dict of used filters which is not included in list_filters form
    """
    used_parameters = list(itertools.chain(*(s.used_parameters.keys() for s in cl.filter_specs)))
    return dict((k, v) for k, v in cl.params.items() if k not in used_parameters)


@register.simple_tag
def jazzmin_list_filter(cl: ChangeList, spec: BooleanFieldListFilter) -> SafeText:
    tpl = get_template(spec.template)
    choices = list(spec.choices(cl))
    field_key = get_filter_id(spec)
    matched_key = field_key
    for choice in choices:
        query_string = choice["query_string"][1:]
        query_parts = urllib.parse.parse_qs(query_string)

        value = ""
        matches = {}
        for key in query_parts.keys():
            if key == field_key:
                value = query_parts[key][0]
                matched_key = key
            elif key.startswith(field_key + "__") or "__" + field_key + "__" in key:
                value = query_parts[key][0]
                matched_key = key

            if value:
                matches[matched_key] = value

        # Iterate matches, use original as actual values, additional for hidden
        i = 0
        for key, value in matches.items():
            if i == 0:
                choice["name"] = key
                choice["value"] = value
            i += 1

    return tpl.render({"field_name": field_key, "title": spec.title, "choices": choices, "spec": spec,})


@register.simple_tag
def jazzy_admin_url(value: Union[str, ModelBase], admin_site: str = "admin") -> str:
    """
    Get the admin url for a given object
    """
    return get_admin_url(value, admin_site=admin_site)


@register.filter
def has_fieldsets(adminform: AdminForm) -> bool:
    """
    Do we have fieldsets
    """
    return has_fieldsets_check(adminform)


@register.filter
def change_lang(request: HttpRequest, language_code: str) -> str:
    """
    Change the url to use the given language
    """
    current_language = translation.get_language()
    return request.get_full_path().replace(current_language, language_code)


@register.filter
def debug(value: Any) -> Any:
    """
    Add in a breakpoint here and use filter in templates for debugging ;)
    """
    return type(value)


@register.filter
def as_json(value: Union[List, Dict]) -> str:
    """
    Take the given item and dump it out as JSON
    """
    return json.dumps(value)


@register.simple_tag
def get_changeform_template(adminform: AdminForm) -> str:
    """
    Go get the correct change form template based on the modeladmin being used,
    the default template, or the overridden one for this modeladmin
    """
    options = get_settings()
    has_fieldsets = has_fieldsets_check(adminform)
    inlines = adminform.model_admin.inlines
    has_inlines = inlines and len(inlines) > 0
    model = adminform.model_admin.model
    model_name = "{}.{}".format(model._meta.app_label, model._meta.model_name).lower()

    format = options.get("changeform_format", "")
    if model_name in options.get("changeform_format_overrides", {}):
        format = options["changeform_format_overrides"][model_name]

    if not has_fieldsets and not has_inlines:
        return CHANGEFORM_TEMPLATES.get("single")

    if not format or format not in CHANGEFORM_TEMPLATES.keys():
        return CHANGEFORM_TEMPLATES.get("horizontal_tabs")

    return CHANGEFORM_TEMPLATES.get(format)


@register.simple_tag
def sidebar_status(request: HttpRequest) -> str:
    """
    Check if our sidebar is open or closed
    """
    if request.COOKIES.get("jazzy_menu", "") == "closed":
        return "sidebar-collapse"
    return ""


@register.filter
def can_view_self(perms: PermWrapper) -> bool:
    """
    Determines whether a user has sufficient permissions to view its own profile
    """
    view_perm = "view_{}".format(User._meta.model_name)
    return perms[User._meta.app_label][view_perm]


@register.simple_tag
def header_class(header: Dict, forloop: Dict) -> str:
    """
    Adds CSS classes to header HTML element depending on its attributes
    """
    classes = []
    sorted, asc, desc = (
        header.get("sorted"),
        header.get("ascending"),
        header.get("descending"),
    )

    if forloop["counter0"] == 0:
        classes.append("djn-checkbox-select-all")

    if not header["sortable"]:
        return " ".join(classes)

    if sorted and asc:
        classes.append("sorting_asc")
    elif sorted and desc:
        classes.append("sorting_desc")
    else:
        classes.append("sorting")

    return " ".join(classes)


@register.filter
def app_is_installed(app: str) -> bool:
    """
    Checks if an app has been installed under INSTALLED_APPS on the project settings
    """
    return app in settings.INSTALLED_APPS


@register.simple_tag
def action_message_to_list(action: LogEntry) -> List[Dict]:
    """
    Retrieves a formatted list with all actions taken by a user given a log entry object
    """
    messages = []

    def added(x: str) -> Dict:
        return {
            "msg": x,
            "icon": "plus-circle",
            "colour": "success",
        }

    def changed(x: str) -> Dict:
        return {
            "msg": x,
            "icon": "edit",
            "colour": "blue",
        }

    def deleted(x: str) -> Dict:
        return {
            "msg": x,
            "icon": "trash",
            "colour": "danger",
        }

    if action.change_message and action.change_message[0] == "[":
        try:
            change_message = json.loads(action.change_message)
        except json.JSONDecodeError:
            return [action.change_message]

        for sub_message in change_message:
            if "added" in sub_message:
                if sub_message["added"]:
                    sub_message["added"]["name"] = gettext(sub_message["added"]["name"])
                    messages.append(added(gettext("Added {name} “{object}”.").format(**sub_message["added"])))
                else:
                    messages.append(added(gettext("Added.")))

            elif "changed" in sub_message:
                sub_message["changed"]["fields"] = get_text_list(
                    [gettext(field_name) for field_name in sub_message["changed"]["fields"]], gettext("and"),
                )
                if "name" in sub_message["changed"]:
                    sub_message["changed"]["name"] = gettext(sub_message["changed"]["name"])
                    messages.append(changed(gettext("Changed {fields}.").format(**sub_message["changed"])))
                else:
                    messages.append(changed(gettext("Changed {fields}.").format(**sub_message["changed"])))

            elif "deleted" in sub_message:
                sub_message["deleted"]["name"] = gettext(sub_message["deleted"]["name"])
                messages.append(deleted(gettext("Deleted “{object}”.").format(**sub_message["deleted"])))

    return messages if len(messages) else [changed(gettext(action.change_message))]


@register.filter
def style_bold_first_word(message: str) -> SafeText:
    """
    Wraps first word in a message with <strong> HTML element
    """
    message_words = message.split()

    if not len(message_words):
        return ""

    message_words[0] = "<strong>{}</strong>".format(message_words[0])

    message = " ".join([word for word in message_words])

    return format_html(message)


@register.filter
def unicode_slugify(message: str) -> str:
    return slugify(message, allow_unicode=True)
