import datetime
import logging

from crequest.middleware import CrequestMiddleware
from django.contrib.auth import get_user_model
from django.db import models
from django.urls import reverse, NoReverseMatch
from django.utils.timezone import now
from django.utils.translation import ugettext_lazy as _

from . import app_settings, registry

logger = logging.getLogger(__name__)
User = get_user_model()


class DiffingMixin(object):
    def __init__(self, *args, **kwargs):
        super(DiffingMixin, self).__init__(*args, **kwargs)
        self._original_state = dict(self.__dict__)
        self.pre_current_state = None

    def save(self, *args, **kwargs):
        state = dict(self.__dict__)
        del state['_original_state']
        super(DiffingMixin, self).save(*args, **kwargs)
        self.pre_current_state = self._original_state
        self._original_state = state

    def is_dirty(self):
        missing = object()
        for key, value in self._original_state.items():
            try:
                if value != self.__dict__.get(key, missing):
                    return True
            except TypeError as e:
                o = self.__dict__.get(key, missing)
                if type(value) == datetime.datetime and type(o) == datetime.datetime:
                    # Make both unaware
                    o = o.replace(tzinfo=None)
                    value = value.replace(tzinfo=None)
                    if value != o:
                        return True

                else:
                    raise e
        return False

    def changed_columns(self):
        missing = object()
        result = {}
        for key, value in self._original_state.items():
            try:
                if value != self.__dict__.get(key, missing):
                    result[key] = {'old': value, 'new': self.__dict__.get(key, missing)}
            except TypeError:
                result[key] = {'old': value, 'new': self.__dict__.get(key, missing)}
        return result


class RAModel(DiffingMixin, models.Model):
    owner = models.ForeignKey(User, related_name='%(app_label)s_%(class)s_related', verbose_name=_('owner'),
                              on_delete=models.CASCADE)
    creation_date = models.DateTimeField(_('creation date and time'), default=now)
    lastmod = models.DateTimeField(_('last modification'), db_index=True)
    lastmod_user = models.ForeignKey(User, related_name='%(app_label)s_%(class)s_lastmod_related',
                                     verbose_name=_('last modification by'), on_delete=models.CASCADE)

    class Meta:
        abstract = True


class RaModelMixin(object):
    """
    This is a sample interface for integrating with teh framework
    """


