# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at https://mozilla.org/MPL/2.0/.

from os.path import relpath, splitext

from django.conf import settings
from django.http import HttpResponsePermanentRedirect, HttpResponseRedirect
from django.shortcuts import render as django_render
from django.template import TemplateDoesNotExist, loader
from django.utils.translation.trans_real import parse_accept_lang_header
from django.views.generic import TemplateView

from product_details import product_details

from bedrock.base import metrics
from bedrock.base.i18n import normalize_language, split_path_and_normalize_language
from bedrock.settings.base import language_url_map_with_fallbacks

from .fluent import fluent_l10n, ftl_file_is_active, get_active_locales as ftl_active_locales


def template_source_url(template):
    if template in settings.EXCLUDE_EDIT_TEMPLATES:
        return None

    if template.split("/")[0] in settings.EXCLUDE_EDIT_TEMPLATES_DIRECTORIES:
        return None

    try:
        absolute_path = loader.get_template(template).template.filename
    except TemplateDoesNotExist:
        return None

    relative_path = relpath(absolute_path, settings.ROOT)
    return f"{settings.GITHUB_REPO}/tree/master/{relative_path}"


def render_to_string(template_name, context=None, request=None, using=None, ftl_files=None):
    if request:
        context = context or {}
        locale = get_locale(request)
        if ftl_files:
            if isinstance(ftl_files, str):
                ftl_files = [ftl_files]

            # do not use list.extend() or += here to avoid modifying
            # the original list passed to the function
            ftl_files = ftl_files + settings.FLUENT_DEFAULT_FILES

            context["fluent_l10n"] = fluent_l10n([locale, "en"], ftl_files)
        else:
            context["fluent_l10n"] = fluent_l10n([locale, "en"], settings.FLUENT_DEFAULT_FILES)

        context["fluent_files"] = ftl_files or settings.FLUENT_DEFAULT_FILES
    return loader.render_to_string(template_name, context, request, using)


def is_root_path_with_no_language_clues(request):
    return request.path_info == "/" and not request.headers.get("Accept-Language")


def redirect_to_best_locale(request, translations):
    # Strict only for the root URL when we have no language clues
    strict = is_root_path_with_no_language_clues(request)
    # Note that translations is list of locale strings (eg ["en-GB", "ru", "fr"])
    locale = get_best_translation(translations, get_accept_languages(request), strict)

    if locale:
        return redirect_to_locale(request, locale)
    return locale_selection(request, translations)


def redirect_to_locale(request, locale, permanent=False):
    redirect_class = HttpResponsePermanentRedirect if permanent else HttpResponseRedirect
    original_prefix, subpath, _ = split_path_and_normalize_language(request.get_full_path())
    response = redirect_class("/" + "/".join([locale, subpath]))
    # Record count of redirects to this locale.
    metrics.incr("locale.redirect", tags=[f"from_locale:{original_prefix or 'none'}", f"to_locale:{locale}"])
    # Add the Vary header to avoid wrong redirects due to a cache
    response["Vary"] = "Accept-Language"
    return response


def locale_selection(request, available_locales=None):
    # We want the root path to return a 200 and slightly adjusted content for search engines.
    is_root = request.path_info == "/"
    has_header = request.headers.get("Accept-Language") is not None
    # If `settings.DEV` is true, make `available_locales` all available locales for l10n testing.
    # Or if empty, set it to at least en-US.
    if not available_locales:
        available_locales = ["en-US"]
    if settings.DEV:
        available_locales = settings.DEV_LANGUAGES

    context = {
        "is_root": is_root,
        "has_header": has_header,
        "fluent_l10n": fluent_l10n(["en"], settings.FLUENT_DEFAULT_FILES),
        "languages": product_details.languages,
        "available_locales": sorted(set(available_locales)),
    }
    response = django_render(request, "404-locale.html", context, status=200 if is_root else 404)
    # Add the Vary header to avoid improper cache
    response["Vary"] = "Accept-Language"
    return response


