/*-
 * ~~~~~~licensing~~~~~~
 * events-rest
 * ==========
 * Copyright (C) 2020 - 2023 EmDev LLC
 * ==========
 * You may not use this file except in accordance with the License Terms of the Copyright
 * Holder located at: https://entaxy.ru/eula . All copyrights, all intellectual property
 * rights to the Software and any copies are the property of the Copyright Holder. Unless
 * it is explicitly allowed the Copyright Holder, the User is prohibited from using the
 * Software for commercial purposes to provide services to third parties.
 * 
 * The Copyright Holder hereby declares that the Software is provided on an "AS IS".
 * Under no circumstances does the Copyright Holder guarantee or promise that the
 * Software provided by him will be suitable or not suitable for the specific purposes
 * of the User, that the Software will meet all commercial and personal subjective
 * expectations of the User, that the Software will work properly, without technical
 * errors, quickly and uninterruptedly.
 * 
 * Under no circumstances shall the Copyright Holder or its Affiliates is not liable
 * to the User for any direct or indirect losses of the User, his expenses or actual
 * damage, including, downtime; loss of bussines; lost profit; lost earnings; loss
 * or damage to data, property, etc.
 * ~~~~~~/licensing~~~~~~
 */
package ru.entaxy.esb.system.core.events.rest;

import org.hibernate.exception.ConstraintViolationException;
import org.osgi.framework.FrameworkUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import ru.entaxy.esb.system.common.osgi.OSGIUtils;
import ru.entaxy.esb.system.core.events.common.SubscriptionType;
import ru.entaxy.esb.system.core.events.jpa.EventTopicService;
import ru.entaxy.esb.system.core.events.jpa.SystemSubscriptionService;
import ru.entaxy.esb.system.core.events.jpa.entity.EventTopic;
import ru.entaxy.esb.system.core.events.jpa.entity.SystemSubscription;
import ru.entaxy.esb.system.core.events.rest.exception.*;
import ru.entaxy.esb.system.core.events.rest.response.CreateTopicResponse;
import ru.entaxy.esb.system.core.permission.common.PermissionConstants;
import ru.entaxy.esb.system.core.permission.jpa.PermissionService;
import ru.entaxy.esb.system.core.permission.jpa.entity.Permission;
import ru.entaxy.esb.system.jpa.SystemService;
import ru.entaxy.esb.system.jpa.entity.System;

import javax.persistence.NoResultException;
import javax.persistence.PersistenceException;
import javax.ws.rs.ForbiddenException;
import java.util.*;
import java.util.stream.Collectors;

public class TopicProcessor {

    private static final Logger LOG = LoggerFactory.getLogger(TopicProcessor.class);

    private SystemService systemService;
    private SystemSubscriptionService systemSubscriptionService;
    private EventTopicService eventTopicService;
    private PermissionService permissionService;

    public CreateTopicResponse registerTopic(String login, String topicName,
                                             List<String> possibleSubscribers, List<String> possiblePublishers) throws TopicAlreadyExist {
        CreateTopicResponse response = null;

        try {
            Optional<EventTopic> topicOpt = getEventTopicService().fetchByName(topicName);
            boolean restoring = false;
            EventTopic topic = null;
            if (topicOpt.isPresent()) {
                topic = topicOpt.get();
                if (topic.isDeleted()) {
                    //restore
                    restoring = true;
                    getEventTopicService().markAsDeleted(topic.getId(), false);
                } else {
                    throw new TopicAlreadyExist();
                }
            } else {
                topic = getEventTopicService().add(topicName, login);
            }

            Map<String, List<String>> subscriberErrors = registerSystemsPermission(topic, possibleSubscribers, PermissionConstants.ACTION_SUBSCRIBE);

            Map<String, List<String>> publisherErrors = registerSystemsPermission(topic, possiblePublishers, PermissionConstants.ACTION_PUBLISH);

            response = new CreateTopicResponse();
            response.setTitle(restoring ? "Topic restored" : "Topic created");
            response.setTopicName(topic.getName());
            response.setSubscriberErrors(subscriberErrors);
            response.setPublisherErrors(publisherErrors);

//			if (restoring) {
//				throw new TopicDeletedStateException();
//			}

        } catch (PersistenceException e) {
            if (e.getCause() instanceof ConstraintViolationException) {
                throw new TopicAlreadyExist(e);
            }
        }

        return response;
    }

