"""This module contains the managers for the ``media`` app"""

import logging
import os
import shutil

from django.conf import settings
from django.core.exceptions import (
    ObjectDoesNotExist,
    MultipleObjectsReturned,
)
from django.db import transaction

from mtp.discrete_apps.versionable.managers import (
    DependencyManager,
    DependencyQuerySet,
    PolymorphicManagerForDependency,
    PolymorphicQuerySetForDependency,
)

from mtp.apps.documents.models import Document, Note as DocumentNote
from mtp.apps.sync.managers import SyncableQuerySet


logger = logging.getLogger('mtp.apps.media')

DOCUMENT_PATH = 'documents'
PDF_PATH = 'media_app/pdf'
VIDEO_PATH = 'media_app/video'

MEDIA_DOCUMENT_PATH = os.path.join(settings.MEDIA_ROOT, DOCUMENT_PATH)
MEDIA_PDF_PATH = os.path.join(settings.MEDIA_ROOT, PDF_PATH)
MEDIA_VIDEO_PATH = os.path.join(settings.MEDIA_ROOT, VIDEO_PATH)

PDF_IMAGE_PREFIX = 'images_'
DOCUMENT_IMAGE_PREFIX = 'pdf_images_'


class DependencyMediaQuerySet(SyncableQuerySet, PolymorphicQuerySetForDependency):
    """QuerySet for the ``mtp.apps.media.models.MediaContent`` model"""

    pass


