package com.ease.gsms.server.services.dispatch;

import com.ease.gsms.server.model.Dispatch;
import com.ease.gsms.server.model.DispatchStatus;
import com.ease.gsms.server.model.MessageStatus;
import com.ease.gsms.server.repositories.DispatchRepository;
import com.ease.gsms.server.services.DispatchUpdateObserver;
import org.slf4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;

import javax.annotation.PostConstruct;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors;

@Service
public class WorkBalancingService {

    @Autowired
    DispatchRepository dispatchRepository;

    @Autowired
    WorkAllocator allocator;

    @Autowired
    Logger logger;

    private Map<String, MessageSender> workers; // indexed by sim serial number

    private Map<String, Map<Long, Dispatch>> work; // indexed by user

    private Map<UUID, Long> dispatchIdByUuidMap;

    private Set<DispatchUpdateObserver> dispatchUpdateObservers;

    @PostConstruct
    private void init() {
        workers = new ConcurrentHashMap<>();
        work = new ConcurrentHashMap<>();
        dispatchIdByUuidMap = new ConcurrentHashMap<>();
        dispatchUpdateObservers = ConcurrentHashMap.newKeySet();
        List<Dispatch> activeDispatches = dispatchRepository.getActiveDispatches();
        for (Dispatch dispatch : activeDispatches) {
            addDispatch(dispatch, false);
        }
        allocator.setWorkerContainer(workers);
        allocator.setWorkContainer(work);
        pruneDispatches();
        allocator.doAllocations(activeDispatches.stream().map(Dispatch::getSubmitter).collect(Collectors.toSet()));
    }

    public boolean addDispatchUpdateObserver(DispatchUpdateObserver observer) {
        return dispatchUpdateObservers.add(observer);
    }

    public boolean removeDispatchUpdateObserver(DispatchUpdateObserver observer) {
        return dispatchUpdateObservers.remove(observer);
    }

    @Scheduled(cron = "0 0 * ? * ?", zone = "Etc/UTC") // run every hour
    private void scheduledReallocation() {

        allocator.doAllocations(work.values().stream().flatMap(longDispatchMap -> longDispatchMap.values().stream()).map(Dispatch::getSubmitter).collect(Collectors.toSet()));

    }

    @Scheduled(cron = "0 0 0 ? * ?", zone = "Etc/UTC") // run every day
    private void pruneDispatches() {

        List<Dispatch> toPrune = new LinkedList<>();

        for (Map<Long, Dispatch> userActiveDispatches : work.values()) {

            for (Dispatch dispatch : userActiveDispatches.values()) {

                boolean shouldPrune = dispatch.getStatus() == DispatchStatus.COMPLETED || dispatch.getStatus() == DispatchStatus.CANCELLED;

                if (shouldPrune) {
                    continue;
                }

                if (dispatch.getMessages().isEmpty()) {

                    shouldPrune = true;

                } else {

                    DispatchStatusHolder holder = new DispatchStatusHolder(dispatch, Collections.emptySet(), false);

                    Map<MessageStatus, Integer> messagesCountsByStatus = holder.getMessagesCountsByStatus();

                    if (messagesCountsByStatus.getOrDefault(MessageStatus.CREATED, 0) + messagesCountsByStatus.getOrDefault(MessageStatus.ALLOCATED, 0) + messagesCountsByStatus.getOrDefault(MessageStatus.RESERVED, 0) == 0) {

                        shouldPrune = true;

                    }

                }

                if (shouldPrune) {

                    dispatchRepository.updateDispatchStatus(dispatch.getId(), new Date(), DispatchStatus.COMPLETED.getId());

                    toPrune.add(dispatch);

                }

            }

        }

        for (Dispatch dispatch : toPrune) {

            removeDispatch(dispatch, false);

        }

    }

    public void addDispatch(Dispatch dispatch) {

        addDispatch(dispatch, true);

    }

