"""This module holds the views of the ``import_export`` app."""

from django.core.exceptions import ObjectDoesNotExist
from django.db import IntegrityError, transaction
from django.shortcuts import get_object_or_404
from django.utils.translation import ugettext as _
from django.views.generic.detail import DetailView
from django.views.generic.list import ListView

from mtp.apps.assessments.resources import DimensionResource
from mtp.apps.trainings.models import Training

from .exceptions import ImportDataError
from .mixins import XLSExportViewMixin, XLSImportViewMixin
from .xls import Row
from .resources import get_resource


class ModelXLSImportView(XLSImportViewMixin):
    """This view allows to import model data from a xls file."""

    def __init__(self, *args, **kwargs):
        """Declare the ``current_line`` argument

        For the parameters, see ``django.views.generic.View``"""

        self.current_line = None
        super().__init__(*args, **kwargs)

    def process_field_relation(self, data_raw):
        """Process field relation for prepare data to `create_object` method.

        Examples
        --------
        {'text': 'ok', 'group__title': 'name'} --> {'text': 'ok', 'group': {'title': 'name'}}
        """
        data = {}
        for field, value in data_raw.items():
            # If field contains '__', we must process this relation field
            # For example, if we get 'dimensions__name' this means the associated value is the name
            # of desired dimension.
            # So we have 'dimensions' like base_field and 'name' like related_field.
            if '__' in field:
                # We separate field in two : base + related
                base_field, related_field = field.split('__', 1)
                # If a ' ' in field_related, we will have many times this field
                # For example, if we get 'name 1' this means that it's the first name
                # among those that we will receive.
                # So we add {'name': name_value} in a list which describe base_field.
                if ' ' in related_field:
                    related_field, __ = related_field.split(' ', 1)  # We truncates his number
                    data[base_field] = data.get(base_field, []) + [
                        self.process_field_relation({related_field: value})
                    ]
                # Field is unique so we store the related_field and his value in data[base_field
                else:
                    try:
                        data[base_field].update(self.process_field_relation({related_field: value}))
                    except KeyError:
                        data[base_field] = self.process_field_relation({related_field: value})
            else:
                data[field] = value
        return data

    def evaluate_data(self, data_raw, resource):
        """Evaluate data.
        Replace display_field by field_name and process field relation.
        """
        data_with_field_name = {}
        for display_field, value in data_raw.items():
            field_name = resource.display_field_to_field_name(display_field)
            data_with_field_name[field_name] = value

        return self.process_field_relation(data_with_field_name)

    def prepare_data_for_fetch_object(self, resource, data):
        """ Prepare data for fetch object.
        We sort data to select only fields in fields_to_get.

        Returns
        -------
        dict
            Contains useful data for fetch an object according the resource.
        """
        if resource:
            fields_to_get = resource.fields_to_get
        else:
            fields_to_get = ()

        if fields_to_get:
            data_get = {field: data[field] for field in data if field in fields_to_get}
        else:
            data_get = {field: data[field] for field in data}

        if resource is DimensionResource:
            data_get.update({'competency__tree__job_profile': self.training.job_profile})

        return data_get

    def process_data_need_creation_or_cast(self, model, data):
        """ Process data need creation or cast.

        This method create intermediate objects that we need to create main object.
        Moreover, we cast some values.
        And we separate in another dictionnary many-2-many fields to process them later.

        Examples
        --------
        data = {'is_random': 'Yes', 'group': {'title': 'name'}}
        We create or cast with this data : {'title': 'name'} --> <Group 'name'>
                                           {'is_random': 'Yes'} --> {'is_random': True}

        Returns
        -------
        data: dict
            Data of object to create
        m2m: dict
            Data of many-2-many fields
        """
        m2m = {}
        for field_name, value in data.items():
            if isinstance(value, str):
                # We truncate too long value
                field = model._meta.get_field(field_name)
                if hasattr(field, 'max_length') and field.max_length:
                    max_length = int(field.max_length)
                    data[field_name] = value[0:max_length]
                # It's potentially a string to cast in boolean.
                if value.lower() in ['true', 'yes', 'oui']:
                    data[field_name] = True
                elif value.lower() in ['false', 'no', 'non']:
                    data[field_name] = False
            elif isinstance(value, dict):
                # It's an object. We create it with provided data.
                related_model = getattr(model, field_name).field.related_model
                resource = get_resource(related_model)
                data[field_name], __ = self.create_object(value, resource=resource, force_get=True)
            elif isinstance(value, list):
                # It's an objects list. We iterate list to create objects.
                descriptor = getattr(model, field_name)
                if getattr(descriptor, 'reverse', True):  # not set for Reverse descriptors
                    related_model = descriptor.rel.related_model
                else:
                    related_model = descriptor.field.related_model
                for elmt_data in value:
                    resource = get_resource(related_model)
                    elmt, __ = self.create_object(elmt_data, resource=resource, force_get=True)
                    m2m[field_name] = m2m.get(field_name, []) + [elmt]

        # Delete m2m field in data
        for field_to_delete in m2m:
            data.pop(field_to_delete)

        return data, m2m

    def process_create_object(self, model, data, commit=True):
        """ Process to create object. Use it in `create_object` method. """
        # We try to create object
        try:
            obj = model(**data)
        except TypeError:
            model_name = model._meta.verbose_name
            raise ImportDataError(
                _("Can not create this model %(model_name)s with this data %(data)s.") % {
                    'model_name': model_name,
                    'data': data
                },
                line=self.current_line
            )

        # We try to save it if commit == True
        if commit:
            try:
                with transaction.atomic():
                    obj.save()
            except IntegrityError:
                raise ImportDataError(
                    _("Can not save %(obj)s, already exists.") % {'obj': obj},
                    line=self.current_line
                )

        return obj

    def process_update_object(self, obj, data):
        """ Process to update object. Use it in `create_object` method. """
        updated = False
        for field, value in data.items():
            if getattr(obj, field) != value:
                setattr(obj, field, value)
                updated = True
        obj.save()
        return obj, updated

    def create_object(self, data, resource=None, commit=True, force_creation=False,
                      force_get=False, **kwargs):
        """ Create object with extracted data.
        Process data which need creation object and cast some values.
        This method must be used at end of `import_data` method.

        Notes
        -----
        You can no save object putting `commit` to False.
        You can to force creation of object without fetch an existing version putting
        `force_creation` to True.
        You can to force fetch of object to raise an exception if not exist with
        `force_get` to True.

        Examples
        --------
        data = {'text': 'ok', 'group': {'title': 'name'}}
        We create group with this data : {'title': 'name'} --> <Group 'name'>
        We replace it by object created : {'text': 'ok', 'group': <Group 'name'>}
        And we create object with this data.
        """

        if force_creation and force_get:
            raise ValueError("It not possible to force_creation and force_get in same time.")

        # We take the model of the resource
        try:
            model = resource.get_model()
        except AttributeError:
            raise AttributeError("You must provide a resource to import data.")

        # We stock line number if data is a `Row`
        if isinstance(data, Row):
            self.current_line = data.line

        data.update(kwargs)
        data = self.evaluate_data(data, resource=resource)

        # Process data need creation or cast
        data, m2m = self.process_data_need_creation_or_cast(model, data)

        # Create or fetch the object
        created = False
        try:
            # We force the creation, using ObjectDoesNotExist error to go in except block.
            if force_creation:
                raise ObjectDoesNotExist
            data_get = self.prepare_data_for_fetch_object(resource, data)
            obj = model.objects.get(is_deleted=False, **data_get)
        except ObjectDoesNotExist:
            if force_get:
                raise ImportDataError(
                    _("Can not fetch this object : %(data_get)s") % {'data_get': str(data_get)},
                    line=self.current_line
                )
            else:
                obj = self.process_create_object(model, data, commit)
                created = True

        # If objects exists already, we set new values
        if not created:
            obj, __ = self.process_update_object(obj, data)

        # Add m2m relation objects
        for field in m2m:
            for elmt in m2m[field]:
                m2m_field = getattr(obj, field)
                m2m_field.add(elmt)

        return obj, created

    def is_compatible(self, data, resource):
        """Check if data are compatible with resource provided.

        Returns
        -------
        bool
            If data is compatible with the resource so we return True else False.
        """
        for field in data:
            field = resource.display_field_to_field_name(field)
            if field not in resource.get_fields(allow_nested=False):
                return False
        return True


