from django.db.models.signals import pre_save, post_save, pre_delete, post_delete, m2m_changed
from django.db import models, transaction
from django.dispatch import receiver

from .models import DataAccess, ModelLog, ModelOperation, BaseModel, SideStoreModel
from authentication.models import Device

from django.contrib.auth import get_user_model
from django.utils import timezone

from .ws_sender import websocket_sender
from .registry import device_registry

User = get_user_model()

@receiver(post_save, sender=Device)
def device_created(sender, instance, **kwargs):
    if not instance.user:
        return
    evaluate_if_user_should_sync(instance.user)

@receiver(pre_delete, sender=Device)
def device_pre_delete(sender, instance, **kwargs):
    instance._user = instance.user if instance.user else None

@receiver(post_delete, sender=Device)
def device_post_delete(sender, instance, **kwargs):
    if not hasattr(instance, '_user') or not instance._user:
        return
    evaluate_if_user_should_sync(instance._user)

@receiver(pre_save)
def presave_handler(sender, instance, **kwargs):
    synchronization_prepare(sender, instance, **kwargs)

def synchronization_prepare(sender, instance, **kwargs):

    signal = kwargs.get('signal')

    # avoid crash in manage.py createsuperuser + delete user in the admin
    if isinstance(instance, User) and (instance._state.db is None or signal == pre_delete):
        return

    # some other classes are excluded in settings_app.py: SYNC_APPS
    if not isinstance(instance, BaseModel) and not isinstance(instance, User):
        return

    if signal == pre_save:
        detect_foreign_key_changes(sender, instance)

@receiver([post_save, post_delete])
def synchronization_notifications(sender, instance, created=False, **kwargs):
    """
    Signal handler that sends notifications through WebSocket channels when model instances are saved or deleted.
    The function creates a WebSocket group name for each affected user and sends a sync update message
    to all clients connected to that group.
    """

    # some classes are excluded in settings_app.py: SYNC_APPS
    if not isinstance(instance, BaseModel) and not isinstance(instance, User):
        return

    process_foreign_key_changes(sender, instance, **kwargs)

    # signal = kwargs.get('signal')
    # save_model_log_if_possible(instance, signal, created)
    # notify_impacted_users(instance)

def notify_impacted_users(instance):
    user_ids = set()
    # add impacted users
    if isinstance(instance, User):
        user_ids.add(instance.id)
    elif isinstance(instance, BaseModel):
        owner = instance.last_updated_by
        if owner:
            user_ids.add(owner.id)

    if isinstance(instance, BaseModel):
        if hasattr(instance, '_users_to_notify'):
            user_ids.update(instance._users_to_notify)
        else:
            print('no users to notify')

    device_id = device_registry.get_device_id(instance.id)

    # print(f'notify: {device_id}')
    for user_id in user_ids:
        websocket_sender.send_user_message(user_id, device_id)

    device_registry.unregister(instance.id)

def save_model_log_if_possible(instance, signal, created):

    users = related_users(instance)
    # print(f'users = {len(users)}, instance = {instance}')
    if users:
        if signal == post_save or signal == pre_save:
            if created:
                operation = ModelOperation.POST
            else:
                operation = ModelOperation.PUT
        else:
            operation = ModelOperation.DELETE

        model_name = instance.__class__.__name__
        store_id = None
        if isinstance(instance, SideStoreModel):
            store_id = instance.store_id

        if operation == ModelOperation.DELETE: # delete now unnecessary logs
            ModelLog.objects.filter(model_id=instance.id).delete()

        user_ids = [user.id for user in users]

        # print(f'users to notify: {user_ids}')
        instance._users_to_notify = user_ids # save this for the post_save signal
        save_model_log(users, operation, model_name, instance.id, store_id)

    else:
        print(f'>>> Model Log could not be created because no linked user could be found: {instance.__class__.__name__} {instance}, {signal}')

def save_model_log(users, model_operation, model_name, model_id, store_id):

    device_id = device_registry.get_device_id(model_id)

    # with transaction.atomic():
    #     for user in users:
    #         print(f'>>> create log for {user.username} : {model_operation} {model_name}')

    #         # if user.should_synchronize:
    #         model_log = ModelLog()
    #         model_log.user = user
    #         model_log.operation = model_operation
    #         model_log.model_name = model_name
    #         model_log.model_id = model_id
    #         model_log.store_id = store_id
    #         model_log.device_id = device_id
    #         model_log.save()

        # print(f'ML users = {len(users)}')
    # existing_log = ModelLog.objects.filter(users__in=users, model_id=model_id, operation=model_operation).first()
    # if existing_log:
    #     # print(f'update existing log {existing_log.users} ')
    #     existing_log.date = timezone.now()
    #     existing_log.device_id = device_id
    #     # existing_log.operation = model_operation
    #     existing_log.save()
    #     existing_log.users.set(users)
    # else:
    #     model_log = ModelLog()
    #     model_log.operation = model_operation
    #     model_log.date = timezone.now()
    #     model_log.model_name = model_name
    #     model_log.model_id = model_id
    #     model_log.store_id = store_id
    #     model_log.device_id = device_id
    #     model_log.save()
    #     model_log.users.set(users)

