from datetime import datetime
from typing import Dict, Iterable, List, Optional, Sequence, Tuple, Type, TypedDict
from itertools import groupby
from enum import Enum
from django.contrib.contenttypes.fields import GenericForeignKey
from django.contrib.postgres.fields import ArrayField
from django.contrib.contenttypes.models import ContentType
from django.utils import timezone
from django.utils.translation import pgettext_lazy
from django.db import models
from django.contrib.postgres.indexes import GinIndex, BTreeIndex
from px_pipeline import Filter

from ..utils import Registry, make_generic_Q, to_intarray, ModelDef
from ..compat import IntegerChoices, JSONField


__all__ = (
    'Readability',
    'NotificationQuerySet', 'Notification',
    'make_generic_Q',
    'prepare_ordering',
)
# Phone keyboard:         READ0
READABILITY_BASE_NUMBER = 73230

RecipientDef = Tuple[ContentType, str]


class FlagStats(TypedDict):
    recipient: RecipientDef
    stats: Dict[int, int]


class Readability(IntegerChoices):
    UNREAD = READABILITY_BASE_NUMBER + 2, pgettext_lazy('wcd_notifications', 'Unread')
    READ = READABILITY_BASE_NUMBER + 6, pgettext_lazy('wcd_notifications', 'Read')


class FlagsRegistry(Registry):
    def add(self, choices: Type[Enum]):
        for item in choices:
            assert item.value not in self, (
                f'Value {item.value} already registered, check '
                'your flags for consistency.'
            )

            self[item.value] = item

    register = add


class NotificationQuerySet(models.QuerySet):
    def read(self):
        return self.filter(flags__contains=to_intarray([self.model.Readability.READ]))

    def unread(self):
        return self.filter(flags__contains=to_intarray([self.model.Readability.UNREAD]))

    def recipients(self, recipients: Sequence[ModelDef]):
        return self.filter(make_generic_Q('recipient', recipients))

    def actors(self, actors: Sequence[ModelDef]):
        return self.filter(make_generic_Q('actor', actors))

    def actions(self, actions: Sequence[ModelDef]):
        return self.filter(make_generic_Q('action', actions))

    def targets(self, targets: Sequence[ModelDef]):
        return self.filter(make_generic_Q('target', targets))

    def sended(self, now: Optional[datetime] = None):
        return self.filter(sended_at=now if now is not None else timezone.now())

    def collect_total_stats(self) -> Dict[RecipientDef, int]:
        records = (
            self
            .values('recipient_content_type', 'recipient_object_id')
            .order_by()
            .annotate(total=models.Count('pk'))
            .values_list('recipient_content_type', 'recipient_object_id', 'total')
        )
        return {
            (ContentType.objects.get_for_id(ct), oid): total
            for (ct, oid, total) in records
        }

    def collect_flag_stats(self) -> List[FlagStats]:
        flat_records = list(
            self
            .annotate(flag=models.Func('flags', function='unnest'))
            .values('flag', 'recipient_content_type', 'recipient_object_id')
            .order_by()
            .annotate(count=models.Count('*'))
        )
        key = lambda x: (x['recipient_content_type'], x['recipient_object_id'])
        stats_map = groupby(sorted(flat_records, key=key), key=key)

        return [
            {
                'recipient': (ContentType.objects.get_for_id(ct), oid),
                'stats': {x['flag']: x['count'] for x in items}
            }
            for (ct, oid), items in stats_map
        ]