    public void addDispatch(Dispatch dispatch, boolean triggerAllocations) {

        dispatchIdByUuidMap.put(dispatch.getUUID(), dispatch.getId());

        work.computeIfAbsent(dispatch.getSubmitter(), s -> new ConcurrentHashMap<>()).put(dispatch.getId(), dispatch);

        if (dispatch.getDispatchStatusHolder() == null) {

            dispatch.setDispatchStatusHolder(
                    new DispatchStatusHolder(
                            dispatch,
                            Collections.singleton(
                                    (userName, dispatchStatusHolder) -> {
                                        int totalMessageCount = dispatchStatusHolder.getTotalMessageCount();
                                        Map<MessageStatus, Integer> messagesCountsByStatus = dispatchStatusHolder.getMessagesCountsByStatus();
                                        DispatchStatus originalStatus = dispatch.getStatus();
                                        int sent = messagesCountsByStatus.getOrDefault(MessageStatus.SENT, 0);
                                        int sentFailed = messagesCountsByStatus.getOrDefault(MessageStatus.SENDING_FAILED, 0);
                                        int deliveryFailed = messagesCountsByStatus.getOrDefault(MessageStatus.DELIVERY_FAILED, 0);
                                        int delivered = messagesCountsByStatus.getOrDefault(MessageStatus.DELIVERED, 0);
                                        int sentSum = sent + sentFailed + deliveryFailed + delivered;
                                        switch (dispatchStatusHolder.getStatus()) {
                                            case REQUESTED:
                                                if (sentSum > 0) {
                                                    dispatch.setStatus(DispatchStatus.IN_PROGRESS);
                                                }
                                                // break; // vrosca: we don't break, might go straight from in_progress to completed
                                            case IN_PROGRESS:
                                                if (totalMessageCount == sentSum) { // we're done
                                                    dispatch.setStatus(DispatchStatus.COMPLETED);
                                                }
                                                break;
                                        }
                                        if (dispatch.getStatus() != originalStatus) {
                                            dispatchRepository.updateDispatchStatus(dispatch.getId(), new Date(), dispatch.getStatus().getId());
                                        }
                                        dispatchUpdateObservers.forEach(observer -> observer.updated(dispatch.getSubmitter(), dispatchStatusHolder));
                                    }
                            ),
                            true
                    )
            );

        }

        if (triggerAllocations) {

            allocator.doAllocations(Collections.singleton(dispatch.getSubmitter()));

        }

    }

    public void removeDispatch(Dispatch dispatch) {

        removeDispatch(dispatch, true);

    }

    public void removeDispatch(Dispatch dispatch, boolean triggerAllocations) {

        dispatchIdByUuidMap.remove(dispatch.getUUID());

        work.computeIfAbsent(dispatch.getSubmitter(), s -> new ConcurrentHashMap<>()).remove(dispatch.getId());

        if (triggerAllocations) {

            allocator.doAllocations(Collections.singleton(dispatch.getSubmitter()));

        }

    }

    public MessageSender getWorker(String id) {

        return workers.get(id);

    }

    public MessageSender getWorker(String id, String userName) {

        MessageSender worker = getWorker(id);

        if (worker != null && worker.getUser().equals(userName)) {

            return worker;

        } else {

            return null;

        }

    }

    public void registerWorker(MessageSender worker) {

        worker.setRepository(dispatchRepository);

        worker.setAllocator(allocator);

        workers.put(worker.getId(), worker);

        allocator.doAllocations(Collections.singleton(worker.getUser()));


    }

    public boolean unregisterWorker(String id) {

        return unregisterWorkers(Collections.singleton(id));

    }

    public boolean unregisterWorkers(Set<String> ids) {

        boolean changed = false;

        String user = null;

        for (String id : ids) {

            MessageSender worker = workers.remove(id);

            if (worker != null) {

                user = worker.getUser();

                changed = true;

            }

        }

        if (changed) {

            allocator.doAllocations(Collections.singleton(user));

        }

        return changed;

    }

    public Dispatch getLiveDispatch(UUID requestId, String userName) {
        Long dispatchId = dispatchIdByUuidMap.get(requestId);
        if (dispatchId == null) {
            return null;
        }
        return work.get(userName).get(dispatchId);
    }

    public Collection<Dispatch> getLiveDispatches(String userName) {
        Map<Long, Dispatch> dispatchMapForUser = work.get(userName);
        return dispatchMapForUser != null ? dispatchMapForUser.values() : Collections.emptySet();
    }

    public Dispatch pauseDispatch(Dispatch dispatch) {
        switch (dispatch.getStatus()) {
            case REQUESTED:
            case IN_PROGRESS:
                dispatch.setStatus(DispatchStatus.PAUSED);
                dispatchRepository.updateDispatchStatus(dispatch.getId(), new Date(), DispatchStatus.PAUSED.getId());
                dispatch.getDispatchStatusHolder().notifyObservers();
                allocator.doAllocations(Collections.singleton(dispatch.getSubmitter()));

        }
        return dispatch;
    }

    public Dispatch resumeDispatch(Dispatch dispatch) {
        switch (dispatch.getStatus()) {
            case PAUSED:
            case SCHEDULED_PAUSE:
                dispatch.setStatus(DispatchStatus.IN_PROGRESS);
                dispatchRepository.updateDispatchStatus(dispatch.getId(), new Date(), DispatchStatus.IN_PROGRESS.getId());
                dispatch.getDispatchStatusHolder().notifyObservers();
                allocator.doAllocations(Collections.singleton(dispatch.getSubmitter()));
        }
        return dispatch;
    }

    public Dispatch cancel(Dispatch dispatch) {
        switch (dispatch.getStatus()) {
            case REQUESTED:
            case IN_PROGRESS:
            case PAUSED:
            case SCHEDULED_PAUSE:
                dispatch.setStatus(DispatchStatus.CANCELLED);
                dispatchRepository.updateDispatchStatus(dispatch.getId(), new Date(), DispatchStatus.CANCELLED.getId());
                dispatch.getDispatchStatusHolder().notifyObservers();
                allocator.doAllocations(Collections.singleton(dispatch.getSubmitter()));
        }
        return dispatch;
    }

}