class EntityModel(RAModel):
    """
    The Main base for Ra `static` models
    Example: Client , Expense etc..
    """
    slug = models.SlugField(_('refer code'), help_text=_('For fast recall'), max_length=50,
                            unique=True, db_index=True, blank=True)
    title = models.CharField(_('name'), max_length=255, unique=True, db_index=True)
    notes = models.TextField(_('notes'), null=True, blank=True)

    # fb = models.DecimalField(_('beginning balance'), help_text=_('Opening Balance or initial balance '), max_digits=19,
    #                          decimal_places=2, default=0)

    class Meta:
        abstract = True

    def __init__(self, *args, **kwargs):
        super(EntityModel, self).__init__(*args, **kwargs)
        # self.reporting_model = None
        if not getattr(self, 'pk_name', False):
            self.pk_name = None

    def __str__(self):
        return self.title

    @classmethod
    def get_class_name(cls):
        """
        return the class name, usable when a ra model is mimicking (ie:proxying)
        another model.
        This method is used is get_doc_type_* functions,
        This method is made to avoid to repeat registered doc_type to make adjustments
        """
        return cls.__name__

    @classmethod
    def get_model_name(cls):
        """
        A convenience method to get the base model name, needed in templates
        :return:
        """

        return cls._meta.model_name.lower()

    @classmethod
    def get_verbose_name_plural(cls):
        """
        A convenience method to get the base model verbose name, needed in templates
        :return:
        """
        return cls._meta.verbose_name_plural

    def get_absolute_url(self):
        model_name = self._meta.model_name.lower()
        try:
            url = reverse('%s:%s_%s_view' % (app_settings.RA_ADMIN_SITE_NAME, self._meta.app_label, model_name)
                          , args=(self.pk,))
        except NoReverseMatch:
            url = reverse(
                '%s:%s_%s_change' % (
                    app_settings.RA_ADMIN_SITE_NAME, self._meta.app_label, self.get_class_name().lower())
                , args=(self.pk,))
        return url

    @property
    def name(self):
        return self.title

    def get_next_slug(self):
        """
        Get the next slug
        If it's a new instance and the slug is not provided, we try and attempt a serial over the already added slugs
        in relation to the model
        :return:
        """
        from .helpers import get_next_serial
        return get_next_serial(self.__class__)  # repr(time.time()).replace('.', '')

    def save(self, force_insert=False, force_update=False, using=None, update_fields=None):
        if self.pk is None:
            if not self.slug:
                self.slug = self.get_next_slug()
            if not self.owner_id:
                try:
                    self.owner = self.lastmod_user
                except:
                    self.owner_id = self.lastmod_user_id
                    logger.info('lastmod_user_id is used instead of lastmod_user object')

        self.lastmod = now()

        super(EntityModel, self).save(force_insert, force_update, using, update_fields)

    @classmethod
    def _get_doc_type_plus_list(cls):
        '''

        Returns List of Identified doctype that a plus effect on the entity
        '''
        return ['fb'] + registry.get_model_doc_type_map(cls.get_class_name()).get('plus_list', [])

    @classmethod
    def _get_doc_type_minus_list(cls):
        """ Returns List of Identified doctype that a minus effect on the entity"""

        return registry.get_model_doc_type_map(cls.get_class_name()).get('minus_list', [])

    @classmethod
    def get_doc_type_neuter_list(cls):
        """ Returns List of Identified doctype that have a neuttral effect on the entity"""

        return []

    #
    # @classmethod
    # def get_doc_type_full_map(cls):
    #     from .registry import model_doc_type_full_map
    #
    #     doc_types_unfiltered = model_doc_type_full_map.get(cls.get_class_name(), [])
    #     doc_typed_filtered = []
    #     for doc_type in doc_types_unfiltered:
    #         if not doc_type.get('hidden', False):
    #             doc_typed_filtered.append(doc_type)
    #
    #     return doc_typed_filtered

    # @classmethod
    # def get_doc_types(cls):
    #     """
    #     Return a list of the doc_types supported by the current model , Must implemented when needed by children
    #     @return:
    #     """
    #     return cls._get_doc_type_plus_list() + cls._get_doc_type_minus_list() + cls.get_doc_type_neuter_list()

    def get_pk_name(self):
        """
        This is used to get the full name of the primary key,
        a bit hackish but is important for reports.
        :return:
        """
        if self.pk_name:
            return self.pk_name
        else:
            # return self._meta.pk.column #not now
            return '%s_id' % self.__class__.__name__.lower()

    def get_title(self):
        """
        A helper function to get a custom title of the instance if needed
        :return:
        """
        return self.title

    @classmethod
    def get_report_list_url(cls):
        """
        Return the url for the report list for this model
        :return: a string url
        """

        return reverse('%s:report_list' % app_settings.RA_ADMIN_SITE_NAME, args=(cls.get_class_name().lower(),))

    @classmethod
    def get_redirect_url_prefix(cls):
        """
        Get the url for the change list of this model
        :return: a string url
        """
        return reverse('%s:%s_%s_changelist' % (
            app_settings.RA_ADMIN_SITE_NAME, cls._meta.app_label, cls.get_class_name().lower()))


# class BasePersonInfo(BaseInfo):
#     address = models.CharField(_('address'), max_length=260, null=True, blank=True)
#     telephone = models.CharField(_('telephone'), max_length=130, null=True, blank=True)
#     email = models.EmailField(_('email'), null=True, blank=True)
#
#     class Meta:
#         abstract = True
#         # swappable = swapper.swappable_setting('ra', 'BasePersonInfo')


