package com.danielflower.apprunner.router.mgmt;

import org.apache.commons.io.FileUtils;
import org.json.JSONArray;
import org.json.JSONObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.servlet.http.HttpServletRequest;
import java.io.File;
import java.io.IOException;
import java.net.URI;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;

import static com.danielflower.apprunner.router.Config.dirPath;

public class Cluster {
    private static final Logger log = LoggerFactory.getLogger(Cluster.class);

    private final File config;
    private final List<Runner> runners = new CopyOnWriteArrayList<>();
    private final MapManager querier;

    private Cluster(File config, MapManager querier, List<Runner> runners) {
        this.config = config;
        this.querier = querier;
        this.runners.addAll(runners);
    }

    public static Cluster load(File config, MapManager mapManager) throws IOException {
        ArrayList<Runner> runners = new ArrayList<>();
        boolean isNew = !config.exists();
        if (config.exists()) {
            JSONObject json = new JSONObject(FileUtils.readFileToString(config));
            for (Object o : json.getJSONArray("runners")) {
                runners.add(Runner.fromJSON((JSONObject) o));
            }
        } else {
            if (!config.getParentFile().mkdirs() || !config.createNewFile()) {
                log.warn("Couldn't create " + dirPath(config) + " apparently, which is a bit worrying but maybe it's okay?");
            }
        }
        Cluster cluster = new Cluster(config, mapManager, runners);
        if (isNew) {
            cluster.save();
        }
        return cluster;
    }

    public List<Runner> getRunners() {
        return runners;
    }

    public synchronized void addRunner(HttpServletRequest clientRequest, Runner runner) throws Exception {
        if (!runners.contains(runner)) {
            runners.add(runner);
        }
        querier.loadRunner(clientRequest, runner);
        save();
    }

    public synchronized void deleteRunner(Runner runner) throws IOException {
        runners.remove(runner);
        querier.removeRunner(runner);
        save();
    }

    private void save() throws IOException {
        FileUtils.write(config, toJSON().toString(4), "UTF-8", false);
    }

    public JSONObject toJSON() {
        JSONArray all = new JSONArray();
        for (Runner runner : runners) {
            all.put(runner.toJSON());
        }
        return new JSONObject()
            .put("runners", all);
    }

    public Optional<Runner> runner(String id) {
        return runners.stream()
            .filter(runner -> runner.id.equals(id))
            .findFirst();
    }

    public Optional<Runner> allocateRunner(ConcurrentHashMap<String, URI> currentMapping) {
        Runner leastContended = null;
        for (Runner runner : runners) {
            if (!runner.hasCapacity()) {
                continue;
            }
            int num = (int)currentMapping.values().stream()
                .filter(url ->  url.getAuthority().equals(runner.url.getAuthority()))
                .count();
            runner.numberOfApps.set(num);
            if (leastContended == null || leastContended.numberOfApps.get() > num) {
                leastContended = runner;
            }
        }
        if (leastContended != null) {
            log.info("Incrementing app count for " + leastContended.id + " because apparently it is the least contended with "
                + leastContended.numberOfApps + " apps (with max capacity of " + leastContended.maxApps + "). The full mapping is: " + currentMapping);
            leastContended.numberOfApps.incrementAndGet();
            return Optional.of(leastContended);
        }
        log.info("Could not allocate a runner because it seems there is no capacity. Current mapping: " + currentMapping);
        return Optional.empty();
    }

    public Optional<Runner> getRunnerByURL(URI url) {
        for (Runner runner : runners) {
            if (runner.url.getAuthority().equals(url.getAuthority())) {
                return Optional.of(runner);
            }
        }
        return Optional.empty();
    }

}

@@ -2849,32 +2849,45 @@
   %7D%0A%0A    public 
+synchronized 
 Optional%3CRunner%3E
@@ -3046,221 +3046,110 @@
    i
-f (!runner.hasCapacity()) %7B%0A                continue;%0A            %7D%0A            int num = (int)currentMapping.values().stream()%0A                .filter(url -%3E  url.getAuthority().equals(runner.url.getAuthor
+nt num = updateNumberOfAppsForRunner(currentMapping, runner);%0A            if (!runner.hasCapac
 ity())
-)
+ %7B
 %0A   
@@ -3165,16 +3165,16 @@
     
-.
 co
-u
 nt
-()
+inue
 ;%0A  
@@ -3187,37 +3187,9 @@
     
-runner.numberOfApps.set(num);
+%7D
 %0A   
@@ -3924,32 +3924,372 @@
 empty();%0A    %7D%0A%0A
+    private static int updateNumberOfAppsForRunner(ConcurrentHashMap%3CString, URI%3E currentMapping, Runner runner) %7B%0A        int num = (int)currentMapping.values().stream()%0A            .filter(url -%3E  url.getAuthority().equals(runner.url.getAuthority()))%0A            .count();%0A        runner.numberOfApps.set(num);%0A        return num;%0A    %7D%0A%0A
     public Optio

Update the number of apps in a runner before checking if it's at capacity to prevent bug where an at-capacity runner would sometimes be allocated an app