""" This module holds the utils of the ``translation`` app. """

import tempfile
from copy import copy

import polib

from django.conf import settings
from django.utils import translation

from mtp.apps.common.utils import popen_wrapper

from .exceptions import PoFileCheckError
from .models import TranslatableLanguage


def get_available_languages():
    languages = []
    for translatable_language in TranslatableLanguage.objects.enabled():
        languages.append(
            (translatable_language.language, translatable_language.get_language_display())
        )
    return languages


def get_available_languages_code():
    languages_code = []
    for translatable_language in TranslatableLanguage.objects.enabled():
        languages_code.append(translatable_language.language)
    return languages_code


def set_language(language_code, request, response):
    """Allow to set interface language according the language code provided."""

    translation.activate(language_code)

    if hasattr(request, 'session'):
        request.session[translation.LANGUAGE_SESSION_KEY] = language_code
    else:
        response.set_cookie(settings.LANGUAGE_COOKIE_NAME, language_code,
                            max_age=settings.LANGUAGE_COOKIE_AGE,
                            path=settings.LANGUAGE_COOKIE_PATH,
                            domain=settings.LANGUAGE_COOKIE_DOMAIN)


def check_po_file(pofile, max_passes=1, trust_line_numbers=True):
    """Check if a ``pofile`` (from ``polib``) is valid.

    Parameters
    ----------
    pofile : polib.POFile
        The pofile to check
    max_passes : int, default to ``1``
        The maximum number of passes to do a full check on this pofile, with each error from a
        pass being removed from the file to check on the next pass.
        It's useful if some errors block ``msgfmt`` to find other errors.
    trust_line_numbers : bool, default to ``True``
        If set to ``False``, we tell this function that we cannot trust the ``linenum`` attribute
        of each entry in ``pofile``. So the file will be saved on a temporary file, and reloaded
        to have a brand new ``pofile`` with correct line numbers

    Returns
    -------
    None

    Raises
    ------
    PoFileCheckError
        This exception is raised if at least an error is found on the file.
        It will contains all the found errors in the ``errors`` attribute.

    """

    # We create a tmp file for the pofile to have it read by ``msgfmt``
    with tempfile.NamedTemporaryFile(suffix='.po') as tmp_file:

        pofile.save(tmp_file.name)

        if not trust_line_numbers:
            # Reload pofile to have correct line numbers
            pofile = polib.pofile(tmp_file.name)

        # Call ``msgfmt`` to check the syntax of the file
        cmd_errors, cmd_status = popen_wrapper(
            ['msgfmt', '-v', '--check-format', tmp_file.name],
            env={'LANG': 'C'}  # We want english errors
        )[1:]

        # No error, we are done
        if not cmd_status:
            return

        # Now we can manage errors

        # Get all the entries from the pofile, by line number
        entries = {entry.linenum: entry for entry in pofile}

        errors = []
        fatal_error_found = False

        # We're only concerned by lines starting with the file path
        cmd_errors = [error[len(tmp_file.name) + 1:]  # we don't need the path
                      for error in cmd_errors.split('\n')
                      if error.startswith(tmp_file.name)]

        # We'll parse the error output, line by line
        for error_msg in cmd_errors:

            # We extract the line of the error, and the error message
            try:

                error_line, error_msg = error_msg.split(': ', 1)
                error_line = int(error_line)

            except ValueError:

                # If we cannot parse the line, we considere it as a fatal error
                fatal_error_found = True
                errors.append((None, error_msg))

            else:
                # Get the matching po entry
                try:
                    # The correct entry is the one with the highest line number before the
                    # line number of the error (or this exact same line)
                    valid_entry_line = sorted(
                        [line for line in entries.keys() if line <= error_line]
                    )[-1]
                except IndexError:
                    # We didn't find a line, we still save the error message, but without entry
                    fatal_error_found = True
                    errors.append((None, error_msg))
                else:
                    # We found a line, add the entry and the error message to the list of errors
                    errors.append((entries[valid_entry_line], error_msg))

        # We were not able to parse any errors, we act as the po file was correct
        if not errors:
            return

        # If we have a fatal error, we cannot do another pass because we cannot remove the failing
        # entry.
        # So in this case, and if we already did the maximum number of passes, we stop here and
        # raise an exception with all found errors
        if fatal_error_found or max_passes <= 1:
            raise PoFileCheckError(errors=errors, fatal=fatal_error_found)

        # Now we can do another pass

        # Create a new pofile from polibe by copying it, without the erroneous entries
        new_pofile = copy(pofile)
        for error in errors:
            new_pofile.remove(error[0])

        # And check it
        try:

            # We mark ``trust_line_numbers`` as ``False`` as we copied the entries from the
            # original pofile, with all entries having line numbers related to the original file.
            check_po_file(new_pofile, max_passes=max_passes - 1, trust_line_numbers=False)

        except PoFileCheckError as exc:

            # This new check still had some errors. We use the entries from the original file to
            # have the line numbers from the original file
            errors += [
                ([entry for entry in pofile if entry == error[0]][0], error[1])
                for error in exc.errors
            ]

        # No error in this new pass, we can raise the exception with errors from the current
        # pass
        raise PoFileCheckError(errors=errors, fatal=fatal_error_found)
