package io.reactivex.lab.services.impls;

import io.netty.buffer.ByteBuf;
import io.netty.handler.codec.http.HttpResponseStatus;
import io.reactivex.lab.services.metrics.HystrixMetricsStreamHandler;
import io.reactivex.lab.services.metrics.Metrics;
import io.reactivex.netty.RxNetty;
import io.reactivex.netty.pipeline.PipelineConfigurators;
import io.reactivex.netty.protocol.http.server.HttpServer;
import io.reactivex.netty.protocol.http.server.HttpServerRequest;
import io.reactivex.netty.protocol.http.server.HttpServerResponse;
import io.reactivex.netty.protocol.http.sse.ServerSentEvent;

import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.UUID;

import rx.Observable;

import com.netflix.eureka2.client.EurekaClient;
import com.netflix.eureka2.registry.InstanceInfo;
import com.netflix.eureka2.registry.ServicePort;
import com.netflix.eureka2.registry.datacenter.BasicDataCenterInfo;

/**
 * Common base for the service impls
 */
public abstract class AbstractMiddleTierService {

    private EurekaClient client;
    private HttpServer<ByteBuf, ServerSentEvent> server;
    protected final String eurekaVipAddress;
    private final Metrics metrics;

    protected AbstractMiddleTierService(String eurekaVipAddress, EurekaClient client) {
        this.eurekaVipAddress = eurekaVipAddress;
        this.client = client;
        this.metrics = new Metrics(eurekaVipAddress);
    }

    public HttpServer<ByteBuf, ServerSentEvent> createServer(int port) {
        System.out.println("Start " + getClass().getSimpleName() + " on port: " + port);

        // declare handler chain (wrapped in Hystrix)
        // TODO create a better way of chaining these (related https://github.com/ReactiveX/RxNetty/issues/232 and https://github.com/ReactiveX/RxNetty/issues/202)
        HystrixMetricsStreamHandler<ByteBuf, ServerSentEvent> handlerChain 
          = new HystrixMetricsStreamHandler<>(metrics, "/hystrix.stream", 1000, (request, response) -> {
            try {
                long startTime = System.currentTimeMillis();
                return handleRequest(request, response)
                        .doOnCompleted(() -> System.out.println("Response => " + request.getPath() + " Time => " + (int) (System.currentTimeMillis() - startTime) + "ms"))
                        .doOnCompleted(() -> metrics.getRollingPercentile().addValue((int) (System.currentTimeMillis() - startTime)))
                        .doOnCompleted(() -> metrics.getRollingNumber().add(Metrics.EventType.SUCCESS, 1))
                        .doOnError(t -> metrics.getRollingNumber().add(Metrics.EventType.FAILURE, 1));
            } catch (Throwable e) {
                e.printStackTrace();
                System.err.println("Server => Error [" + request.getPath() + "] => " + e);
                response.setStatus(HttpResponseStatus.BAD_REQUEST);
                return response.writeStringAndFlush("data: Error 500: Bad Request\n" + e.getMessage() + "\n");
            }
        });

        return RxNetty.createHttpServer(port, (request, response) -> {
            // System.out.println("Server => Request: " + request.getPath());
                return handlerChain.handle(request, response);
            }, PipelineConfigurators.<ByteBuf> serveSseConfigurator());
    }

    public void start(int port) {
        server = createServer(port);
        server.start();
        client.register(createInstanceInfo(port)).toBlocking().lastOrDefault(null);
    }

    public void startAndWait(int port) {
        start(port);
        try {
            server.waitTillShutdown();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public void stop() {
        client.unregister(createInstanceInfo(server.getServerPort())).doOnError(Throwable::printStackTrace).subscribe();
        if (null != server) {
            try {
                server.shutdown();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    protected abstract Observable<Void> handleRequest(HttpServerRequest<?> request, HttpServerResponse<ServerSentEvent> response);

    protected InstanceInfo createInstanceInfo(int port) {
        final HashSet<ServicePort> ports = new HashSet<>(Arrays.asList(new ServicePort(port, false)));

        String hostAddress = "unknown";
        try {
            hostAddress = InetAddress.getLocalHost().getHostAddress();
        } catch (UnknownHostException e) {
            e.printStackTrace();
            hostAddress = "unknown-" + UUID.randomUUID();
        }

        return new InstanceInfo.Builder()
                .withId(hostAddress + "-" + port)
                .withApp("reactive-lab")
                .withStatus(InstanceInfo.Status.UP)
                .withVipAddress(eurekaVipAddress)
                .withPorts(ports)
                .withDataCenterInfo(BasicDataCenterInfo.fromSystemData())
                .build();
    }

    protected static Observable<Void> writeError(HttpServerRequest<?> request, HttpServerResponse<?> response, String message) {
        System.err.println("Server => Error [" + request.getPath() + "] => " + message);
        response.setStatus(HttpResponseStatus.BAD_REQUEST);
        return response.writeStringAndFlush("Error 500: " + message + "\n");
    }

    protected static int getParameter(HttpServerRequest<?> request, String key, int defaultValue) {
        List<String> v = request.getQueryParameters().get(key);
        if (v == null || v.size() != 1) {
            return defaultValue;
        } else {
            return Integer.parseInt(String.valueOf(v.get(0)));
        }
    }

    public Metrics getMetrics() {
        return metrics;
    }
}