"""This module holds the Mangers for the ``photostory`` app."""
import logging
import os
import re

from shutil import copyfile

from django.conf import settings
from django.core.exceptions import ObjectDoesNotExist
from django.core.files.base import File
from django.db import transaction

from mtp.apps.accounts.models import MTPUser
from mtp.apps.trainings.managers import TrainingActivityManager
from mtp.apps.trainings.models import TrainingSession, Training

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

DATE_FORMAT = "%Y-%m-%d_%H-%M-%S"
UUID_REGEX_STRING = '[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}'
UUID_REGEX = re.compile(UUID_REGEX_STRING)
PHOTO_REGEX_STRING = '_result_session_%s_ps_user_%s_.*.jpg' % (UUID_REGEX_STRING, UUID_REGEX_STRING)
PHOTO_REGEX = re.compile(PHOTO_REGEX_STRING)

class PhotostoryManager(TrainingActivityManager):
    """Manager used by the ``Photostory`` model."""

    use_in_migrations = True

    def list_trainings_dir(self, photostories_path):
        """ This function iterate on training folder

        Parameters
        ----------
        photostories_path: string
            The path containing training sub directories.

        Yields
        -------
        training_path: str
            The path of the actual training folder
        training: Training
            The training corresponding of the training_path

        """

        # The folder is named with the training ID
        for training_id in os.listdir(photostories_path):
            if not UUID_REGEX.match(training_id):
                logger.error('Training Id %s is not well formated', training_id)
                continue

            training_path = os.path.join(photostories_path, training_id)
            if not os.path.isdir(training_path):
                logger.error('Training path %s is not a directory', training_path)
                continue

            if not Training.objects.filter(pk=training_id).exists():
                logger.error('Training %s does not exist', training_id)
                continue

            yield training_path


    def list_sessions_dir(self, training_path):
        """ This function iterate on session folder

        Parameters
        ----------
        training_path: str
            The path of the actual training folder containing sessions sub directories

        Yields
        -------
        session_path: str
            The path of the actual session folder
        session: TrainingSession
            The session corresponding of the session_path

        """

        # The folder is named with the session ID
        for session_id in os.listdir(training_path):
            if not UUID_REGEX.match(session_id):
                logger.error('Session Id %s is not well formated', session_id)
                continue

            session_path = os.path.join(training_path, session_id)
            if not os.path.isdir(training_path):
                logger.error('Session path %s is not a directory', session_path)
                continue

            try:
                session = TrainingSession.objects.get(pk=session_id)
            except ObjectDoesNotExist:
                logger.error('Session %s does not exist', session_id)
                continue

            yield session_path, session

    def list_photos_files(self, session_path):
        """ This function iterate on photo files into a directory

        Parameters
        ----------
        session_path: str
            The path of the actual session folder containing photo files

        Yields
        -------
        photo_path: str
            The path of the photo file
        user: MTPUser
            The user corresponding of the photo_path

        """

        users = {}
        # The file is named with the session ID, User Id, and the date
        for photo_name in os.listdir(session_path):
            if not PHOTO_REGEX.match(photo_name):
                logger.error('Photo file name %s is not well formated', photo_name)
                continue

            photo_path = os.path.join(session_path, photo_name)
            if not os.path.isfile(photo_path):
                logger.error('Photo path %s is not file', photo_path)
                continue

            user_id = UUID_REGEX.findall(photo_name)[1]  # The second UUID in photo filename
            if user_id not in users:
                try:
                    users[user_id] = MTPUser.objects.get(pk=user_id)
                except ObjectDoesNotExist:
                    logger.error('User %s does not exist', user_id)
                    continue

            yield photo_path, users[user_id]

    def import_photo(self, photostory, session, user, photo_path):
        """ This function import a photo file in an instance of Photo linked to a new strip
        There is a transaction to avoid to create a strip and photo if photo save failed
        At the end, we delete old photo file

        Parameters
        ----------
        photostory: Photostory
            The photostory which contains the future strip
        session: TrainingSession
            The session corresponding of the session_path
        user: MTPUser
            The owner of the photo
        photo_path: str
            The path of the photo file

        Returns
        -------
        bool
            The success of the photo migration

        """

        from .models import Photo, Strip

        try:
            with transaction.atomic():
                try:
                    # Get a strip to attach the photo
                    strip, __ = Strip.objects.get_or_create(
                        photostory=photostory,
                        session=session,
                        owner=user,
                        defaults={
                            'name': '%s - %s %s' % (
                                photostory.name,
                                user.first_name,
                                user.last_name
                            ),
                            'state': Strip.STRIP_STATE.PUBLISHED
                        }
                    )
                except Exception:
                    logger.error('Error during creating strip for file : %s', photo_path)
                    raise

                # Read file and add it to Photo instance
                try:
                    photo_file = open(photo_path, 'rb')
                except Exception:
                    # When there is an error reading file
                    logger.error('Error during opening photo file %s', photo_path)
                    raise
                else:
                    name = os.path.split(photo_path)[1]
                    try:
                        Photo.objects.create(strip=strip, image=File(photo_file, name=name))
                    except Exception:
                        logger.error('Error during creating photo for file : %s', photo_path)
                        raise
                    finally:
                        photo_file.close()

        except Exception as exception: # pylint: disable=broad-except
            logger.exception('Error during importing photo %s', exception)
            return False
        else:
            return True

    def export_photo(self, photostories_path, strip, photo):
        """ This function export a photo in a directory

        Parameters
        ----------
        photostories_path: string
            The path that will contain training sub directories.
        strip: Strip
            The strip which contains the photo
        photo: Photo
            An instance of the model photo

        Returns
        -------
        Boolean

        """

        src_file = os.path.join(settings.MEDIA_ROOT, photo.image.path)
        dest_path = os.path.join(photostories_path, strip.session.training_id, strip.session_id)

        # If dest path doesn't exist, we create it
        if not os.path.exists(dest_path):
            try:
                os.makedirs(dest_path)
            except OSError:
                logger.error('Error during creating photo dir %s', dest_path)
                return False
        elif not os.path.isdir(dest_path):
            logger.error('The path %s already exist but it is not a dir', dest_path)
            return False

        # If dest filename already exist, we incremente the filename
        i = 0
        photo_date = photo.created.strftime(DATE_FORMAT)
        while True:
            dest_name = '_result_session_%s_ps_user_%s_%s_%i.jpg' % (
                strip.session_id,
                strip.owner_id,
                photo_date,
                i
            )
            dest_file = os.path.join(dest_path, dest_name)
            if not os.path.isfile(dest_file):
                break
            i += 1

        # We copy the file
        try:
            copyfile(src_file, dest_file)
        except OSError:
            logger.error('Error copying file %s to %s', src_file, dest_file)
            return False

        return True

    def import_photostory_from(self, photostories_path):
        """ This function import old photostory files format into Strip

        Parameters
        ----------
        photostories_path: string
            The path that contain old photostories.

        """

        from .models import Photostory

        # The first level is named with the training ID
        for training_path in self.list_trainings_dir(photostories_path):
            # The second level is named with the training ID
            for session_path, session in self.list_sessions_dir(training_path):
                # We create a live strip photostory by session
                try:
                    photostory = Photostory.objects.create(
                        training=None,
                        session=session,
                        name='Photostory',
                        description='Photostory'
                    )
                except Exception as exception: # pylint: disable=broad-except
                    logger.exception(
                        'Error during creation of photostory for session %s : %s',
                        session.id,
                        exception
                    )
                    continue
                # The third level is named with the session ID, user ID, and Date
                for photo_path, user in self.list_photos_files(session_path):
                    # Migrate strip
                    if self.import_photo(photostory, session, user, photo_path):
                        # If photo is created, we remove photo file
                        try:
                            os.remove(photo_path)
                        except OSError:
                            logger.error('Error removing photo %s', photo_path)

                # Remove session path if empty
                if not os.listdir(session_path):
                    try:
                        os.rmdir(session_path)
                    except OSError:
                        logger.error('Error removing empty session dir %s', session_path)

            # Remove training path if empty
            if not os.listdir(training_path):
                try:
                    os.rmdir(training_path)
                except OSError:
                    logger.error('Error removing empty training dir %s', training_path)

    def export_photostory_to(self, photostories_path):
        """ This function export strips into old photostory files format

        Parameters
        ----------
        photostories_path: string
            The path that will contains photostories files.

        """

        from .models import Photo, Photostory, Strip

        photostories = Photostory.objects.prefetch_related('strips__photos').all()

        for photostory in photostories:
            for strip in photostory.strips.all():
                for photo in strip.photos.all():

                    # If photo is reversed, we remove photo instance
                    if self.export_photo(photostories_path, strip, photo):
                        try:
                            photo.delete()
                        except ObjectDoesNotExist:
                            logger.error('Error during deleting photo instance %s', photo.id)

                # if there is no photo, we remove the strip
                # We don't use strip.photos.count(), because we need to refresh data
                if Photo.objects.filter(strip=strip.id).count() == 0:
                    try:
                        strip.delete()
                    except ObjectDoesNotExist:
                        logger.error('Error during deleting strip instance %s', strip.id)

            # if there is no strip, and it's a live photostory we remove the photostory
            # We don't use photostory.strips.count(), because we need to refresh data
            if Strip.objects.filter(photostory=photostory).count() == 0 and photostory.is_live:
                try:
                    photostory.delete()
                except ObjectDoesNotExist:
                    logger.error('Error during deleting photostory instance %s', photostory.id)