    public CreateTopicResponse updateTopic(String login, String topicName,
                                           List<String> possibleSubscribers, List<String> possiblePublishers) throws TopicNotFound {
        CreateTopicResponse response = null;
        try {
            EventTopic topic = getEventTopicService().getByName(topicName);

            topic = getEventTopicService().update(topic.getId(), topic.getName(), login);

            Map<String, List<String>> subscriberErrors = registerSystemsPermission(
                    topic,
                    possibleSubscribers,
                    PermissionConstants.ACTION_SUBSCRIBE);

            Map<String, List<String>> publisherErrors = registerSystemsPermission(
                    topic,
                    possiblePublishers,
                    PermissionConstants.ACTION_PUBLISH);

            response = new CreateTopicResponse();
            response.setTitle(topic.isDeleted() ? "Topic restored" : "Topic updated");
            response.setTopicName(topic.getName());
            response.setSubscriberErrors(subscriberErrors);
            response.setPublisherErrors(publisherErrors);

            if (topic.isDeleted()) {
                getEventTopicService().markAsDeleted(topic.getId(), false);
            }

        } catch (NoResultException e) {
            LOG.error("Topic not found", e);
            throw new TopicNotFound(e);
        }

        return response;
    }

    private Map<String, List<String>> registerSystemsPermission(EventTopic topic, List<String> systemUuids, String action) {
        Map<String, List<String>> errorHolder = new HashMap<>();

        if (systemUuids != null && !systemUuids.isEmpty()) {
            List<String> notFoundSystems = new ArrayList<>();
            Map<Long, System> passedSystemMap = new HashMap<>();

            for (String uuid : systemUuids) {
                try {
                    System system = getSystemService().getByUuid(uuid);
                    LOG.debug("System " + uuid + " " + (system != null ? system.getName() : "null") + " for " + action);
                    passedSystemMap.put(system.getId(), system);
                } catch (NoResultException e) {
                    LOG.error(e.getMessage() + " system uuid=" + uuid);
                    notFoundSystems.add(uuid);
                }
            }

            List<Long> passedSystemIds = new ArrayList<Long>(passedSystemMap.keySet());

            List<List<String>> systemSubjectsToAdd = new ArrayList<>();
            List<Permission> permissionToDelete = new ArrayList<>();

            List<Permission> permissionsOld = getPermissionService().get(topic.getId(), PermissionConstants.TYPE_EVENT_TOPIC, action);
            if (permissionsOld != null && !permissionsOld.isEmpty()) {
                for (Permission permission : permissionsOld) {
                    if (PermissionConstants.TYPE_SYSTEM.equals(permission.getSubjectType())
                            && !passedSystemIds.contains(Long.valueOf(permission.getSubjectId()))) {
                        permissionToDelete.add(permission);
                    }
                }
                List<Long> currentSystemIdsWithPermission = permissionsOld.stream()
                        .map(p -> Long.valueOf(p.getSubjectId()))
                        .collect(Collectors.toList());

                for (long systemId : passedSystemIds) {
                    if (!currentSystemIdsWithPermission.contains(systemId)) {
                        systemSubjectsToAdd.add(Arrays.asList(String.valueOf(systemId), PermissionConstants.TYPE_SYSTEM, action));
                    }
                }

            } else {
                for (long systemId : passedSystemMap.keySet()) {
                    systemSubjectsToAdd.add(Arrays.asList(String.valueOf(systemId), PermissionConstants.TYPE_SYSTEM, action));
                }
            }

            //Add new permissions
            List<Permission> permissions = getPermissionService().addAll(topic.getId(), PermissionConstants.TYPE_EVENT_TOPIC, systemSubjectsToAdd);

            //Delete old permissions
            for (Permission permission : permissionToDelete) {
                getPermissionService().remove(permission.getId());
            }


//			List<Long> systemsWithPermission = permissions.stream()
//													.map(p ->Long.valueOf(p.getSubjectId()))
//													.collect(Collectors.toList());
//			List<String> systemsWithoutPermission = passedSystemMap.entrySet().stream()
//													.filter(e -> !systemsWithPermission.contains(e.getKey()))
//													.map(e -> e.getValue().getUuid())
//													.collect(Collectors.toList());
            if (!notFoundSystems.isEmpty()) {
                errorHolder.put("systemNotFound", notFoundSystems);
            }
//			errorHolder.put("permissionCreateSystemError", systemsWithoutPermission);
        } else {
            //if not passed remove all permission for current object action
            getPermissionService().remove(topic.getId(), PermissionConstants.TYPE_EVENT_TOPIC, action);
        }

        return errorHolder;
    }

    public void restoreTopic() {

    }

    public void deleteTopic(String topicName) throws TopicNotFound {
        EventTopic topic = null;
        try {
            topic = getEventTopicService().getByNameAndDeleted(topicName, false);
        } catch (NoResultException e) {
            LOG.error("Topic not found", e);
            throw new TopicNotFound(e);
        }

        getEventTopicService().markAsDeleted(topic.getId(), true);

    }

    public SystemSubscription getExistingSubscription(String systemUUID, String topicName) throws Exception {
        System System = null;
        try {
            System = getSystemService().getByUuid(systemUUID);
        } catch (NoResultException e) {
            LOG.error("System not found", e);
            throw new SystemNotFound(e);
        }

        EventTopic topic = getActualTopic(topicName);

        SystemSubscription systemSubscription = null;
        try {
            systemSubscription = getSystemSubscriptionService()
                    .getBySystemAndTopic(System.getId(), topic.getId());
        } catch (NoResultException e) {
            /* keep silence */
        }

        return systemSubscription;
    }

