"""This module contains the ``update_up`` django command"""

import io
import json
import os
import requests
from requests.exceptions import HTTPError
from uuid import getnode as get_mac
from zipfile import ZipFile, ZIP_LZMA

from django.conf import settings
from django.core import serializers
from django.core.management.base import BaseCommand, CommandError
from django.core.urlresolvers import reverse

from tendo import singleton

import mtp
from mtp.apps.common.utils import bypass_orderable
from mtp.apps.sync.hooks import run_pre_sync_hooks, run_post_sync_hooks
from mtp.apps.sync.models import Syncable, Update


class Command(BaseCommand):
    """Django admin-command to update a "master" instance from a "slave" one"""

    args = ''
    help = 'Updating the "master" instance with the data from the current "slave"'

    def check_version(self):
        """Check that the "master" instance version if the same of this current "slave" version

        Raises
        ------
        CommandError
            If the "master" and "slave" version are different

        """

        settings_url = settings.MASTER_INSTANCE_URL + reverse('apiv2:client_settings')
        response = requests.get(settings_url)

        try:
            response.raise_for_status()
            version = response.json()['server_version']
        except (ValueError, KeyError, HTTPError):
            raise CommandError(
                'Failed to check the master instance version. '
                'Got unexpected response with status %d from the master instance' % (
                    response.status_code
                )
            )

        if mtp.__version__ != version:
            raise CommandError(
                'MTP version mismatch. The slave version is %(slave_version)s and '
                'the master version is %(master_version)s' % {
                    'slave_version': mtp.__version__,
                    'master_version': version
                }
            )

    def upload_data(self, data_to_sync):
        """Upload the serialized ``Syncable`` instances to the "master" instance

        Parameters
        ----------
        data_to_sync: list
            The list of ``Syncable`` instance to upload to the "master"

        """

        zip_buffer = io.BytesIO()
        zip_file = ZipFile(zip_buffer, 'w', ZIP_LZMA)
        zip_file.setpassword(bytes(settings.SYNC_ZIP_PASSWORD, encoding='utf-8'))

        serialized_objects = []
        for data in data_to_sync:
            serialized_objects.extend(data.serialize_for_sync())

        zip_file.writestr('data.json', json.dumps(serialized_objects))
        zip_file.close()

        update_up_url = settings.MASTER_INSTANCE_URL + reverse('sync:upload')
        response = requests.post(
            update_up_url,
            {'mac': get_mac(), 'version': mtp.__version__},
            files={'data_archive': ('data_archive.zip', zip_buffer.getvalue(),)}
        )

        try:
            response.raise_for_status()
        except HTTPError:
            try:
                error_message = response.json()['message']
            except (ValueError, KeyError):
                raise CommandError(
                    'Failed to upload the data. '
                    'Got unexpected response with status %d from the master instance' % (
                        response.status_code
                    )
                )
            else:
                raise CommandError(
                    'Failed to upload the data. The master instance answer was "%s"' % error_message
                )

        with bypass_orderable():  # Keep order from objects in the response
            for deserialized_object in serializers.deserialize('json', response.text):
                deserialized_object.save()

    def upload_media_files(self, files_to_sync):
        """Upload the media files of the ``Syncable`` models to the "master" instance

        Parameters
        ----------
        files_to_sync: list(dict)
            A list of dict of the media files to synchronize

        """

        zip_buffer = io.BytesIO()
        zip_file = ZipFile(zip_buffer, 'w', ZIP_LZMA)
        zip_file.setpassword(bytes(settings.SYNC_ZIP_PASSWORD, encoding='utf-8'))

        for file in files_to_sync:
            file_path = os.path.join(settings.MEDIA_ROOT, file)
            zip_file.write(file_path, file)

        zip_file.close()

        upload_url = settings.MASTER_INSTANCE_URL + reverse('sync:upload_media_archive')
        response = requests.post(
            upload_url,
            files={
                'media_archive': ('media_archive.zip', zip_buffer.getvalue(),)
            }
        )

        try:
            # Raise if we got anything but a 200
            response.raise_for_status()
        except HTTPError:
            try:
                error_message = response.json()['message']
            except (ValueError, KeyError):
                raise CommandError(
                    'Failed to upload the media. '
                    'Got unexpected response with status %d from the master instance' % (
                        response.status_code
                    )
                )
            else:
                raise CommandError(
                    'Failed to upload the media. The master instance answer was "%s"' % (
                        error_message
                    )
                )

    def handle(self, *args, **options):
        """Main routine of the "update_up" command

        The steps of the "Up" sync are the following:
        - Running the pre-sync hooks
        - Collecting the ``Syncable`` model instances who have the ``update_locally`` to ``True``
        - Uploading an archive with the media files linked to these instances
        - Uploading the json-serialized ``Syncable`` instances
        - Updating these instances with the last ``Update`` object from the "master"
        - Running the post-sync hooks

        """

        singleton.SingleInstance()

        if settings.INSTANCE_IS_MASTER:
            raise CommandError('This command can only be run from a "SLAVE" instance')

        # Check that the slave version and the master version are the same
        self.check_version()

        self.stdout.write('Running pre-sync hooks')
        run_pre_sync_hooks()
        self.stdout.write('=> DONE!')

        objects_to_sync, files_to_sync, queryset_list = Syncable.get_objects_and_files_to_sync()

        data_to_sync_nb = len(objects_to_sync)

        # No data to send to the master, abort
        if not data_to_sync_nb:
            self.stdout.write('No data to upload')
            return

        self.stdout.write('Data to upload (%d):' % data_to_sync_nb)
        for object_to_sync in objects_to_sync:
            self.stdout.write('%s: %s' % (object_to_sync.__class__.__name__, object_to_sync,))

        self.stdout.write('Files to upload (%d):' % len(files_to_sync))
        for file in files_to_sync:
            self.stdout.write(file)

        if files_to_sync:
            self.stdout.write('Uploading the media files archive...')
            self.upload_media_files(files_to_sync)
            self.stdout.write('Media files archive successfully uploaded !')

        self.stdout.write('Uploading the data...')
        self.upload_data(objects_to_sync)
        self.stdout.write('Data successfully uploaded !')

        self.stdout.write('Updating the data using the last update...')
        # If everything went right, update the object we just send with the last update
        last_update = Update.objects.order_by('-date').first()
        for queryset in queryset_list:
            queryset.update(last_update=last_update, update_locally=False)

        last_update.healthy = True
        last_update.save()
        self.stdout.write('Update successful !')

        self.stdout.write('Run post-sync hooks')
        run_post_sync_hooks()
        self.stdout.write('=> DONE!')
