package com.ease.gsms.server.services;

import com.ease.gsms.server.model.Dispatch;
import com.ease.gsms.server.model.Message;
import com.ease.gsms.server.model.MessageStatus;
import com.ease.gsms.server.model.util.MD5Hash;
import com.ease.gsms.server.repositories.DeviceRepository;
import com.ease.gsms.server.repositories.DispatchRepository;
import com.ease.gsms.server.services.dispatch.DispatchStatusHolder;
import com.ease.gsms.server.services.dispatch.EmptyDispatchException;
import com.ease.gsms.server.services.dispatch.WorkBalancingService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.security.Principal;
import java.util.*;
import java.util.function.Predicate;
import java.util.stream.Collectors;

@Service
public class DispatchService {

    @Autowired
    DispatchRepository repository;

    @Autowired
    DeviceRepository deviceRepository;

    @Autowired
    WorkBalancingService workBalancingService;

    /**
     * Prepare sending messages belonging to a dispatch request
     *
     * @param request to dispatch
     */
    public UUID dispatch(Dispatch request) throws EmptyDispatchException {

        request.setUUID(UUID.randomUUID());

        // filter messages

        request.setMessages(
                request.getMessages().stream().filter(
                        applicablePredicates(
                                request,
                                getMessagesByHashList(request.getMessages().stream().map(Message::getHash).collect(Collectors.toList()))
                        ).stream().reduce(x -> true, Predicate::and)
                ).collect(Collectors.toList())
        );

        if (request.getMessages().isEmpty()) {

            throw new EmptyDispatchException();

        }

        UUID uuid = repository.saveRequest(request);

        workBalancingService.addDispatch(request);

        return uuid;

    }

    /**
     * Retrieves all messages where the hash matches the hash list
     *
     * @param hashList MD5 hashes in hex string format
     * @return map of hashes to list of messages for each hash
     */
    public Map<MD5Hash, List<Message>> getMessagesByHexHashList(List<String> hashList) {
        return getMessagesByHashList(hashList.stream().map(MD5Hash::fromHexString).collect(Collectors.toList()));
    }

    public Long getMessageCountsBetweenCreationTimestamps(Long fromTimestamp, Long toTimestamp, String userName) {
        return repository.getMessageCountsBetweenCreationTimestamps(fromTimestamp, toTimestamp, userName);
    }

    public List<Message> getMessagesBetweenCreationTimestamps(Long fromTimestamp, Long toTimestamp, String userName, String sortBy, Boolean descending, Integer startingRow, Integer fetchCount) {
        return repository.getMessagesBetweenCreationTimestamps(fromTimestamp, toTimestamp, userName, sortBy, descending, startingRow, fetchCount);
    }

    /**
     * Retrieves all messages where the hash matches the hash list
     *
     * @param hashList MD5 hashes
     * @return map of hashes to list of messages for each hash
     */
    public Map<MD5Hash, List<Message>> getMessagesByHashList(List<MD5Hash> hashList) {
        return repository.getMessagesByHashList(hashList);
    }

    private static Predicate<Message> any(Map<MD5Hash, List<Message>> messagesByHashList, Predicate<Message> predicate) {

        return message -> {

            List<Message> preExisting = messagesByHashList.get(message.getHash());

            if (preExisting == null || preExisting.isEmpty()) {

                return true;

            } else {

                for (Message preExistingMessage : preExisting) {

                    if (predicate.test(preExistingMessage)) {

                        return true;

                    }

                }

                return false;

            }

        };

    }

    private static Predicate<Message> all(Map<MD5Hash, List<Message>> messagesByHashList, Predicate<Message> predicate) {

        return message -> {

            List<Message> preExisting = messagesByHashList.get(message.getHash());

            if (preExisting == null || preExisting.isEmpty()) {

                return true;

            } else {

                for (Message preExistingMessage : preExisting) {

                    if (!predicate.test(preExistingMessage)) {

                        return false;

                    }

                }

                return true;

            }

        };

    }

    private static List<Predicate<Message>> applicablePredicates(Dispatch request, Map<MD5Hash, List<Message>> messagesByHashList) {
        List<Predicate<Message>> predicates = new LinkedList<>();
        if (!request.getSendToInternationalNumbers()) {
            predicates.add(message -> message.getTo().startsWith("+40"));
        }
        if (!request.getSendToNationalLandlines()) {
            predicates.add(message -> !message.getTo().startsWith("+402") && !message.getTo().startsWith("+403"));
        }
        if (!request.getSendToNationalMobiles()) {
            predicates.add(message -> !message.getTo().startsWith("+407"));
        }
        if (!request.getResendIdenticalSuccessfullyDeliveredMessages()) {
            predicates.add(
                    all(
                            messagesByHashList,
                            message -> message.getStatus() != MessageStatus.DELIVERED
                    )
            );
        }
        if (!request.getResendIdenticalSuccessfullySentMessages()) {
            predicates.add(
                    all(
                            messagesByHashList,
                            message -> {
                                switch (message.getStatus()) {
                                    case DELIVERED:
                                    case DELIVERY_FAILED:
                                    case SENT:
                                        return false;
                                    default:
                                        return true;
                                }
                            }
                    )
            );
        }
        if (!request.getResendIdenticalUnsuccessfullySentMessages()) {
            predicates.add(
                    all(
                            messagesByHashList,
                            message -> message.getStatus() != MessageStatus.SENDING_FAILED
                    )
            );
        }
        if (!request.getResendUnsentMessages()) {
            predicates.add(
                    any(
                            messagesByHashList,
                            message -> !message.isUnsent()
                    )
            );
        }
        return predicates;
    }


    public DispatchStatusHolder getDispatchStatus(UUID requestId, Principal principal) {

        // check live dispatches first

        Dispatch dispatch = workBalancingService.getLiveDispatch(requestId, principal.getName());

        if (dispatch != null) {

            return dispatch.getDispatchStatusHolder();

        } else {

            dispatch = repository.getDispatchByUuid(requestId);

            if (dispatch != null && dispatch.getSubmitter().equals(principal.getName())) {

                return new DispatchStatusHolder(
                                dispatch,
                                Collections.emptySet(),
                                false
                        );

            }

        }

        return null;

    }

    public Collection<Dispatch> getLiveDispatches(String userName) {
        return workBalancingService.getLiveDispatches(userName);
    }

}