class ActivityXLSImportView(ModelXLSImportView):
    """View to import activity.

    Attributes
    ----------
    template_example_file: str
        The static path to access the template example file.

    """

    template_example_file = None

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.training = None

    def get_training(self):
        return get_object_or_404(Training, pk=self.kwargs['training_uuid'])

    def form_valid(self, form):
        self.training = self.get_training()
        return super().form_valid(form)

    def get_context_data(self, **kwargs):
        """Pass the template example file in context.

        For the parameters, see ``django.forms.generic.base.ContextMixin``
        """

        return super().get_context_data(
            template_example_file=self.template_example_file,
            **kwargs
        )

    def get_back_url(self):
        """Get the back url of this view.

        Returns
        -------
        str

        """

        return self.get_training().get_detail_url()


class XLSExportDetailView(XLSExportViewMixin, DetailView):
    """ Export View to XLS format.

    Attributes
    ----------
    model : models.Model
        Main model to export
    title : string
        Title wanted into export file
    """

    def create_sheets(self, context):  # pylint: disable=unused-argument
        """ Create sheets of export file. By default, only first sheet is used.

        Parameters
        ----------
        context : dict
            Data computed in ``get_context_data`` that may be useful to create the doc.

        """

        sheet_name = self.writer.first_sheet_name
        sheet_name = self.writer.initialize(sheet_name, self.get_title)
        self.export_data(sheet_name, self.get_object())


class XLSExportListView(XLSExportViewMixin, ListView):
    """ Export View to XLS format.

    Attributes
    ----------
    model : models.Model
        Main model to export
    title : string
        Title wanted into export file
    """

    def create_sheets(self, context):  # pylint: disable=unused-argument
        """Create sheets of export file. By default, only first sheet is used.

        Parameters
        ----------
        context : dict
            Data computed in ``get_context_data`` that may be useful to create the doc.

        """

        self.writer.remove_all_sheet()
        for item in self.object_list:
            sheet = self.writer.create_new_sheet(str(item))
            self.writer.initialize(sheet.title)
            self.export_data(sheet.title, item)
