import collections
import datetime
from decimal import Decimal

import pytz
from dirtyfields import DirtyFieldsMixin
from django.conf import settings
from django.db import models, transaction
from django.db.models.signals import pre_save
from django.dispatch import receiver
from django.forms.models import model_to_dict
from typing import Any, Dict

try:
    from django.contrib.postgres.fields import ArrayField

    USING_POSTGRES = True
except ImportError:
    USING_POSTGRES = False
    pass
from .m2m_field_changed_actions import M2MFieldChangedActionsMixin
from .post_save_actions import PostSaveActionsMixin


class AnnounceChangesMixin(M2MFieldChangedActionsMixin, PostSaveActionsMixin):
    """
    Announces changes made to any models which include this class.
    Messages are built post-save, so times will reflect the `.save()` time,
    but they're not announced until the transaction is committed (which is
    immediately if the change is not in a transaction).
    """

    def _announce(self, message):
        redis_announce = getattr(settings, "ANNOUNCE_METHOD", None)
        sns_announce = getattr(settings, "SNS_ANNOUNCE_METHOD", None)

        if redis_announce:
            redis_announce(message)
        if sns_announce:
            sns_announce(message)

    @staticmethod
    def sanitise_for_json(value):
        if issubclass(value.__class__, Decimal):
            value = str(value)
        elif (
            issubclass(value.__class__, datetime.datetime)
            or issubclass(value.__class__, datetime.date)
            or issubclass(value.__class__, datetime.time)
        ):
            value = value.isoformat()
        elif issubclass(value.__class__, models.Model):
            value = value.pk
        elif issubclass(value.__class__, list):
            value = [AnnounceChangesMixin.sanitise_for_json(v) for v in value]
        elif USING_POSTGRES:
            if issubclass(value.__class__, ArrayField) or (
                isinstance(value, collections.Iterable) and not isinstance(value, str)
            ):
                value = [AnnounceChangesMixin.sanitise_for_json(v) for v in value]
        return value

    def _post_creation_saved(self, *args, **kwargs):
        model_to_dict_object = model_to_dict(self).items()
        sanitized_object = {
            key: self.sanitise_for_json(value) for key, value in model_to_dict_object
        }

        message = {
            "message_type": "{}_{}".format(self.__class__.__name__.lower(), "created"),
            "params": {
                "id": self.id,
                "saved_datetime": datetime.datetime.now(tz=pytz.utc).isoformat(),
                "saved_object": sanitized_object,
                "differences": {
                    key: {"before": None, "after": value}
                    for key, value in sanitized_object.items()
                },
            },
        }
        self._announce(message)

    def _post_changes_saved(self, changed_fields, *args, **kwargs):
        model_to_dict_object = model_to_dict(self).items()
        sanitized_object = {}
        differences = {}

        for field_name, value in model_to_dict_object:
            new_value = self.sanitise_for_json(value)
            sanitized_object[field_name] = new_value

            if field_name in changed_fields.keys():
                differences[field_name] = {
                    "before": self.sanitise_for_json(changed_fields[field_name]),
                    "after": new_value,
                }

        message = {
            "message_type": "_".join([self.__class__.__name__.lower(), "updated"]),
            "params": {
                "saved_datetime": datetime.datetime.now(tz=pytz.utc).isoformat(),
                "saved_object": sanitized_object,
                "differences": differences,
                "id": self.id,
            },
        }
        self._announce(message)

    def _post_no_changes_saved(self, *args, **kwargs):
        pass

    def _post_m2m_added(self, item_id, m2m_model_name, **kwargs):
        instance_model_name = m2m_model_name.lower()
        item_model_name = self.__class__.__name__.lower()

        model_names = sorted([item_model_name, instance_model_name])
        message = {
            "message_type": "{}_m2m_{}_updated".format(*model_names),
            "params": {
                "{}_id".format(item_model_name): self.id,
                "{}_id".format(instance_model_name): item_id,
                "operation_type": "add",
            },
        }
        transaction.on_commit(lambda: settings.ANNOUNCE_METHOD(message))

    def _post_m2m_removed(self, item_id, m2m_model_name, **kwargs):
        instance_model_name = m2m_model_name.lower()
        item_model_name = self.__class__.__name__.lower()
        model_names = sorted([item_model_name, instance_model_name])
        message = {
            "message_type": "{}_m2m_{}_updated".format(*model_names),
            "params": {
                "{}_id".format(item_model_name): self.id,
                "{}_id".format(instance_model_name): item_id,
                "operation_type": "remove",
            },
        }
        transaction.on_commit(lambda: settings.ANNOUNCE_METHOD(message))