class Notification(models.Model):
    Readability = Readability

    preparator = Filter()
    flags_registry = FlagsRegistry()
    objects = NotificationQuerySet.as_manager()

    class Meta:
        verbose_name = pgettext_lazy('wcd_notifications', 'Notification')
        verbose_name_plural = pgettext_lazy('wcd_notifications', 'Notifications')
        ordering = ('-ordering_field', '-sended_at', '-created_at', '-pk',)
        indexes = [
            GinIndex(
                name='wcdnt_%(class)s_flags',
                fields=['flags'], opclasses=('gin__int_ops',)
            ),
            BTreeIndex(fields=['recipient_content_type', 'recipient_object_id']),
            BTreeIndex(fields=['actor_content_type', 'actor_object_id']),
            BTreeIndex(fields=['action_content_type', 'action_object_id']),
            BTreeIndex(fields=['target_content_type', 'target_object_id']),
        ]

    parent = models.ForeignKey(
        'self', null=True, blank=True, on_delete=models.SET_NULL,
        verbose_name=pgettext_lazy('wcd_notifications', 'Parent notification'),
        related_name='children',
    )

    recipient_content_type = models.ForeignKey(
        ContentType,
        verbose_name=pgettext_lazy('wcd_notifications', 'Recipient: Content type'),
        related_name='notification_recipient', on_delete=models.CASCADE,
    )
    recipient_object_id = models.CharField(
        verbose_name=pgettext_lazy('wcd_notifications', 'Recipient: Id'),
        max_length=255, null=False, blank=False,
    )
    recipient = GenericForeignKey('recipient_content_type', 'recipient_object_id')

    actor_content_type = models.ForeignKey(
        ContentType, null=True, blank=True,
        verbose_name=pgettext_lazy('wcd_notifications', 'Actor: Content type'),
        related_name='notification_actor', on_delete=models.SET_NULL,
    )
    actor_object_id = models.CharField(
        verbose_name=pgettext_lazy('wcd_notifications', 'Actor: Id'),
        max_length=255, null=True, blank=True,
    )
    actor = GenericForeignKey('actor_content_type', 'actor_object_id')

    action_content_type = models.ForeignKey(
        ContentType, null=True, blank=True,
        verbose_name=pgettext_lazy('wcd_notifications', 'Action: Content type'),
        related_name='notification_action', on_delete=models.SET_NULL,
    )
    action_object_id = models.CharField(
        verbose_name=pgettext_lazy('wcd_notifications', 'Action: Id'),
        max_length=255, null=True, blank=True,
    )
    action = GenericForeignKey('action_content_type', 'action_object_id')

    target_content_type = models.ForeignKey(
        ContentType, null=True, blank=True,
        verbose_name=pgettext_lazy('wcd_notifications', 'Target: Content type'),
        related_name='notification_target', on_delete=models.SET_NULL,
    )
    target_object_id = models.CharField(
        verbose_name=pgettext_lazy('wcd_notifications', 'Target: Id'),
        max_length=255, null=True, blank=True,
    )
    target = GenericForeignKey('target_content_type', 'target_object_id')

    flags = ArrayField(
        models.PositiveIntegerField(),
        verbose_name=pgettext_lazy('wcd_notifications', 'Flags'),
        null=False, blank=True, default=list,
    )
    message = models.TextField(
        verbose_name=pgettext_lazy('wcd_notifications', 'Message'),
        null=False, blank=False,
    )

    data = JSONField(
        verbose_name=pgettext_lazy('wcd_notifications', 'Data'),
        blank=True, null=False, default=dict,
    )
    sended_at = models.DateTimeField(default=timezone.now, db_index=True)
    created_at = models.DateTimeField(default=timezone.now, db_index=True)
    ordering_field = models.BigIntegerField(db_index=True, editable=False)

    def __str__(self):
        return self.message

    def prepare(
        self,
        force_insert: bool = False,
        force_update: bool = False,
        using: Optional[str] = None,
        update_fields: Optional[Iterable[str]] = None,
    ):
        result = self.preparator({
            'instance': self,
            'force_insert': force_insert,
            'force_update': force_update,
            'using': using,
            'update_fields': update_fields,
        })
        result.setdefault('force_insert', force_insert)
        result.setdefault('force_update', force_update)
        result.setdefault('using', using)
        result.setdefault('update_fields', update_fields)

        return result

    def save(self, **kwargs) -> None:
        prepared = self.prepare(**kwargs)

        return super().save(
            force_insert=prepared['force_insert'],
            force_update=prepared['force_update'],
            using=prepared['using'],
            update_fields=prepared['update_fields'],
        )


Notification.flags_registry.add(Notification.Readability)


def _set_update_field_context(context: dict, value: str):
    if 'update_field' in context and context['update_field']:
        if not isinstance(context['update_field'], list):
            context['update_field'] = list(context['update_field'])

        context['update_field'].append(value)


@Notification.preparator.add
def prepare_ordering(context: dict):
    _set_update_field_context(context, 'ordering_field')
    instance: Notification = context['instance']
    instance.ordering_field = int(instance.sended_at.timestamp() * 1000)

    return context


@Notification.preparator.add
def set_readability_state(context: dict):
    instance: Notification = context['instance']

    if set(instance.flags) & set(x.value for x in Notification.Readability):
        return

    _set_update_field_context(context, 'flags')
    instance.flags.append(Notification.Readability.UNREAD)

    return context
