"""This module contains the views of the ``sync`` app"""

import datetime
import logging
import json

from django.core import serializers
from django.db import transaction
from django.http import (
    HttpResponse, HttpResponseBadRequest,
    HttpResponseForbidden, JsonResponse
)
from django.utils.decorators import method_decorator
from django.utils.timezone import now
from django.views.decorators.csrf import csrf_exempt
from django.views.generic import View

from waffle.models import Switch

import mtp
from mtp.apps.common.utils import *
from mtp.apps.common.views_utils import CrsfExemptMixin, HandleZipFileMixin
from mtp.apps.trainings.models import *
from .models import (
    LocalServer, LocalServerConnection,
    Syncable
)


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


@csrf_exempt
def ping(request):
    if request.method == 'POST':
        if 'mac' in request.POST:
            mac = request.POST['mac']
            update_count = int(request.POST['update_count'])
            try:
                local_server = LocalServer.objects.get(mac=mac)
            except:
                local_server = LocalServer(mac=mac)
                local_server.save()

            try:
                connection = local_server.localserverconnection_set.filter(
                    date__gte=now() -
                    datetime.timedelta(
                        minutes=1,
                        seconds=40)).order_by('-date')[0]
                if connection.update_count != update_count:
                    connection = LocalServerConnection(
                        local_server=local_server,
                        time=connection.time + 1,
                        update_count=update_count)
                else:
                    connection.time = connection.time + 1
                    connection.date = now()
            except:
                connection = LocalServerConnection(local_server=local_server,
                                                   update_count=update_count)

            connection.save()
    return HttpResponse('')


def save_db(request):
    if request.method == 'POST':
        filename = os.path.join(settings.PGDUMP_SAVE_ROOT, request.POST['filename'])
        handle_uploaded_file(request.FILES['file'], filename)

        return HttpResponse('')
    return HttpResponseBadRequest('')


def save_revision(request):
    if request.method == 'POST':
        filename = os.path.join(settings.REV_SAVE_ROOT, request.POST['filename'])
        handle_uploaded_file(request.FILES['file'], filename)

        return HttpResponse('')
    return HttpResponseBadRequest('')


def upload_file(request):
    if request.method == 'POST':
        folder = os.path.join(settings.MEDIA_ROOT, request.POST['folder'])

        if not os.path.exists(folder):
            os.makedirs(folder)

        filename = os.path.join(folder, request.POST['filename'])
        handle_uploaded_file(request.FILES['file'], filename)

        return HttpResponse('')
    return HttpResponseBadRequest('')


class DownloadWaffle(View):
    """A view to download data related to the ``waffle`` app.

    Currently, only switches are returned.

    Request
    -------
    This view must be called in POST mode with post-data:
        waffle-download = 1

    Response
    --------
    json
        The response of this view is json formatted. It's a dict following this schema:

        {
            'waffle': {  # name of the "waffle" app
                'switch': [  # name of the "switch" model
                    {  # one dict per switch
                        "model": "waffle.switch",
                        "pk": 1,
                        "fields": {
                            "active": false,
                            "note": "",
                            "modified": "2015-06-20T13:51:48.812Z",
                            "name": "test1",
                            "created": "2015-06-02T12:12:12Z"
                        }
                    }
                ]
            }
        }

    """

    @method_decorator(csrf_exempt)
    def dispatch(self, request, *args, **kwargs):
        """Override the ``dispatch`` method to apply the ``csrf_exempt`` decorator.

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

        return super().dispatch(request, *args, **kwargs)

    def post(self, request, *args, **kwargs):  # pylint: disable=unused-argument
        """Check request validity and return a response with json data.

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

        if 'download-waffle' not in request.POST:
            return HttpResponseForbidden('')

        return self.render_to_response(self.get_data())

    def get_data(self):
        """Get, convert to json, and return all switches."""

        switches = Switch.objects.all().order_by('name')
        json_switches = serializers.serialize('json', switches)
        return {'waffle': {'switch': json_switches}}

    def render_to_response(self, data, **response_kwargs):
        """Return a ``JsonResponse`` object with given data as content."""

        return JsonResponse(
            data,
            **response_kwargs
        )


class UploadDataArchive(CrsfExemptMixin, HandleZipFileMixin, View):
    """A view to upload an archive containing the data of a "SLAVE" instance"""

    def post(self, request, *args, **kwargs):  # pylint: disable=unused-argument
        """Handle the archive containing the serialized data"""

        slave_mac = request.POST.get('mac', None)
        slave_version = request.POST.get('version', None)

        if not slave_mac or not slave_version:
            return JsonResponse(
                status=400,
                data={'message': 'No mac or server version provided'}
            )

        if mtp.__version__ != slave_version:
            return JsonResponse(
                status=400,
                data={'message': 'Server version mismatch'}
            )

        try:
            zip_file = self.handle_zipfile('data_archive')
        except ValueError:
            return JsonResponse(
                status=400,
                data={'message': 'Bad Zip file'}
            )

        try:
            json_data = self.decode_zipped_json_data(zip_file)
        except ValueError:
            return JsonResponse(
                status=400,
                data={'message': 'data.json not found or incorrect json'}
            )

        # Store the archive, because we may need it later to know what's wrong
        archive_name = 'sync_upload_%s_%s.json' % (slave_mac, now().date())
        archive_path = os.path.join(settings.SYNC_HISTORY_PATH, archive_name)

        os.makedirs(settings.SYNC_HISTORY_PATH, exist_ok=True)
        with open(archive_path, 'w') as file:
            file.write(json_data)

        # Time to handle the data send by this "slave" instance
        try:
            with transaction.atomic():
                last_update = Syncable.update_from_slave(slave_mac, json_data)
        except Exception as exception:  # pylint: disable=broad-except
            return JsonResponse(
                status=400,
                data={
                    'message': 'Update failed, the exception message is "%s"' % (
                        exception
                    )
                }
            )

        # Remove the file containing the data, because everything went right
        os.remove(archive_path)

        # Finally, answer with the json-serialized ``Update`` so the "slave" can update itself
        combined = (last_update,)
        last_update_json = serializers.serialize('json', combined)

        return HttpResponse(last_update_json)

    def decode_zipped_json_data(self, zip_file):
        """Extract and decode the json data from the provided zipfile

        Parameters
        ----------
        zip_file: ZipFile
            The zip file containing the json data file

        Returns
        -------
        str
            The decoded json data, as a str

        Raises
        ------
        ValueError
            If the ZipFile does not contain the expected file,
            or if the data file does not contain valid json

        """

        # Get the data.json from the archive
        try:
            zip_file.getinfo('data.json')  # Will raise KeyError if data.json is not found
            data = zip_file.read('data.json', pwd=bytes(settings.SYNC_ZIP_PASSWORD, 'utf-8'))
            json_data = json.loads(data.decode('utf-8'))  # Will raise if not valid json
        except (KeyError, ValueError):
            raise ValueError

        return json.dumps(json_data)


class UploadMediaArchive(CrsfExemptMixin, HandleZipFileMixin, View):
    """A view to upload an archive containing the media files of a "SLAVE" instance"""

    def post(self, request, *args, **kwargs):  # pylint: disable=unused-argument
        """Handle the archive containing the media files"""

        try:
            zip_file = self.handle_zipfile('media_archive')
        except ValueError:
            return JsonResponse(
                status=400,
                data={'message': 'Bad Zip file'}
            )

        # Decompress the archive into the media folder
        zip_file.extractall(settings.MEDIA_ROOT, pwd=bytes(settings.SYNC_ZIP_PASSWORD, 'utf-8'))

        return JsonResponse({})