def render(request, template, context=None, ftl_files=None, activation_files=None, **kwargs):
    """
    Same as django's render() shortcut, but with l10n template support.
    If used like this::

        return l10n_utils.render(request, 'myapp/mytemplate.html')

    ... this helper will render the following template::

        l10n/LANG/myapp/mytemplate.html

    if present, otherwise, it'll render the specified (en-US) template.
    """
    # use copy() here to avoid modifying the dict in a view that will then
    # be different on the next call to the view.
    context = context.copy() if context else {}
    l10n = None
    ftl_files = ftl_files or context.get("ftl_files")
    locale = get_locale(request)

    # is this a non-locale page?
    name_prefix = request.path_info.split("/", 2)[1]
    non_locale_url = name_prefix in settings.SUPPORTED_NONLOCALES or request.path_info in settings.SUPPORTED_LOCALE_IGNORE

    # is this a CMS page?
    is_cms_page = hasattr(request, "is_cms_page") and request.is_cms_page

    # Make sure we have a single template
    if isinstance(template, list):
        template = template[0]

    if ftl_files:
        if isinstance(ftl_files, str):
            ftl_files = [ftl_files]

        # do not use list.extend() or += here to avoid modifying
        # the original list passed to the function
        ftl_files = ftl_files + settings.FLUENT_DEFAULT_FILES

        context["fluent_l10n"] = l10n = fluent_l10n([locale, "en"], ftl_files)
    else:
        context["fluent_l10n"] = fluent_l10n([locale, "en"], settings.FLUENT_DEFAULT_FILES)

    context["fluent_files"] = ftl_files or settings.FLUENT_DEFAULT_FILES
    context["template"] = template
    context["template_source_url"] = template_source_url(template)

    # if it's a CMS page, draw the active locales from the Page data.
    # if `active_locales` is given use it as the full list of active translations
    translations = []

    if is_cms_page and request._locales_available_via_cms:
        translations = request._locales_available_via_cms
    elif "active_locales" in context:
        translations = context["active_locales"]
        del context["active_locales"]
    else:
        if activation_files:
            translations = set()
            for af in activation_files:
                translations.update(ftl_active_locales(af))
            translations = sorted(translations)  # `sorted` returns a list.
        elif l10n:
            translations = l10n.active_locales

        # if `add_active_locales` is given then add it to the translations for the template
        if "add_active_locales" in context:
            translations.extend(context["add_active_locales"])
            del context["add_active_locales"]

        if not translations:
            translations = [settings.LANGUAGE_CODE]

    context["translations"] = get_translations_native_names(translations)

    # Ensure the path requires a locale prefix.
    if not non_locale_url:
        # If the requested path's locale is different from the best matching
        # locale stored on the `request`, and that locale is one of the active
        # translations, redirect to it. Otherwise we need to find the best
        # matching locale.

        # Does that path's locale match the request's locale?
        # AND is it NOT for the root path with no discernable lang?
        if locale in translations and not is_root_path_with_no_language_clues(request):
            # Redirect to the locale if:
            # - The URL is the root path but is missing the trailing slash OR
            # - The locale isn't the current prefix in the URL
            if request.path == f"/{locale}" or locale != request.path.lstrip("/").partition("/")[0]:
                return redirect_to_locale(request, locale)
        else:
            return redirect_to_best_locale(request, translations)

        # Look for locale-specific template in app/templates/
        locale_tmpl = f".{locale}".join(splitext(template))
        try:
            return django_render(request, locale_tmpl, context, **kwargs)
        except TemplateDoesNotExist:
            pass

    # Render originally requested/default template.
    return django_render(request, template, context, **kwargs)


def get_locale(request):
    # request.locale is added in bedrock.base.middleware.BedrockLangCodeFixupMiddleware
    lang = getattr(request, "locale", None)
    if not lang:
        lang = settings.LANGUAGE_CODE
    return normalize_language(lang)


def get_accept_languages(request):
    """
    Parse the user's Accept-Language HTTP header and return a list of languages in ranked order.
    """
    ranked = parse_accept_lang_header(request.headers.get("Accept-Language", ""))
    return [lang for lang, rank in ranked]


def get_best_translation(translations, accept_languages, strict=False):
    """
    Return the best translation available comparing the accept languages against available translations.

    This attempts to find a matching translation for each accept language. It
    compares each accept language in full, and also the root. For example,
    "en-CA" looks for "en-CA" as well as "en", which maps to "en-US".

    If none found, it returns the first language code for the first available translation.

    """
    lang_map = language_url_map_with_fallbacks()
    # translations contains mixed-case items e.g. "en-US" and the keys
    # of `lang_map` are (now) also mixed case.
    valid_lang_map = {k: v for k, v in lang_map.items() if v in translations}
    for lang in accept_languages:
        if lang in valid_lang_map:
            return valid_lang_map[lang]
        pre = lang.split("-")[0]
        if pre in valid_lang_map:
            return valid_lang_map[pre]

    if strict:
        # We couldn't find a best locale to return so we return `None`.
        return None
    else:
        # Use the default locale if it is an available translation.
        if settings.LANGUAGE_CODE in translations:
            return settings.LANGUAGE_CODE

        # In the rare case the default language isn't in the list,
        # return the first translation in the valid_lang_map.
        return list(valid_lang_map.values())[0]


def get_translations_native_names(locales):
    """
    Return a dict of locale codes and native language name strings.

    Returned dict is suitable for use in view contexts and is filtered to only codes in PROD_LANGUAGES.

    :param locales: list of locale codes
    :return: dict, like {'en-US': 'English (US)', 'fr': 'Français'}
    """
    translations = {}
    for locale in locales:
        if locale in settings.PROD_LANGUAGES:
            language = product_details.languages.get(locale)
            translations[locale] = language["native"] if language else locale

    return translations


class LangFilesMixin:
    """Generic views mixin that uses l10n_utils to render responses."""

    old_template_name = None
    active_locales = None
    add_active_locales = None
    # a list of ftl files to use or a single ftl filename
    ftl_files = None
    # a dict of template names to ftl files
    ftl_files_map = None
    # a list of ftl or template files to use to determine the full list of active locales
    # mostly useful during a redesign where multiple templates are used for a single URL
    activation_files = None

    def get_context_data(self, **kwargs):
        ctx = super().get_context_data(**kwargs)
        if self.active_locales:
            ctx["active_locales"] = self.active_locales
        if self.add_active_locales:
            ctx["add_active_locales"] = self.add_active_locales

        return ctx

    def get_ftl_files(self, template_names):
        if self.ftl_files:
            return self.ftl_files

        if self.ftl_files_map:
            return self.ftl_files_map.get(template_names[0])

        return None

    def render_to_response(self, context, **response_kwargs):
        template_names = self.get_template_names()
        return render(
            self.request,
            template_names,
            context,
            ftl_files=self.get_ftl_files(template_names),
            activation_files=self.activation_files,
            **response_kwargs,
        )

    def get_template_names(self):
        template_names = super().get_template_names()
        if self.old_template_name is None:
            return template_names

        ftl_files = self.get_ftl_files(template_names)
        if ftl_file_is_active(ftl_files[0]):
            return template_names

        return [self.old_template_name]


class RequireSafeMixin:
    http_method_names = ["get", "head"]


class L10nTemplateView(LangFilesMixin, RequireSafeMixin, TemplateView):
    pass
