"""This module contains the managers for the ``django-discover`` app"""

import io
import os
from zipfile import ZipFile, ZIP_LZMA

from django.conf import settings
from django.core.serializers import deserialize, serialize
from django.db import transaction
from django.db.models import (
    Manager,
    Q,
    QuerySet
)

import requests
from psycopg2.extras import NumericRange


class DiscoverItemQueryset(QuerySet):
    """QuerySet for the ``DiscoverItem`` model"""

    def for_language(self, language):
        """Filter the discover items for the given language

        Parameters
        ----------
        language: str
            The language to filter on

        Returns
        -------
        DiscoverItemQueryset
            The filtered Queryset

        """

        return self.filter(language=language)

    def for_version(self, major_version, minor_version=0, hotfix_version=0):
        """Filter the discover items for the given version

        The query below can be decomposed like this:
        - Return all the item for which the major versions field is not filled
        - Return the items for which the major version field is filled IF:
          - The given version is in the bounds
          - AND The minor version is in the bounds (if filled)
          - AND The hotfix version is in the bounds (if filled)

        Parameters
        ----------
        major_version: int
            The major version
        minor_version: int
            The minor version
        hotfix_version: int
            The hotfix version

        Returns
        -------
        DiscoverItemQueryset
            The filtered Queryset

        """

        return self.filter(
            # The major version was not filled
            Q(major_versions__isnull=True) |
            Q(
                # The major versions contains the current version
                Q(
                    major_versions__contains=NumericRange(
                        major_version,
                        major_version,
                        bounds='[]'
                    )
                ) &
                Q(
                    Q(minor_versions__isnull=True) |
                    Q(
                        minor_versions__isnull=False,
                        minor_versions__contains=NumericRange(
                            minor_version,
                            minor_version,
                            bounds='[]'
                        )
                    )
                ) &
                Q(
                    Q(hotfix_versions__isnull=True) |
                    Q(
                        hotfix_versions__isnull=False,
                        hotfix_versions__contains=NumericRange(
                            hotfix_version,
                            hotfix_version,
                            bounds='[]'
                        )
                    )
                )
            )
        )


class DiscoverItemManager(Manager.from_queryset(DiscoverItemQueryset)):
    """Manager for the ``DiscoverItem`` model"""

    @staticmethod
    def get_zipped_discover_items(discover_items_queryset):
        """Get a ZipFile containing the serialized discover items queryset

        Parameters
        ----------
        discover_items_queryset: DiscoverItemQueryset
            The filtered queryset containing the discover items to serialize

        Returns
        -------
        io.BytesIO
            A buffer of a ZipFile containing the following elements:
            - A json file containing the serialized discover items
            - The media of the serialized items

        """

        # Build the ZipFile
        zip_buffer = io.BytesIO()
        zip_file = ZipFile(zip_buffer, 'w', ZIP_LZMA)

        # Copy the serialized discover items
        serialized_items = serialize('json', discover_items_queryset)
        zip_file.writestr('discover_items.json', serialized_items)

        # Copy the media
        medias_list = discover_items_queryset.values_list('image', flat=True)
        for media in medias_list:
            file_path = os.path.join(settings.MEDIA_ROOT, media)
            zip_file.write(file_path, media)

        zip_file.close()

        return zip_buffer

    def load_zipped_discover_items(self):
        """Load an archive containing serialized discover items

        Raises
        ------
        Exception
            If something went wrong

        """

        discover_items_media_path = os.path.join(
            settings.MEDIA_ROOT,
            'django_discover'
        )

        try:
            with transaction.atomic():
                # 1 - Get the discover items archive from the Command center
                response = requests.get(settings.COMMAND_CENTER_URL, stream=True)

                try:
                    response.raise_for_status()
                except requests.HTTPError:
                    # A 404 error means no discover items were available
                    if response.status_code == 404:
                        return False
                    else:
                        # If we encounter another error, re-raise the ``HTTPError``
                        raise

                zip_file = ZipFile(io.BytesIO(response.content), 'r', ZIP_LZMA)

                # 2 - Erase all of the actual discover items
                self.get_queryset().all().delete()

                # 3 - Rename the path containing the media of the current discover items
                try:
                    os.rename(discover_items_media_path, discover_items_media_path + '_save')
                except OSError:
                    pass

                # 4 - Extract the media files of the new discover items
                for file_name in zip_file.namelist():
                    if file_name == 'discover_items.json':
                        continue

                    zip_file.extract(file_name, settings.MEDIA_ROOT)

                # 5 - Load the new discover items objects from the json file
                for discover_item in deserialize('json', zip_file.read('discover_items.json')):
                    discover_item.save()

        except Exception as exception:  # pylint: disable=broad-except

            # Some things went wrong, move back the media...
            try:
                os.rename(discover_items_media_path + '_save', discover_items_media_path)
            except OSError:
                pass

            # ...And raise
            raise ValueError(
                'Error during the discover items fetching. The exception was "%s"' % exception
            )

        return True



