package hm.binkley.vertx.health;

import hm.binkley.vertx.health.HealthChecker.HealthCheck;
import io.vertx.core.AbstractVerticle;
import io.vertx.core.Verticle;
import io.vertx.core.json.JsonObject;
import io.vertx.ext.web.Router;
import io.vertx.ext.web.RoutingContext;
import org.kohsuke.MetaInfServices;

import javax.inject.Inject;
import javax.inject.Singleton;
import java.net.UnknownHostException;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.function.BinaryOperator;
import java.util.stream.Collectors;

import static hm.binkley.vertx.health.HealthStatus.ALIVE;
import static io.vertx.core.http.HttpHeaders.CACHE_CONTROL;
import static io.vertx.core.http.HttpHeaders.CONTENT_TYPE;
import static io.vertx.core.http.HttpMethod.GET;
import static java.lang.String.format;
import static java.net.InetAddress.getLocalHost;
import static java.util.Arrays.asList;
import static java.util.stream.Collectors.joining;
import static java.util.stream.Collectors.toMap;

/**
 * {@code HealthVerticle} <strong>needs documentation</strong>.
 *
 * @author <a href="mailto:binkley@alumni.rice.edu">B. K. Oxley (binkley)</a>
 * @todo Duplication in text/json health methods; avoid overdoing it
 */
@MetaInfServices(Verticle.class)
@Singleton
public final class HealthVerticle
        extends AbstractVerticle {
    /** Package scope for testing. */
    @Inject
    Router router;

    private final List<HealthChecker> checks = new CopyOnWriteArrayList<>();

    public void add(final HealthChecker check) {
        checks.add(check);
    }

    @Override
    public void start()
            throws Exception {
        router.route(GET, "/health").
                produces("text/plain").
                handler(this::healthBrowser);
        router.route(GET, "/health").
                produces("application/json").
                handler(this::healthRest);
    }

    private void healthBrowser(final RoutingContext context) {
        final Map<String, Object> headers = headers(context);

        final HealthStatus[] alive = {ALIVE};
        final String body = checks.stream().
                map(HealthChecker::get).
                peek(c -> alive[0] = alive[0].with(c.getStatus())).
                map(c -> format("%c %s %s - %s - %s", c.getStatus().mark(),
                        c.getStatus(), c.getDisplay(), c.getResource(),
                        c.getMessage())).
                collect(joining("\n"));
        headers.put("status", alive[0]);

        // Build headers AFTER body so dep health checks can change status
        final String header = headers.entrySet().stream().
                map(e -> format("%s: %s", e.getKey().toUpperCase(),
                        e.getValue())).
                collect(joining("\n"));

        context.request().response().
                putHeader(CONTENT_TYPE, context.getAcceptableContentType()).
                putHeader(CACHE_CONTROL, "no-cache").
                end(format("%s\n\n%s\n", header, body));
    }

    private void healthRest(final RoutingContext context) {
        final HealthStatus[] alive = {ALIVE};
        final Map<String, Object> body = checks.stream().
                map(HealthChecker::get).
                peek(c -> alive[0] = alive[0].with(c.getStatus())).
                collect(toMap(HealthCheck::getDisplay,
                        c -> mapOf("name", c.getDisplay(), "status",
                                c.getStatus(), "resource", c.getResource(),
                                "message", c.getMessage()), throwingMerger(),
                        () -> headers(context)));
        body.put("status", alive[0]);

        context.request().response().
                putHeader(CONTENT_TYPE, context.getAcceptableContentType()).
                putHeader(CACHE_CONTROL, "no-cache").
                end(new JsonObject(body).
                        encode());
    }

    private static Map<String, Object> headers(final RoutingContext context) {
        return mapOf("name", "???", "status", ALIVE, "resource",
                context.request().absoluteURI(), "hostname", hostname());
    }

    /** @todo Utility code - drops null-valued keys */
    @SuppressWarnings("unchecked")
    @SafeVarargs
    private static <R, K extends R, V extends R> Map<K, V> mapOf(
            final R... pairs) {
        final List<R> x = asList(pairs);
        final Iterator<R> it = x.iterator();
        final Map<K, V> map = new LinkedHashMap<>();
        while (it.hasNext()) {
            final K key = (K) it.next();
            if (!it.hasNext())
                throw new IllegalArgumentException("Key with no value");
            final V value = (V) it.next();
            if (null != value)
                map.put(key, value);
        }
        return map;
    }

    /** Stolen from {@link Collectors#throwingMerger}. */
    private static <T> BinaryOperator<T> throwingMerger() {
        return (u, v) -> {
            throw new IllegalStateException(format("Duplicate key %s", u));
        };
    }

    private static String hostname() {
        try {
            return getLocalHost().getHostName();
        } catch (final UnknownHostException e) {
            throw new AssertionError(e);
        }
    }
}

@@ -556,86 +556,8 @@
 ist;
-%0Aimport java.util.function.BinaryOperator;%0Aimport java.util.stream.Collectors;
 %0A%0Aim
@@ -2031,54 +2031,8 @@
 );%0A%0A
-        final HealthStatus%5B%5D alive = %7BALIVE%7D;%0A
     
@@ -2143,45 +2143,121 @@
  -%3E 
-alive%5B0%5D = alive%5B0%5D.with(c.get
+headers.merge(%22status%22, c.getStatus(),%0A                        (a, b) -%3E ((HealthStatus) a).with((Health
 Status
-(
+) b
 ))).
@@ -2490,49 +2490,8 @@
 %22));
-%0A        headers.put(%22status%22, alive%5B0%5D);
 %0A%0A  
@@ -3082,40 +3082,54 @@
 nal 
-HealthStatus%5B%5D alive = %7BALIVE%7D
+Map%3CString, Object%3E json = headers(context)
 ;%0A
+%0A
     
@@ -3253,45 +3253,118 @@
  -%3E 
-alive%5B0%5D = alive%5B0%5D.with(c.get
+json.merge(%22status%22, c.getStatus(),%0A                        (a, b) -%3E ((HealthStatus) a).with((Health
 Status
-(
+) b
 ))).
@@ -3626,86 +3626,21 @@
 e())
-, throwingMerger(),%0A                        () -%3E headers(context)
 ));%0A
+%0A
         
 body
@@ -3639,35 +3639,24 @@
     
-body.put(%22status%22, alive%5B0%5D
+json.putAll(body
 );%0A%0A
@@ -3859,20 +3859,20 @@
 nObject(
-body
+json
 ).%0A     
@@ -4770,24 +4770,24 @@
 ;%0A        %7D%0A
+
         retu
@@ -4805,247 +4805,8 @@
  %7D%0A%0A
-    /** Stolen from %7B@link Collectors#throwingMerger%7D. */%0A    private static %3CT%3E BinaryOperator%3CT%3E throwingMerger() %7B%0A        return (u, v) -%3E %7B%0A            throw new IllegalStateException(format(%22Duplicate key %25s%22, u));%0A        %7D;%0A    %7D%0A%0A
     

Use merge, not array hack.