package de.bornemisza.users.da;

import java.util.Collection;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.logging.Logger;
import java.util.stream.Collectors;

import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import javax.ejb.ScheduleExpression;
import javax.ejb.Singleton;
import javax.ejb.Startup;
import javax.ejb.Timeout;
import javax.ejb.Timer;
import javax.ejb.TimerConfig;
import javax.ejb.TimerService;
import javax.inject.Inject;

import com.hazelcast.core.HazelcastInstance;
import com.hazelcast.core.Member;
import com.hazelcast.core.MemberAttributeEvent;
import com.hazelcast.core.MembershipEvent;
import com.hazelcast.core.MembershipListener;

import de.bornemisza.users.HealthChecks;
import de.bornemisza.loadbalancer.da.Pool;

@Singleton
@Startup
public class PoolMaintenance {

    @Resource
    private TimerService timerService;

    private static final String TIMER_NAME = "PoolMaintenanceTimer";

    @Inject
    HazelcastInstance hazelcast;

    @Inject
    HealthChecks healthChecks;

    public static String LIST_COUCHDB_HOSTQUEUE = Pool.LIST_COUCHDB_HOSTQUEUE;
    public static String MAP_COUCHDB_UTILISATION = Pool.MAP_COUCHDB_UTILISATION;

    private Map<String, Integer> couchDbHostUtilisation;
    private List<String> couchDbHostQueue;

    public PoolMaintenance() {
    }

    // Constructor for Unit Tests
    PoolMaintenance(HazelcastInstance hz) {
        this.hazelcast = hz;
    }

    // Constructor for Unit Tests
    PoolMaintenance(Map<String, Integer> utilisationMap) {
        this.couchDbHostUtilisation = utilisationMap;
    }

    // Constructor for Unit Tests
    PoolMaintenance(List<String> hostQueue, HealthChecks checks) {
        this.couchDbHostQueue = hostQueue;
        this.healthChecks = checks;
    }

    @PostConstruct
    public void init() {
        couchDbHostUtilisation = hazelcast.getMap(MAP_COUCHDB_UTILISATION);
        couchDbHostQueue = hazelcast.getList(LIST_COUCHDB_HOSTQUEUE);
        hazelcast.getCluster().addMembershipListener(new MembershipListener() {
            @Override public void memberAdded(MembershipEvent me) { rebuildTimer(); }
            @Override public void memberRemoved(MembershipEvent me) { rebuildTimer(); }
            @Override public void memberAttributeChanged(MemberAttributeEvent mae) { }
        });
        rebuildTimer();
    }

    void rebuildTimer() {
        Collection<Timer> timers = timerService.getAllTimers();
        for (Timer timer: timers) {
            if (timer.getInfo().equals(TIMER_NAME)) {
                timer.cancel();
            }
        }
        ScheduleExpression expression = new ScheduleExpression();
        expression.hour("*").minute(calculateMinuteExpression());
        TimerConfig timerConfig = new TimerConfig();
        timerConfig.setPersistent(false);
        timerConfig.setInfo(TIMER_NAME);
        timerService.createCalendarTimer(expression, timerConfig);
        Logger.getAnonymousLogger().info("Installed Timer with " + expression.toString());
    }

    String calculateMinuteExpression() {
        Set<Member> members = hazelcast.getCluster().getMembers();
        List<String> sortedUuids = members.stream()
                .map(member -> member.getUuid())
                .sorted(Comparator.<String>naturalOrder())
                .collect(Collectors.toList());
        String myself = hazelcast.getCluster().getLocalMember().getUuid();
        return sortedUuids.indexOf(myself) + "/" + members.size();
    }

    @Timeout
    public void checkPool() {
        List<String> sortedHostnames = sortHostnamesByUtilisation();

        updateQueue(sortedHostnames);

        logNewQueueState();
    }

    List<String> sortHostnamesByUtilisation() {
        return this.couchDbHostUtilisation.entrySet().stream()
                .sorted(Map.Entry.<String, Integer>comparingByValue())
                .map(entry -> entry.getKey())
                .collect(Collectors.toList());
    }

    void updateQueue(List<String> sortedHostnames) {
        for (String hostname : sortedHostnames) {
            if (healthChecks.isHostAvailable(hostname, 443)) {
                if (couchDbHostQueue.contains(hostname)) {
                    couchDbHostQueue.add(hostname);
                    couchDbHostQueue.remove(hostname); // first occurrence, i. e. previous position in list
                }
                else {
                    couchDbHostQueue.add(hostname);
                }
            }
            else {
                couchDbHostQueue.remove(hostname); // stale
                
            }
        }
    }

    private void logNewQueueState() {
        StringBuilder sb = new StringBuilder("CouchDbHostQueue");
        for (String hostname : couchDbHostQueue) {
            sb.append(" | ").append(hostname).append(":").append(couchDbHostUtilisation.get(hostname));
        }
        Logger.getAnonymousLogger().info(sb.toString());
    }

}

@@ -290,24 +290,69 @@
 n.Resource;%0A
+import javax.ejb.NoSuchObjectLocalException;%0A
 import javax
@@ -812,26 +812,28 @@
 sza.
-users.HealthChecks
+loadbalancer.da.Pool
 ;%0Aim
@@ -851,36 +851,34 @@
 nemisza.
-loadbalancer.da.Pool
+users.HealthChecks
 ;%0A%0A@Sing
@@ -2589,24 +2589,46 @@
 : timers) %7B%0A
+            try %7B%0A    
             
@@ -2681,24 +2681,28 @@
             
+    
 timer.cancel
@@ -2709,32 +2709,36 @@
 ();%0A            
+  
+  
 %7D%0A        %7D%0A    
@@ -2723,34 +2723,221 @@
       %7D%0A        
+    %7D%0A            catch (IllegalStateException %7C NoSuchObjectLocalException e) %7B%0A                Logger.getAnonymousLogger().warning(%22Timer inaccessible: %22 + timer);%0A            %7D%0A        
 %7D
-%0A
         Schedule

handle exceptions on timer failure