package com.iqser.red.service.persistence.management.v1.processor.service.persistence;

import java.time.OffsetDateTime;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

import jakarta.transaction.Transactional;

import org.springframework.beans.BeanUtils;
import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Service;

import com.iqser.red.service.persistence.management.v1.processor.entity.notification.NotificationPreferencesEntity;
import com.iqser.red.service.persistence.management.v1.processor.service.persistence.repository.NotificationPreferencesRepository;
import com.iqser.red.service.persistence.management.v1.processor.service.persistence.repository.NotificationRepository;
import com.knecon.fforesight.databasetenantcommons.providers.utils.MagicConverter;
import com.iqser.red.service.persistence.service.v1.api.shared.model.notification.NotificationPreferences;
import com.iqser.red.service.persistence.service.v1.api.shared.model.notification.NotificationType;

import lombok.AccessLevel;
import lombok.RequiredArgsConstructor;
import lombok.experimental.FieldDefaults;

@Service
@RequiredArgsConstructor
@FieldDefaults(makeFinal = true, level = AccessLevel.PRIVATE)
public class NotificationPreferencesPersistenceService {

    NotificationPreferencesRepository notificationPreferencesRepository;

    NonThreadSafeNotificationPreferencesRepositoryWrapper notificationPreferencesRepositoryWrapper;

    NotificationRepository notificationRepository;


    @Transactional
    public void setNotificationPreference(String userId, NotificationPreferences newNotificationPreferences) {

        notificationPreferencesRepository.findById(userId).ifPresentOrElse(oldNotificationPreferences -> {

            // if we disabled the notification preferences just now
            if (oldNotificationPreferences.isInAppNotificationsEnabled() != newNotificationPreferences.isInAppNotificationsEnabled()) {
                notificationRepository.deleteAllByUserId(userId, OffsetDateTime.now().truncatedTo(ChronoUnit.MILLIS));
            }

            var removedNotificationTypes = new ArrayList<>(oldNotificationPreferences.getInAppNotifications());
            removedNotificationTypes.removeAll(newNotificationPreferences.getInAppNotifications());

            if (!removedNotificationTypes.isEmpty()) {
                notificationRepository.deleteAllByUserIdAndNotificationTypeIn(userId, removedNotificationTypes, OffsetDateTime.now().truncatedTo(ChronoUnit.MILLIS));
            }

            BeanUtils.copyProperties(newNotificationPreferences, oldNotificationPreferences);

        }, () -> {
            NotificationPreferencesEntity notificationPreferencesEntity = new NotificationPreferencesEntity();
            BeanUtils.copyProperties(newNotificationPreferences, notificationPreferencesEntity);
            notificationPreferencesEntity.setUserId(userId);
            notificationPreferencesRepository.save(notificationPreferencesEntity);
        });
    }


    @Transactional
    public void deleteNotificationPreferences(String userId) {

        notificationPreferencesRepository.deleteByUserId(userId);
    }


    // This method intentionally does not have a @Transactional annotation, since it needs to handle an underlying transaction exception.
    public NotificationPreferences getOrCreateNotificationPreferences(String userId) {

        try {
            // The method called here will fail if it is called concurrently (more than 1 thread), since it will always try to create
            // the desired entity.  But the exception only means, that the entity has been created by another thread.
            // In that case we can just fetch the data from the db.
            return notificationPreferencesRepositoryWrapper.getOrCreateNotificationPreferences(userId);
        } catch (DataIntegrityViolationException ex) {
            return notificationPreferencesRepositoryWrapper.getByUserId(userId);
        }
    }


    @Transactional
    public void initializePreferencesIfNotExists(String userId) {

        getOrCreateNotificationPreferences(userId);
    }


    public List<NotificationPreferencesEntity> findAll() {

        return notificationPreferencesRepository.findAll();
    }


    @Component
    @RequiredArgsConstructor
    @FieldDefaults(makeFinal = true, level = AccessLevel.PRIVATE)
    private static class NonThreadSafeNotificationPreferencesRepositoryWrapper {

        NotificationPreferencesRepository notificationPreferencesRepository;

        @Transactional(Transactional.TxType.REQUIRES_NEW)
        public NotificationPreferences getByUserId(String userId){
            return convertPreferences(notificationPreferencesRepository.getByUserId(userId));
        }



        @Transactional(Transactional.TxType.REQUIRES_NEW)
        public NotificationPreferences getOrCreateNotificationPreferences(String userId) {

            var preferences = notificationPreferencesRepository.findByUserId(userId).orElseGet(() -> {

                var notificationPreference = new NotificationPreferencesEntity();
                notificationPreference.setUserId(userId);
                notificationPreference.setEmailNotificationsEnabled(false);
                notificationPreference.setInAppNotificationsEnabled(true);
                notificationPreference.setInAppNotifications(Arrays.stream(NotificationType.values()).map(Enum::name).collect(Collectors.toList()));
                return notificationPreferencesRepository.save(notificationPreference);
            });

            return convertPreferences(preferences);

        }

        private NotificationPreferences convertPreferences(NotificationPreferencesEntity preferences){
            var result = MagicConverter.convert(preferences, NotificationPreferences.class);
            result.setInAppNotifications(new ArrayList<>(preferences.getInAppNotifications()));
            result.setEmailNotifications(new ArrayList<>(preferences.getEmailNotifications()));
            return result;
        }

    }

}