def detect_foreign_key_changes(sender, instance):
    if not hasattr(instance, 'pk') or not instance.pk:
        return
    if not isinstance(instance, BaseModel):
        return

    data_access_list = related_data_access(instance)
    if data_access_list:
        try:
            old_instance = sender.objects.get(pk=instance.pk)
        except sender.DoesNotExist:
            return

        # Check foreign key fields
        for field in sender._meta.get_fields():
            if isinstance(field, models.ForeignKey) and not field.related_model == User:
                # print(f'field.related_model = {field.related_model}')
                old_value = getattr(old_instance, field.name, None)
                new_value = getattr(instance, field.name, None)
                if old_value != new_value:
                    instance._fk_changes.append({
                                           'data_access_list': data_access_list,
                                           'old_value': old_value,
                                           'new_value': new_value
                                       })

def process_foreign_key_changes(sender, instance, **kwargs):

    if hasattr(instance, '_fk_changes'):
        for change in instance._fk_changes:
            for data_access in change['data_access_list']:
                if change['old_value']:
                    model_name = change['old_value'].__class__.__name__
                    save_model_log(data_access.concerned_users(), 'REVOKE_ACCESS',
                                 model_name, change['old_value'].id,
                                 change['old_value'].get_store_id())
                if change['new_value']:
                    model_name = change['new_value'].__class__.__name__
                    save_model_log(data_access.concerned_users(), 'GRANT_ACCESS',
                                 model_name, change['new_value'].id,
                                 change['new_value'].get_store_id())

@receiver(post_delete)
def delete_data_access_if_necessary(sender, instance, **kwargs):
    if not isinstance(instance, BaseModel):
        return
    if hasattr(instance, 'id'):
        DataAccess.objects.filter(model_id=instance.id).delete()

@receiver(m2m_changed, sender=DataAccess.shared_with.through)
def handle_shared_with_changes(sender, instance, action, pk_set, **kwargs):

    users = User.objects.filter(id__in=pk_set)

    if action == "post_add":
        instance.create_access_log(users, 'GRANT_ACCESS')
    elif action == "post_remove":
        instance.create_access_log(users, 'REVOKE_ACCESS')

    device_id = device_registry.get_device_id(instance.id)
    for user_id in pk_set:
        websocket_sender.send_user_message(user_id, device_id)
    device_registry.unregister(instance.id)

    for user in users:
        evaluate_if_user_should_sync(user)

@receiver(post_save, sender=DataAccess)
def data_access_post_save(sender, instance, **kwargs):
    if instance.related_user:
        evaluate_if_user_should_sync(instance.related_user)

@receiver(pre_delete, sender=DataAccess)
def revoke_access_after_delete(sender, instance, **kwargs):
    instance.create_revoke_access_log()
    instance._user = instance.related_user

@receiver(post_delete, sender=DataAccess)
def data_access_post_delete(sender, instance, **kwargs):
    if not hasattr(instance, '_user') or not instance._user:
        return
    evaluate_if_user_should_sync(instance._user)

def related_users(instance):
    users = set()
    if isinstance(instance, User):
        users.add(instance)
    elif isinstance(instance, BaseModel):
        users.add(instance.related_user)
        users.add(instance.last_updated_by)

        # look in hierarchy
        related_instances = instance.related_instances()
        # print(f'related_instances = {related_instances}')
        related_users = [ri.related_user for ri in related_instances if isinstance(ri, BaseModel)]
        users.update(related_users)

        # look in related DataAccess
        data_access_list = instances_related_data_access(instance, related_instances)
        for data_access in data_access_list:
            users.add(data_access.related_user)
            users.update(data_access.shared_with.all())

        if isinstance(instance, DataAccess):
            users.update(instance.shared_with.all())

    return {user for user in users if user is not None}

def related_data_access(instance):
    related_instances = instance.related_instances()
    return instances_related_data_access(instance, related_instances)

def instances_related_data_access(instance, related_instances):
    related_ids = [ri.id for ri in related_instances]
    related_ids.append(instance.id)
    return DataAccess.objects.filter(model_id__in=related_ids)

def evaluate_if_user_should_sync(user):
    should_synchronize = False
    if user.devices.count() > 1:
        should_synchronize = True
    elif DataAccess.objects.filter(
            models.Q(shared_with=user) |
            models.Q(related_user=user)
        ).count() > 0:
        should_synchronize = True

    print(f'should_synchronize = {should_synchronize}')

    with transaction.atomic():
        user.should_synchronize = should_synchronize
        # if we go from True to False we might want to delete ModelLog once the last device has synchronized
        user.save()
