"""This module holds classes allowing to import several types of resources
from the Saba LMS into the MTP database."""

import csv
import logging
import os
import tempfile

from django.conf import settings

from constance import config

from mtp.apps.accounts.constants import (
    GROUPTYPE_BUSINESS_LEVEL,
    GROUPTYPE_BUSINESS_TARGET,
    GROUPTYPE_BUSINESS_UNIT,
    GROUPTYPE_BUSINESS_ZONE,
    GROUPTYPE_JOB_NAME,
    GROUPTYPE_JOB_PROFILE,
    GROUPTYPE_JOB_TYPE,
    INTERNAL_GROUP_TRAINEE,
    INTERNAL_GROUP_WHOSWHO,
)
from mtp.apps.accounts.models import Group, GroupRole, GroupType, MTPUser
from mtp.apps.assessments.models import LessonReference


logger = logging.getLogger('mtp.nissan.import_export')


class SabaImporter(object):
    """This class contains base mechanisms for all Saba LSM imports.

    Parameters
    ----------
    request: HttpRequest
        The request with an attached file to handle
    file_field_name: str
        The name of the file in the request
    file_path: str
        The import file path on disk if we do not use request

    """

    cache = {}
    import_file = None
    import_file_path = None
    row = None
    offset = 0

    def __init__(self, request=None, file_field_name='file', file_path=None):
        """Initialize our importer from request or file"""

        if request:
            self.handle_upload(request, file_field_name)

        elif file_path:
            self.import_file_path = file_path

    def handle_upload(self, request, file_field_name):
        """Save the uploaded data into a temporary file.

        Parameters
        ----------
        request : django ``HttpRequest``
            The request containing the uploaded data.
        file_field_name : str
            The name of the form field containing the uploaded data.

        Returns
        -------
        None

        """

        # We dump the uploaded file contents into a temporary file...
        uploaded_file = request.FILES[file_field_name]
        __, self.import_file_path = tempfile.mkstemp()
        import_file = open(self.import_file_path, 'wb')
        import_file.write(uploaded_file.read())
        import_file.close()

    def open_import_file(self):
        """Open the import file and initialize a csv reader.

        Parameters
        ----------
        None

        Returns
        -------
        ``csv.reader``
            A csv reader on the import file.

        """
        self.import_file = open(self.import_file_path, newline='', encoding='utf-8')
        return csv.reader(self.import_file, delimiter=',', quotechar='"')

    def row_value(self, idx):
        """Return the stripped value of the row data at column ``idx``.

        Parameters
        ----------
        idx : int
            The index of the column in which to read the data.

        Returns
        -------
        str
            The stripped data.

        """
        return self.row[idx].strip()

    def bool_row_value(self, idx):
        """Return the boolean value of the row data at column ``idx``.

        Parameters
        ----------
        idx : int
            The index of the column in which to read the data.

        Returns
        -------
        str
            The stripped data.

        """
        return bool(self.row_value(idx))

    def close_import_file(self):
        """Close the import file.

        """
        self.import_file.close()

    def delete_import_file(self):
        """Delete the import file.

        """
        os.remove(self.import_file_path)