    public SystemSubscription subscribe(String systemUUID, String topicName, String subscriptionType) throws Exception {
        SubscriptionType type = null;
        try {
            type = SubscriptionType.valueOf(subscriptionType.toUpperCase());
        } catch (IllegalArgumentException e) {
            throw new UnknownSubscriptionType(e);
        }

        System system = null;
        try {
            system = getSystemService().getByUuid(systemUUID);
        } catch (NoResultException e) {
            LOG.error("System not found", e);
            throw new SystemNotFound(e);
        }

        try {
            EventTopic topic = getEventTopicService().getByNameAndDeleted(topicName, false);

            return registerConsumer(topic, system, type);
        } catch (NoResultException e) {
            LOG.error("Topic not found", e);
            throw new TopicNotFound(e);
        } catch (IllegalAccessException e) {
            LOG.error("No permission to subscribe", e);
            throw new ForbiddenException(e);
        }
    }

    public void restoreSubscription(SystemSubscription subscription) {
        getSystemSubscriptionService().markAsDeleted(subscription.getId(), false);
    }

    private SystemSubscription registerConsumer(EventTopic topic, System system, SubscriptionType type) throws Exception {
        SystemSubscription systemSubscription = null;
        try {
            systemSubscription = getSystemSubscriptionService()
                    .getBySystemAndTopic(system.getId(), topic.getId());
        } catch (NoResultException e) {
            /* keep silence */
        }
        if (systemSubscription != null) {
            if (!systemSubscription.getType().equals(type)) {
                systemSubscription.setType(type);
                getSystemSubscriptionService().update(systemSubscription);
//				throw new SubscriptionTypeModificationException();
            }
        } else {
            systemSubscription = getSystemSubscriptionService().add(system, topic,
                    type);
        }

        return systemSubscription;
    }

    public SystemSubscription unsubscribe(String systemUUID, String topicName) throws Exception {
        System system = null;
        try {
            system = getSystemService().getByUuid(systemUUID);
        } catch (NoResultException e) {
            LOG.error("System not found " + systemUUID, e);
            throw new SystemNotFound(e);
        }

        EventTopic topic = getActualTopic(topicName);

        if (system != null && topic != null) {
            try {
                SystemSubscription systemSubscription = getSystemSubscriptionService()
                        .getBySystemAndTopic(system.getId(), topic.getId());
                getSystemSubscriptionService().markAsDeleted(systemSubscription.getId(), true);

                return systemSubscription;
            } catch (NoResultException e) {
                LOG.warn("Subscription not found for system=" + systemUUID + " topic=" + topicName, e);
                throw new SubscriptionNotFound();
            }
        }

        return null;
    }

    public EventTopic getTopic(String topicName) throws TopicNotFound {
        try {
            return getEventTopicService().getByName(topicName);
        } catch (NoResultException e) {
            LOG.error("Topic not found", e);
            throw new TopicNotFound(e);
        }
    }

    public EventTopic getActualTopic(String topicName) throws TopicNotFound {
        try {
            return getEventTopicService().getByNameAndDeleted(topicName, false);
        } catch (NoResultException e) {
            LOG.error("Topic not found", e);
            throw new TopicNotFound(e);
        }
    }

    public List<SystemSubscription> getTopicSubscriptions(EventTopic topic) throws TopicNotFound {
        return getSystemSubscriptionService().getByTopic(topic.getId());
    }

    public SystemService getSystemService() {
        if (systemService == null) {
            systemService = (SystemService) OSGIUtils.getServiceReference(
                    FrameworkUtil.getBundle(TopicProcessor.class).getBundleContext(),
                    SystemService.class.getName());
        }
        return systemService;
    }

    public SystemSubscriptionService getSystemSubscriptionService() {
        if (systemSubscriptionService == null) {
            systemSubscriptionService = (SystemSubscriptionService) OSGIUtils.getServiceReference(
                    FrameworkUtil.getBundle(TopicProcessor.class).getBundleContext(),
                    SystemSubscriptionService.class.getName());
        }
        return systemSubscriptionService;
    }

    public EventTopicService getEventTopicService() {
        if (eventTopicService == null) {
            eventTopicService = (EventTopicService) OSGIUtils.getServiceReference(
                    FrameworkUtil.getBundle(TopicProcessor.class).getBundleContext(),
                    EventTopicService.class.getName());
        }
        return eventTopicService;
    }

    public PermissionService getPermissionService() {
        if (permissionService == null) {
            permissionService = (PermissionService) OSGIUtils.getServiceReference(
                    FrameworkUtil.getBundle(TopicProcessor.class).getBundleContext(),
                    PermissionService.class.getName());
        }
        return permissionService;
    }

}