class DependencyMediaManager(
        PolymorphicManagerForDependency.from_queryset(DependencyMediaQuerySet)):
    """Manager for the ``mtp.apps.media.models.Media`` model"""

    use_in_migrations = True

    def migrate_documents_to_media(self):
        """ Migrate all ``Document`` to ``Media``

        - Iterate over the existing ``Document``instance, and migrate each one
        - Migrate freenotes

        """

        # Migrate documents
        start = 0
        while True:  # Avoid filling the memory with too many objects
            documents = Document.objects.all()[start:100+start]

            if len(documents) == 0:
                break

            for document in documents:
                if not self.migrate_document_to_media(document):
                    # if migration not succeed, we avoid to migrate it another time
                    start += 1

        # Migrate free notes
        self.migrate_free_notes()

    @staticmethod
    def get_values_for_document(document):
        """Get verified values for a ``Document``

        Parameters
        ----------
        document: Document
            A ``Document`` to get verified values

        Returns
        -------
        ``dict``
            A dict containing the verified values of the ``Document``

        """

        # base document values
        values = {
            'id': document.id,
            'name': document.name,
            'description': document.description,
            'is_video': document.is_video,
            'sort_order': document.sort_order,
            'training_id': document.training_id,
            'trainer_only': document.trainer_only,
            'is_deleted': document.is_deleted,
        }

        # I - Trainee document file
        # If document is supported
        if document.trainee_document_file_type in (Document.FILE_TYPES.PDF,
                                                   Document.FILE_TYPES.VIDEO):
            if not os.path.isfile(document.trainee_document_file.path):
                # The document "trainee_document_file" file doesn't exist
                logger.error(
                    'The document %s trainee file(s) are not on disk',
                    document.trainee_document_file.path)
            else:
                # Update values with verified path for trainee file
                values['trainee_document_file_name'] = document.trainee_document_file.name
                values['trainee_document_file_hash'] = document.trainee_document_file_hash

        # II - If document is a video, there is enough values
        if document.is_video:
            return values

        # III - Trainer document file
        # If document is supported
        if (document.trainer_document_file and
                document.trainer_document_file_type is Document.FILE_TYPES.PDF):
            if not os.path.isfile(document.trainer_document_file.path):
                # The document "trainer_document_file" file doesn't exist
                logger.error(
                    'The trainer document %s trainer file(s) are not on disk',
                    document.trainer_document_file.path
                )
            else:
                # Update values with new path for trainer file
                values['trainer_document_file_name'] = document.trainer_document_file.name
                values['trainer_document_file_hash'] = document.trainer_document_file_hash

        return values

    def move_files_to_media(self, document_values):
        """Move files from ``Document`` to ``Media``

        Parameters
        ----------
        document_values: dict
            A dict containing the values of the ``Document`` to move files

        Returns
        -------
        ``dict``

        """

        if not os.path.isdir(MEDIA_PDF_PATH):
            os.makedirs(MEDIA_PDF_PATH)

        if not os.path.isdir(MEDIA_VIDEO_PATH):
            os.makedirs(MEDIA_VIDEO_PATH)

        # Document trainee
        document_values = self.move_file_to_media('trainee_document_file', document_values)

        # If document is pdf type
        if not document_values['is_video']:
            # Trainee images
            document_values = self.move_file_images_to_media(
                'trainee_document_file', document_values)

            # Trainer document file
            document_values = self.move_file_to_media('trainer_document_file', document_values)

            # Trainer images
            document_values = self.move_file_images_to_media(
                'trainer_document_file', document_values)

        return document_values

    def move_file_to_media(self, file_field_name, document_values):
        """Move trainee files from ``Document`` to ``Media``

        Parameters
        ----------
        file_field_name: str
            The file field name to move
        document_values: dict
            A dict containing the values of the ``Document`` to move files

        Returns
        -------
        ``dict``

        """
        file_name_key = file_field_name + '_name'
        file_hash_key = file_field_name + '_hash'

        if file_name_key not in document_values:
            return document_values

        src_path = document_values[file_name_key]
        if document_values['is_video']:
            dest_path = self.get_video_path(src_path)
        else:
            dest_path = self.get_pdf_path(src_path)
        try:
            shutil.move(
                self.get_media_path(src_path),
                self.get_media_path(dest_path)
            )
        except (OSError, FileNotFoundError):
            logger.error('Error during moving trainee file : %s', src_path)
            # remove all files
            document_values.pop(file_name_key)
            document_values.pop(file_hash_key)
        else:
            # Update values with new path for trainee file
            document_values[file_name_key] = dest_path

        return document_values

    def move_file_images_to_media(self, file_field_name, document_values):
        """Move trainee pdf images from ``Document`` to ``Media``

        Parameters
        ----------
        file_field_name: str
            The file field name to move
        document_values: dict
            A dict containing the values of the ``Document`` to move files

        Returns
        -------
        ``dict``

        """

        file_hash_key = file_field_name + '_hash'
        if file_hash_key not in document_values:
            return document_values

        hash_value = document_values[file_hash_key]
        src_path = self.get_document_images_path(hash_value)
        dest_path = self.get_pdf_images_path(hash_value)

        try:
            shutil.move(
                self.get_media_path(src_path),
                self.get_media_path(dest_path)
            )
        except (OSError, FileNotFoundError):
            logger.error('Error during moving images : %s', src_path)

        return document_values

    def move_back_files_to_document(self, document_values):
        """Move back files from ``Media`` to ``Document``

        Parameters
        ----------
        document_values: dict
            A dict containing the values of the ``Document`` to move back files

        """

        if not os.path.isdir(MEDIA_DOCUMENT_PATH):
            os.makedirs(MEDIA_DOCUMENT_PATH)

        # Document trainee
        self.move_back_file_to_media('trainee_document_file', document_values)

        # If document is a video, there is nothing to do else
        if document_values['is_video']:
            return

        # Trainee images
        self.move_back_file_images_to_media('trainee_document_file', document_values)

        # Trainer document file
        self.move_back_file_to_media('trainer_document_file', document_values)

        # Trainer images
        self.move_back_file_images_to_media('trainer_document_file', document_values)

    def move_back_file_to_media(self, file_field_name, document_values):
        """Move back trainee file from ``Media`` to ``Document``

        Parameters
        ----------
        file_field_name: str
            The file field name to move
        document_values: dict
            A dict containing the values of the ``Document`` to move files

        """

        file_name_key = file_field_name + '_name'

        if file_name_key in document_values:
            src_path = document_values[file_name_key]
            dest_path = self.get_document_path(src_path)

            try:
                shutil.move(
                    self.get_media_path(src_path),
                    self.get_media_path(dest_path)
                )
            except (OSError, FileNotFoundError):
                logger.error('Error during moving back file : %s', src_path)

    def move_back_file_images_to_media(self, file_field_name, document_values):
        """Move back trainee pdf images from ``Media`` to ``Document``

        Parameters
        ----------
        file_field_name: str
            The file field name to move
        document_values: dict
            A dict containing the values of the ``Document`` to move files

        """

        file_name_key = file_field_name + '_hash'

        if file_name_key in document_values:
            hash_value = document_values[file_name_key]
            src_path = self.get_pdf_images_path(hash_value)
            dest_path = self.get_document_images_path(hash_value)

            try:
                shutil.move(
                    self.get_media_path(src_path),
                    self.get_media_path(dest_path)
                )
            except (OSError, FileNotFoundError):
                logger.error('Error during moving back images : %s', src_path)

    def migrate_document_to_media(self, document):
        """Migrate a ``Document`` instance to ``Media``

        1 - Storing the useful values we want to use for creating the relevant
            ``Media`` subclass
        2 - Move files to new media destination
        3 - Create the new ``Media`` instance subclass accordingly to the type of
            the ``Document``
        4 - Migrate the ``Note``s
        5 - Move back files to original destination if error during 3 or 4

        For 2 :
            - On slave :
                - We create only presentation, with the same id of equivalent document.
                  To be abble to attach user notes to them.
            - On master :
                - We create all models

        Parameters
        ----------
        document: Document
            A ``Document`` to migrate

        Returns
        -------
        ``boolean``
            True if operation succeed, False otherwise

        """

        # Presentation base values
        try:
            values = self.get_values_for_document(document)
        except Exception as exception:  # pylint: disable=broad-except
            logger.exception('Error during the document verification %s', exception)
            return False

        # Move files to media app, and update values
        try:
            values = self.move_files_to_media(values)
        except Exception as exception:  # pylint: disable=broad-except
            logger.exception('Error during the document files copy %s', exception)
            return False

        # Migrate model
        try:
            with transaction.atomic():
                if document.is_video:
                    document.delete()

                    self.migrate_document_to_video_media(values)
                else:
                    note_values = self.get_note_values_for_document_id(document.id)
                    document.delete()

                    self.migrate_document_to_presentation(values)
                    self.migrate_document_notes(note_values)

                return True
        # If error in model migration, move back files
        except Exception as exception:  # pylint: disable=broad-except
            logger.exception('Error during the document migration %s', exception)
            # move files backward
            self.move_back_files_to_document(values)

            return False

    @staticmethod
    def migrate_document_to_video_media(document_values):
        """Migrate a ``Document`` instance of video type to a media ``Video`` instance

        Parameters
        ----------
        document_values: dict
            A dict containing the values of the ``VideoMedia`` to create

        """

        from mtp.apps.media.models import Video, VideoMedia

        # The videos should be only migrated on the ``MASTER`` instance since no
        # other content are pointing to it
        if settings.INSTANCE_IS_SLAVE:
            return

        # Do nothing if no video file
        if 'trainee_document_file_name' not in document_values:
            return

        # Firstly, create the ``VideoMedia`` with the ``trainee_document_file``
        video_media = VideoMedia.objects.create(id=document_values['id'])
        video_media.file.name = document_values['trainee_document_file_name']
        video_media.file_hash = document_values['trainee_document_file_hash']
        video_media.save()

        # Secondly, create the ``Video``
        Video.objects.create(
            id=document_values['id'],
            name=document_values['name'],
            description=document_values['description'],
            sort_order=document_values['sort_order'],
            training_id=document_values['training_id'],
            is_deleted=document_values['is_deleted'],
            media=video_media
        )

    @staticmethod
    def migrate_document_to_presentation(document_values):
        """Migrate a ``Document`` instance of pdf type to a media ``Presentation`` instance

        Parameters
        ----------
        document_values: dict
            A dict containing the values of the ``Presentation`` to create

        """

        from mtp.apps.media.models import (
            PdfMedia,
            Presentation,
            PresentationMedia
        )

        # Create the ``Presentation``
        presentation = Presentation(
            id=document_values['id'],
            name=document_values['name'],
            description=document_values['description'],
            sort_order=document_values['sort_order'],
            training_id=document_values['training_id'],
            is_deleted=document_values['is_deleted'],
            is_accessible_by_trainees=not document_values['trainer_only']
        )

        # If we are on a slave instance, bypass the sync stuff
        if settings.INSTANCE_IS_SLAVE:
            presentation.save(mtp_update=True)
        else:
            presentation.save()

        # The stuff below should be created only on the ``MASTER`` instance
        if not settings.INSTANCE_IS_SLAVE:
            if 'trainee_document_file_name' in document_values:
                # Create the ``PdfMedia``s with the ``trainee_document_file``
                trainee_pdf_media = PdfMedia.objects.create()
                trainee_pdf_media.file.name = document_values['trainee_document_file_name']
                trainee_pdf_media.file_hash = document_values['trainee_document_file_hash']
                trainee_pdf_media.save()

                # Associate it with the ``Presentation``
                PresentationMedia.objects.create(
                    is_restricted=False,
                    media=trainee_pdf_media,
                    presentation=presentation
                )

            # Create the ``PdfMedia``s with the ``trainer_document_file``, if any
            if 'trainer_document_file_name' in document_values:
                trainer_pdf_media = PdfMedia.objects.create()
                trainer_pdf_media.file.name = document_values['trainer_document_file_name']
                trainer_pdf_media.file_hash = document_values['trainer_document_file_hash']
                trainer_pdf_media.save()

                # Associate it with the ``Presentation``
                PresentationMedia.objects.create(
                    is_restricted=True,
                    media=trainer_pdf_media,
                    presentation=presentation
                )

    @staticmethod
    def get_note_values_for_document_id(document_id):
        """Get notes list values linked to a ``document``

        Parameters
        ----------
        document_id: uuid
            The id of the ``Document`` to move note from

        Returns
        ----------
        notes_values: ``list``
            Notes values list
        """

        return list(
            DocumentNote.objects.all().filter(document_id=document_id).values(
                'id',
                'text',
                'user',
                'session',
                'document_id',
                'page'
            )
        )

    @staticmethod
    def migrate_document_notes(notes_values):
        """Migrate the documents notes from the ``document`` app to the notes of ``media`` app

        Parameters
        ----------
        notes_values: list
            Notes values list
        """

        from mtp.apps.media.models import Note as MediaNote

        for note_value in notes_values:
            MediaNote.objects.create(
                id=note_value['id'],
                content=note_value['text'],
                user_id=note_value['user'],
                training_session_id=note_value['session'],
                presentation_id=note_value['document_id'],
                presentation_page=note_value['page']
            )

    @staticmethod
    def migrate_free_notes():
        """Migrate the free notes from the ``document`` app to the notes of ``media`` app """

        from mtp.apps.media.models import Note as MediaNote

        try:
            with transaction.atomic():
                while True:  # Avoid filling the memory with too many objects
                    free_notes = DocumentNote.objects.all().filter(document_id=None)[:100]

                    if len(free_notes) == 0:
                        break

                    # Iterate over free note
                    for free_note in free_notes:
                        MediaNote.objects.create(
                            id=free_note.id,
                            content=free_note.text,
                            user=free_note.user,
                            training_session=free_note.session
                        )

                        free_note.delete()
        except Exception as exception:  # pylint: disable=broad-except
            logger.exception('Error during the free notes migration %s', exception)

    @staticmethod
    def get_document_path(path):
        """Get relative document path for a file path

        Parameters
        ----------
        path: str
            A string containing file path

        """

        return os.path.join(
            DOCUMENT_PATH,
            os.path.basename(path)
        )

    @staticmethod
    def get_pdf_path(path):
        """Get relative pdf media path for a file path

        Parameters
        ----------
        path: str
            A string containing file path

        """

        return os.path.join(
            PDF_PATH,
            os.path.basename(path)
        )

    @staticmethod
    def get_video_path(path):
        """Get relative video media path for a file path

        Parameters
        ----------
        path: str
            A string containing file path

        """

        return os.path.join(
            VIDEO_PATH,
            os.path.basename(path)
        )

    @staticmethod
    def get_media_path(path):
        """Get absolute media path for a file path

        Parameters
        ----------
        path: str
            A string containing file path

        """

        return os.path.join(
            settings.MEDIA_ROOT,
            path
        )

    def get_document_images_path(self, hash_value):
        """Get relative document images path for a file hash

        Parameters
        ----------
        hash_value: str
            A string containing file hash

        """

        return self.get_document_path(
            DOCUMENT_IMAGE_PREFIX + hash_value
        )

    def get_pdf_images_path(self, hash_value):
        """Get relative pdf media images path for a file hash

        Parameters
        ----------
        hash_value: str
            A string containing file hash

        """

        return self.get_pdf_path(
            PDF_IMAGE_PREFIX + hash_value
        )