class UserImporter(SabaImporter):
    """This class allows to import users (aka ``MTPUser``s) from the Saba LMS.

    For the parameters, see ``SabaImporter``

    """

    # Saba "dimension" name
    # ---------------------
    COL_USERNAME = 0            # Person Username
    COL_FIRST_NAME = 1          # Person First Name
    COL_LAST_NAME = 2           # Person Last Name
    COL_EMAIL = 3               # Person E-mail
    COL_EXTERNAL_ID = 0         # Person Username
    COL_LANGUAGE = 5            # Person Language
    COL_COUNTRY = 6             # Person Domain
    COL_JOB_FAMILY = 7          # Job Family Name
    COL_JOB_NAME = 8            # Person Job Type Name
    COL_JOB_TYPE = 9            # Person Organization C4 Dealer Type/Person Organization Custom 4
    COL_STORE_CODE = 10         # Person Organization Number
    # [S3] Individual Additional Activities (Held or identified for development): Fleet
    COL_IS_B2B = 11
    COL_ZONE_B2B_SALES = 12     # Person Organization C2 PC Fleet Zone/Person Organization Custom2
    COL_ZONE_B2C_SALES = 13     # Person Organization C0 PC Sales Zone/Person Organization Custom0
    # Person Organization C1 PC After Sales Zone/Person Organization Custom1
    COL_ZONE_AFTER_SALES = 14

    RBUS = {
        'RBU BENELUX': ['Netherlands', 'Belux'],
        'RBU CEE': ['HU-CZ-SK-PL'],
        'RBU CENTER': ['Switzerland', 'Germany', 'Austria'],
        'RBU EAST': ['Kazakhstan', 'Ukraine', 'Russia'],
        'RBU FRANCE': ['France'],
        'RBU IBERIA': ['Spain', 'Portugal'],
        'RBU NNE': ['Denmark', 'Finland', 'Baltics', 'Norway', 'Sweden'],
        'RBU NORTH': ['United Kingdom'],
        'RBU SOUTH': ['Italy'],
        'INSCs': [
            'FOTs',
            'CYP-ICE-MAL-PAL',
            'Greece',
            'Adriatics',
            'Bulgaria',
            'Romania',
            'Israel',
            'Ireland',
        ],
    }

    BUSINESS_UNITS = 'business_units'
    COUNTRY_GROUPS = 'country_groups'
    JOB_NAMES = 'job_names'
    JOB_PROFILES = 'job_profiles'
    JOB_TYPES = 'job_types'
    RBU_GROUPS = 'rbu_groups'
    STORE_GROUPS = 'store_groups'
    ZONE_GROUPS = 'zone_groups'

    offset_config_var = 'SABA_USERS_IMPORT_OFFSET'

    def __init__(self, *args, **kwargs):
        """Set the offset from config"""

        super().__init__(*args, **kwargs)

        self.offset = config.SABA_USERS_IMPORT_OFFSET

    def _prefix(self, string, prefix):
        """Prefix the given string if it actually exists.

        Parameters
        ----------
        string : str
            The string to prefix.
        prefix : str
            The prefix to add.

        Returns
        -------
        str
            The prefixed string.

        """
        if string:
            return '%s %s' % (prefix, string)
        return None

    def _language_name_to_code(self, name):
        """Return the ISO 639-1 code of a language.

        Parameters
        ----------
        name : str
            The name of the language.

        Returns
        -------
        str
            The language code.

        Notes
        -----
        This method uses the languages defined in the Django settings.

        """
        for lang in settings.LANGUAGES:
            if lang[1] == name:
                return lang[0]
        logger.warning(
            "No language code for name '%s'",
            name,
        )

    def _country_to_rbu(self, country):
        """Returns the Nissan RBU to which the specified country belongs.

        Parameters
        ----------
        country : str
            The country name, as stored in the Saba LMS.

        Returns
        -------
        str
            The name of the Nissan RBU.

        """
        for rbu_name, country_names in self.RBUS.items():
            if country in country_names:
                return rbu_name
        logger.warning(
            "No RBU for country '%s', please check the RBU mappings.",
            country,
        )

    def _add_user_to_group(self, user, group_name, group_type_name, cache_key):
        """Add the given user to the specified group.

        Parameters
        ----------
        user : ``mtp.apps.accounts.models.MTPUser``
            The user to add to the group.
        group_name : str
            The name of the group to add the user to.
        group_type_name : str
            The type of the group to add the user to.
        cache_key : str
            The key to the internal cache in which to try to look for
            an already instantiated group.

        Returns
        -------
        None

        Notes
        -----
        This method is likely to modify the ``self.cache`` attribute.

        """
        cache = self.cache.get(cache_key)

        # First we try to find the group in the cache.
        group = cache.get(group_name)

        # If we don't...
        if not group:

            # We try to get_or_create it in the database.
            try:
                group, __ = Group.objects.get_or_create(
                    name=group_name,
                    type=GroupType.objects.get(name=group_type_name),
                )

            # If it fails...
            except Exception:  # pylint: disable=broad-except
                               # Let's not crash here please!

                # We keep track of the incident...
                logger.exception(
                    "Cannot get or create group with name: %s and type: %s",
                    group_name,
                    group_type_name,
                )

                # ... and we bail out.
                return

            # Otherwise, we add the group to the cache.
            else:
                cache[group_name] = group

        # We can now finally add the user to the group.
        GroupRole.objects.update_or_create(user=user, group=group)

    def import_users(self):
        """Import users from a Saba LMS export."""

        self.cache = {
            self.BUSINESS_UNITS: {},
            self.COUNTRY_GROUPS: {},
            self.JOB_NAMES: {},
            self.JOB_PROFILES: {},
            self.JOB_TYPES: {},
            self.RBU_GROUPS: {},
            self.STORE_GROUPS: {},
            self.ZONE_GROUPS: {}
        }

        trainee_group = Group.objects.get(name=INTERNAL_GROUP_TRAINEE)
        whoswho_group = Group.objects.get(name=INTERNAL_GROUP_WHOSWHO)
        b2b_group, __ = Group.objects.get_or_create(
            name='B2B', type__name=GROUPTYPE_BUSINESS_TARGET
        )
        b2c_group, __ = Group.objects.get_or_create(
            name='B2C', type__name=GROUPTYPE_BUSINESS_TARGET
        )

        users = []

        csv_data = self.open_import_file()
        for self.row in csv_data:

            if csv_data.line_num < self.offset:
                continue

            # Those should always be present.
            username = self.row_value(self.COL_USERNAME)
            first_name = self.row_value(self.COL_FIRST_NAME).title()
            last_name = self.row_value(self.COL_LAST_NAME).upper()

            # If the email is missing, we use NO-EMAIL.firstname.lastname@nissan.com.
            email = self.row_value(self.COL_EMAIL).lower()
            if not email:
                email = 'NO-EMAIL.%s.%s@nissan.com' % (first_name, last_name)

            # The password is the same as the username at first.
            # Because security is our top priority.
            password = username

            # The external ID is used to keep a link between MTP and Saba.
            external_id = self.row_value(self.COL_EXTERNAL_ID)

            # The language is stored as a full text constant.
            # We are going to need to retrieve the ISO code from it.
            language = self.row_value(self.COL_LANGUAGE)

            # The country is also stored as a full text constant.
            # We need it to deduce the RBU this trainee belongs to.
            country = self.row_value(self.COL_COUNTRY)

            # The following fields contain data about the user's job.
            job_family = self.row_value(self.COL_JOB_FAMILY)  # e.g. "Management"
            job_name = self.row_value(self.COL_JOB_NAME)  # e.g. "D1PR Dealer Principal_ES"
            job_type = self.row_value(self.COL_JOB_TYPE)  # e.g. "Main Dealer Sales & After Sales"

            # The store is what we call in French "concession".
            # We only get a code, which we'll have to prefix with the country
            # name, to ensure unicity.
            store_code = self._prefix(self.row_value(self.COL_STORE_CODE), country)

            # It is now time to differentiate B2B and B2C personnel...
            is_b2b = self.bool_row_value(self.COL_IS_B2B)
            is_b2c = not is_b2b

            # ... and find out the zone(s) they work in.
            # Just like the store code, the zone names should be prefixed
            # by the country name to ensure unicity.
            b2b_sales_zone = self._prefix(self.row_value(self.COL_ZONE_B2B_SALES), country)
            b2c_sales_zone = self._prefix(self.row_value(self.COL_ZONE_B2C_SALES), country)
            after_sales_zone = self._prefix(self.row_value(self.COL_ZONE_AFTER_SALES), country)

            # We have all the required information, we can now
            # import this trainee into the database.
            user, __ = MTPUser.objects.update_or_create(
                username=username, defaults={
                    'first_name': first_name,
                    'last_name': last_name,
                    'email': email,
                    'external_id': external_id,
                }
            )
            user.set_password(password)

            # We add the user to the relevant common groups.
            GroupRole.objects.update_or_create(user=user, group=trainee_group)
            GroupRole.objects.update_or_create(user=user, group=whoswho_group)

            # We set the user's language.
            # We get data such as "English (United Kingdom)" or "Portuguese Continental".
            # We must deduce from this the textual name of the language ("English")
            # and its 2-letter ISO code ("en").
            language = language.replace('(', ' ').replace(')', ' ').strip()
            language_parts = language.split(' ')
            language_name = language_parts[0]
            language_code = self._language_name_to_code(language_name)
            user.natural_language = language_code

            # We can also add the user to the proper business level groups,
            # namely RBU, country and store.
            rbu = self._country_to_rbu(country)
            self._add_user_to_group(
                user, rbu, GROUPTYPE_BUSINESS_LEVEL, self.RBU_GROUPS
            )
            self._add_user_to_group(
                user, country, GROUPTYPE_BUSINESS_LEVEL, self.COUNTRY_GROUPS
            )
            self._add_user_to_group(
                user, store_code, GROUPTYPE_BUSINESS_LEVEL, self.STORE_GROUPS
            )

            # We set the user's job profile.
            # Right now it can be "Sales" or "Service".
            # We can deduce this from the ``job_family``.
            job_profile_name = job_family
            if job_family == 'Sales':
                job_profile_name = 'Salesperson'
            elif job_family == 'After Sales':
                job_profile_name = 'Service Advisor'

            # We can then assign the user's job information...
            self._add_user_to_group(
                user, job_profile_name, GROUPTYPE_JOB_PROFILE, self.JOB_PROFILES
            )
            self._add_user_to_group(
                user, job_name, GROUPTYPE_JOB_NAME, self.JOB_NAMES
            )
            self._add_user_to_group(
                user, job_type, GROUPTYPE_JOB_TYPE, self.JOB_TYPES
            )

            # ... and their business target(s).
            # (even though, yes, right now those are mutually exclusive)
            if is_b2b:
                GroupRole.objects.update_or_create(user=user, group=b2b_group)
            if is_b2c:
                GroupRole.objects.update_or_create(user=user, group=b2c_group)

            # Since we now know the user's job family, country and language,
            # we can add them to the proper business unit.
            business_unit_name = 'Nissan %s %s (%s)' % (job_family, country, language_name)
            self._add_user_to_group(
                user, business_unit_name, GROUPTYPE_BUSINESS_UNIT, self.BUSINESS_UNITS
            )

            # Finally we can add the user to the proper zone(s).
            if b2b_sales_zone:
                self._add_user_to_group(
                    user, b2b_sales_zone, GROUPTYPE_BUSINESS_ZONE, self.ZONE_GROUPS
                )
            if b2c_sales_zone:
                self._add_user_to_group(
                    user, b2c_sales_zone, GROUPTYPE_BUSINESS_ZONE, self.ZONE_GROUPS
                )
            if after_sales_zone:
                self._add_user_to_group(
                    user, after_sales_zone, GROUPTYPE_BUSINESS_ZONE, self.ZONE_GROUPS
                )

            # We're done for this one!
            user.save()
            users.append(user)

        self.close_import_file()
        self.delete_import_file()

        return users


class CourseImporter(SabaImporter):
    """This class allows to import courses (aka ``LessonReference``s) from the Saba LMS.

    For the parameters, see ``SabaImporter``

    """

    # Saba "dimension" name
    # ---------------------
    COL_COURSE_ID = 0       # Course Course ID
    COL_COURSE_NAME = 1     # Course Title

    def __init__(self, *args, **kwargs):
        """Set the offset from config"""

        super().__init__(*args, **kwargs)

        self.offset = config.SABA_COURSES_IMPORT_OFFSET

    def import_courses(self):
        """Import courses from a Saba LMS export."""

        courses = []

        csv_data = self.open_import_file()
        for self.row in csv_data:

            if csv_data.line_num < self.offset:
                continue

            courses.append(LessonReference.objects.update_or_create(
                external_id=self.row_value(self.COL_COURSE_ID),
                defaults={
                    'external_name': self.row_value(self.COL_COURSE_NAME),
                },
            ))

        self.close_import_file()
        self.delete_import_file()

        return courses
