# -*- coding: utf-8 -*-

import datetime
import hashlib
import logging
import subprocess
import sys

from colorama import Fore

from django.conf import settings
from django.core.management import call_command
from django.db import models

from mtp.discrete_apps.models_dependency.manager import DependencyManager

from mtp.apps.common.models import UUIDPkModel
from mtp.apps.common.utils import upload_to_generator
from mtp.apps.sync.hooks import post_sync_hook
from mtp.apps.sync.managers import SyncableManager


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


"""
Legacy comments:

# DB update, only create on Global server. When a LocalServer sync with
# the GS, one Update is create for all content create on LocalServer,
# then, when the sync is finish, a second Update is create and will be
# use for all new content create on GS

# ID: UUId
# is_deleted: no delete possible
# sync: GS -> LS
# order: date
"""


class Syncable(UUIDPkModel):

    is_deleted = models.BooleanField(default=False)  # TODO: add index here!

    first_update = models.ForeignKey('Update', blank=True, null=True,
                                     related_name="%(app_label)s_%(class)s_by_first")
    last_update = models.ForeignKey('Update', blank=True, null=True,
                                    related_name="%(app_label)s_%(class)s_by_last")

    # This field was previously called `localy_update`...
    # TODO: Check if renaming it has any problematic side effects.
    update_locally = models.BooleanField(default=False)

    objects = SyncableManager()

    class Meta:
        abstract = True

    @staticmethod
    def get_syncable_models():
        """Return the ordered list of syncable models.

        The ordering depends of the dependencies: each dependencies of a model should come
        before a model in the list.

        Returns
        -------
        list
            List the representation of all models to synchronize.

        """

        return DependencyManager.get_dependencies(Syncable)

    def save(self, *args, **kwargs):
        if 'mtp_update' in kwargs:
            kwargs.pop('mtp_update')
            super(Syncable, self).save(*args, **kwargs)
            return

        if settings.INSTANCE_IS_SLAVE:
            self.update_locally = True
        else:
            update = Update.get_unsync_update()
            self.update_locally = False
            self.last_update = update
            if not self.first_update:
                self.first_update = self.last_update

        super(Syncable, self).save(*args, **kwargs)

    def remove(self):
        """Soft-delete a syncable object."""

        if not self.is_deleted:
            self.is_deleted = True
            self.save()

    def get_sync_objects(self):
        """
        If an object has a dependancy with non-Syncable objects, this method
        needs to return a list with the dependancy first.
        """
        return [self, ]

    def get_sync_files(self):
        """
        If this object refers to media, this method needs to return the list of
        media as a dict with `hash` and `name` properties.
        """
        return []  # TODO...? FIXME...?


class Update(UUIDPkModel):
    """
    healthy: only set on local server when the synchronization for this update is completed
    synchronized: is false as long as no local server has been synchronized with this update
    """

    date = models.DateTimeField(auto_now_add=True, db_index=True)
    healthy = models.BooleanField(default=False)
    synchronized = models.BooleanField(default=False)

    class Meta:
        ordering = ('-date',)

    def __str__(self):
        return "%s" % self.date

    @classmethod
    def get_unsync_update(cls):
        try:
            update = cls.objects.get(synchronized=False)
        except cls.DoesNotExist:
            update = cls(synchronized=False)
            update.save()

        # FIXME: This happened when importing the Renault database. Why?
        except cls.MultipleObjectsReturned:
            update = None

        return update

    @classmethod
    def synchronize(cls):
        cls.objects.filter(synchronized=False).update(synchronized=True)

    @classmethod
    def new_update_for_sync(cls):
        update = cls(synchronized=True)
        update.save()
        return update


class LocalServer(Syncable):
    mac = models.CharField(max_length=50, unique=True)
    name = models.CharField(max_length=150, blank=True)
    currently_update = models.BooleanField(default=False)

    def __str__(self):
        return self.mac

    def is_connected(self):
        if self.localserverconnection_set.filter(
            date__gte=datetime.datetime.now() - datetime.timedelta(minutes=1, seconds=40)
        ).count() > 0:
            return True
        return False

    def last_connection(self):
        connections = self.localserverconnection_set.all().order_by('-date')
        if len(connections) != 0:
            return connections[0].date
        else:
            return ""

    def uptime(self):
        connections = self.localserverconnection_set.all().order_by('-date')
        if len(connections) != 0:
            return connections[0].time
        else:
            return ""

    def is_updated(self):
        connections = self.localserverconnection_set.all().order_by('-date')
        if len(connections) != 0:
            if connections[0].update_count == Update.objects.filter(synchronized=True).count():
                return True
            else:
                return False
        else:
            return ""

    def needed_updates(self):

        last_connection = self.localserverconnection_set.order_by('-date').first()

        if last_connection:
            return Update.objects.filter(synchronized=True).count() - last_connection.update_count
        else:
            return Update.objects.filter(synchronized=True).count()

        return 123  # guaranteed successful value™


class LocalServerConnection(models.Model):
    local_server = models.ForeignKey('LocalServer')
    date = models.DateTimeField(auto_now_add=True, db_index=True)
    time = models.PositiveIntegerField(default=1)
    update_count = models.PositiveIntegerField(default=0)


class ServerVersion(Syncable):

    name = models.CharField(max_length=30, db_index=True)
    version = models.PositiveIntegerField(db_index=True)
    file = models.FileField(upload_to=upload_to_generator)
    hash = models.CharField(max_length=36, blank=True, null=True)

    class Meta:
        ordering = ('name', '-version',)
        index_together = [['name', 'version', ], ]

    def get_sync_files(self):
        return [{'name': self.file.name, 'hash': self.hash}, ]


class ClientVersion(Syncable):

    name = models.CharField(max_length=30, db_index=True)
    bundle_name = models.CharField(max_length=255)
    version = models.PositiveIntegerField(db_index=True)
    file = models.FileField(upload_to=upload_to_generator)
    hash = models.CharField(max_length=36, blank=True, null=True)

    class Meta:
        ordering = ('-version',)
        index_together = [['name', 'version', ], ]

    def get_sync_files(self):
        return [{'name': self.file.name, 'hash': self.hash}, ]

    def save(self, *args, **kwargs):
        try:
            md5 = hashlib.md5()
            for chunk in self.file.chunks():
                md5.update(chunk)
            self.hash = md5.hexdigest()
        except:
            pass

        super(ClientVersion, self).save(*args, **kwargs)


class Task(models.Model):

    date = models.DateTimeField(auto_now_add=True, db_index=True)
    path = models.CharField(max_length=1000)
    done = models.BooleanField(default=False, db_index=True)
    finish_path = models.CharField(max_length=1000)

    class Meta:
        ordering = ('done', 'date',)
        index_together = [['done', 'date', ], ]

    def launch(self):
        try:
            retcode = subprocess.call(self.path, shell=True)
            if retcode != 0:
                return False
            else:
                return True
        except OSError as e:
            print("Execution failed:", e, file=sys.stderr)

    def finish(self):
        try:
            retcode = subprocess.call(self.finish_path, shell=True)
            if retcode != 0:
                return False
            else:
                return True
        except OSError as e:
            print("Execution failed:", e, file=sys.stderr)


@post_sync_hook(settings.INSTANCE_IS_SLAVE)
def update_waffle_from_master():
    """After sync from master to slave, replace all waffle data with data from the master."""

    print(Fore.GREEN + '  Update waffle from master' + Fore.RESET)
    call_command('update_waffle_from_master')
    print(Fore.GREEN + '  => DONE!' + Fore.RESET)

# Signals need to be imported in ``models`` file to be seen by django
from .signals import m2m_changed_syncable