class TransactionModel(EntityModel):
    title = None

    slug = models.SlugField(_('refer code'), max_length=50, db_index=True, validators=[], blank=True)
    doc_date = models.DateTimeField(_('date'), db_index=True)
    doc_type = models.CharField(max_length=30, db_index=True)
    notes = models.TextField(_('notes'), null=True, blank=True)
    value = models.DecimalField(_('value'), max_digits=19, decimal_places=2, default=0)

    owner = models.ForeignKey(User, related_name='%(app_label)s_%(class)s_related', verbose_name=_('owner'),
                              on_delete=models.CASCADE)
    creation_date = models.DateTimeField(_('creation date and time'), default=now)
    lastmod = models.DateTimeField(_('last modification'), db_index=True)
    lastmod_user = models.ForeignKey(User, related_name='%(app_label)s_%(class)s_lastmod_related',
                                     verbose_name=_('last modification by'), on_delete=models.CASCADE)

    @classmethod
    def get_doc_type(cls):
        """
        Return the doc_type
        :return:
        """
        raise NotImplementedError(
            f'Class {cls} dont have a get_doc_type override. Each Transaction should define a *doc_type*')

    def __str__(self):
        return '%s-%s' % (self._meta.verbose_name, self.slug)

    def __repr__(self):
        return '<%s pk:%s slug:%s doc_type:%s>' % (self.__class__.__name__, self.pk, self.slug, self.doc_type)

    class Meta:
        abstract = True

    def save(self, force_insert=False, force_update=False, using=None, update_fields=None):
        """
        Custom save, it assign the user  As owner and the last modifed
        it sets the doc_type
        make sure that dlc_date has correct timezone ?!
        :param force_insert:
        :param force_update:
        :param using:
        :param update_fields:
        :return:
        """

        from ra.base.helpers import get_next_serial
        request = CrequestMiddleware.get_request()
        self.doc_type = self.get_doc_type()
        if not self.slug:
            self.slug = get_next_serial(self.__class__)

        # self.slug = slugify(self.slug)
        if not self.pk:
            if not self.lastmod_user_id:
                self.lastmod_user_id = request.user.pk
            if not self.owner_id:
                self.owner_id = self.lastmod_user_id
        self.lastmod = now()
        # if self.doc_date:
        #     if self.doc_date.tzinfo is None:
        #         self.doc_date = pytz.utc.localize(self.doc_date)

        super(EntityModel, self).save(force_insert, force_update, using, update_fields)

    #
    # @classmethod
    # def get_doc_type_verbose_name(cls, doc_type):
    #     """
    #     Return the doc_type verbose name , Must be implemented when needed by children
    #     @param doc_type: the doc_type field value
    #     @return: the description of the doc_type
    #     Example: In: get_doc_type_verbose_name('1')
    #             Out: Purchase
    #     """
    #     # Example :
    #     # if doc_type == '1': return _('purchase')
    #     raise NotImplemented()

    def get_absolute_url(self):
        doc_types = registry.get_doc_type_settings()
        if self.doc_type in doc_types:
            return '%sslug/%s/' % (doc_types[self.doc_type]['redirect_url_prefix'], self.slug)
        else:
            return self.doc_type

    @property
    def title(self):
        return self.doc_date.strftime('%Y/%m/%d %H:%M')


class TransactionItemModel(TransactionModel):
    """
    Abstract model to identify a movement with a value
    """

    class Meta:
        abstract = True


class QuantitativeTransactionItemModel(TransactionItemModel):
    quantity = models.DecimalField(_('quantity'), max_digits=19, decimal_places=2, default=0)
    price = models.DecimalField(_('price'), max_digits=19, decimal_places=2, default=0)
    discount = models.DecimalField(_('discount'), max_digits=19, decimal_places=2, default=0)

    def save(self, force_insert=False, force_update=False, using=None, update_fields=None):
        self.value = self.quantity * self.price
        if self.discount:
            self.value -= self.value * self.discount / 100
        super(QuantitativeTransactionItemModel, self).save(force_insert, force_update, using, update_fields)

    class Meta:
        abstract = True