class DependencyVideoMediaQuerySet(SyncableQuerySet, PolymorphicQuerySetForDependency):
    """QuerySet for the ``mtp.apps.media.models.VideoMedia`` model"""

    pass


class DependencyVideoMediaManager(PolymorphicManagerForDependency):
    """Manager for the ``mtp.apps.media.models.VideoMedia`` model"""

    pass


class DependencyPdfMediaQuerySet(SyncableQuerySet, PolymorphicQuerySetForDependency):
    """QuerySet for the ``mtp.apps.media.models.PdfMedia`` model"""

    pass


class DependencyPdfMediaManager(PolymorphicManagerForDependency):
    """Manager for the ``mtp.apps.media.models.PdfMedia`` model"""

    pass


class DependencyPresentationMediaQuerySet(DependencyQuerySet, SyncableQuerySet):
    """QuerySet for the ``mtp.apps.media.models.PresentationMedia`` model"""

    def get_media_for_presentation(self, presentation, is_restricted=False):
        """Get the media linked to the provided ``Presentation``

        Parameters
        ----------
        presentation: Presentation
            The presentation for which we want to get the media
        is_restricted: bool
            If True, we will get the restricted media,
            If False, we will get the non-restricted media

        Returns
        -------
        ``Media`` or None

        """

        try:
            return self.get(presentation=presentation, is_restricted=is_restricted).media
        except (ObjectDoesNotExist, MultipleObjectsReturned):
            return None


class DependencyPresentationMediaManager(DependencyManager):
    """Manager for the ``mtp.apps.media.models.PresentationMedia`` model"""

    pass


class NoteQueryset(SyncableQuerySet):
    """Queryset for the ``mtp.apps.media.models.Note`` model"""

    def without_presentation(self):
        """Filter only notes not linked to a ``Presentation``

        Returns
        -------
        NoteQueryset
            A clone of the original queryset (``self``), filtered.

        """

        return self.filter(presentation__isnull=True)

    def for_presentation(self, presentation):
        """Filter only notes linked to the given ``Presentation``

        Parameters
        ----------
        presentation: Presentation
            The presentation we want to want the notes linked to

        Returns
        -------
        NoteQueryset
            A clone of the original queryset (``self``), filtered.

        """

        return self.filter(presentation=presentation)