class BaseReportModel(DiffingMixin, models.Model):
    slug = models.SlugField(_('refer code'), max_length=50, db_index=True, validators=[], blank=True)
    doc_date = models.DateTimeField(_('date'), db_index=True)
    doc_type = models.CharField(max_length=30, db_index=True)
    notes = models.TextField(_('notes'), null=True, blank=True)
    value = models.DecimalField(_('value'), max_digits=19, decimal_places=2, default=0)

    owner = models.ForeignKey(User, related_name='%(app_label)s_%(class)s_related', verbose_name=_('owner'),
                              on_delete=models.DO_NOTHING)
    creation_date = models.DateTimeField(_('creation date and time'), default=now)
    lastmod = models.DateTimeField(_('last modification'), db_index=True)
    lastmod_user = models.ForeignKey(User, related_name='%(app_label)s_%(class)s_lastmod_related',
                                     verbose_name=_('last modification by'), on_delete=models.DO_NOTHING)

    @classmethod
    def get_doc_type_plus_list(cls):
        '''

        Returns List of Identified doctype that a plus effect on the entity
        '''
        return []  # ['0','3' , 'client-cash-in','supplier-cash-in']

    @classmethod
    def get_doc_type_minus_list(cls):
        ''' Returns List of Identified doctype that a minus effect on the entity'''

        return []  # ['1', '2', 'client-cash-out', 'supplier-cash-out']

    # @classmethod
    # def get_doc_types(cls):
    #     """
    #     Return a list of the doc_types supported by the current model , Must implemented when needed by children
    #     @return:
    #     """
    #     return cls.get_doc_type_plus_list() + cls.get_doc_type_minus_list()

    class Meta:
        abstract = True


class QuanValueReport(BaseReportModel):
    quantity = models.DecimalField(_('quantity'), max_digits=19, decimal_places=2, default=0)
    price = models.DecimalField(_('price'), max_digits=19, decimal_places=2, default=0)
    discount = models.DecimalField(_('discount'), max_digits=19, decimal_places=2, default=0)

    @classmethod
    def get_doc_type_plus_list(cls):
        '''

        Returns List of Identified doctype that a plus effect on the entity
        '''
        return []  # ['0','3' , 'client-cash-in','supplier-cash-in']

    @classmethod
    def get_doc_type_minus_list(cls):
        ''' Returns List of Identified doctype that a minus effect on the entity'''

        return []  # ['1', '2', 'client-cash-out', 'supplier-cash-out']

    # @classmethod
    # def get_doc_types(cls):
    #     """
    #     Return a list of the doc_types supported by the current model , Must implemented when needed by children
    #     @return:
    #     """
    #     return cls.get_doc_type_plus_list() + cls.get_doc_type_minus_list()

    class Meta:
        abstract = True


class ProxyMovementManager(models.Manager):
    def get_queryset(self):
        return super(ProxyMovementManager, self).get_queryset().filter(doc_type=self.model.get_doc_type())


class ProxyMovement(object):
    objects = ProxyMovementManager()

    @classmethod
    def get_doc_type(cls):
        """
        Get the doc-type of the row.
        This method is called internally during save to ensure that the record in the
        proxy model always have the right doc_type
        :return: string (doc_type)
        """
        return ''

    def __init__(self, *args, **kwargs):
        super(ProxyMovement, self).__init__(*args, **kwargs)
        self._doc_type = ''

    def save(self, force_insert=False, force_update=False, using=None, update_fields=None):
        self.doc_type = self.__class__.get_doc_type()
        super(ProxyMovement, self).save(force_insert, force_update, using, update_fields)

    class Meta:
        proxy = True