package org.xbib.elasticsearch.helper.client.http;

import org.elasticsearch.action.Action;
import org.elasticsearch.action.ActionListener;
import org.elasticsearch.action.ActionRequest;
import org.elasticsearch.action.ActionRequestBuilder;
import org.elasticsearch.action.ActionResponse;
import org.elasticsearch.action.GenericAction;
import org.elasticsearch.action.admin.cluster.settings.ClusterUpdateSettingsAction;
import org.elasticsearch.action.admin.cluster.settings.HttpClusterUpdateSettingsAction;
import org.elasticsearch.action.admin.indices.create.CreateIndexAction;
import org.elasticsearch.action.admin.indices.create.HttpCreateIndexAction;
import org.elasticsearch.action.admin.indices.refresh.HttpRefreshIndexAction;
import org.elasticsearch.action.admin.indices.refresh.RefreshAction;
import org.elasticsearch.action.admin.indices.settings.put.HttpUpdateSettingsAction;
import org.elasticsearch.action.admin.indices.settings.put.UpdateSettingsAction;
import org.elasticsearch.action.bulk.BulkAction;
import org.elasticsearch.action.bulk.HttpBulkAction;
import org.elasticsearch.action.search.HttpSearchAction;
import org.elasticsearch.action.search.SearchAction;
import org.elasticsearch.client.support.AbstractClient;
import org.elasticsearch.client.support.Headers;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.threadpool.ThreadPool;
import org.jboss.netty.bootstrap.ClientBootstrap;
import org.jboss.netty.channel.Channel;
import org.jboss.netty.channel.ChannelFuture;
import org.jboss.netty.channel.ChannelHandlerContext;
import org.jboss.netty.channel.ChannelPipeline;
import org.jboss.netty.channel.ChannelPipelineFactory;
import org.jboss.netty.channel.Channels;
import org.jboss.netty.channel.ExceptionEvent;
import org.jboss.netty.channel.MessageEvent;
import org.jboss.netty.channel.SimpleChannelUpstreamHandler;
import org.jboss.netty.channel.socket.nio.NioClientSocketChannelFactory;
import org.jboss.netty.handler.codec.http.HttpChunkAggregator;
import org.jboss.netty.handler.codec.http.HttpClientCodec;
import org.jboss.netty.handler.codec.http.HttpContentDecompressor;
import org.jboss.netty.handler.codec.http.HttpResponse;
import org.xbib.elasticsearch.helper.client.Future;
import org.xbib.elasticsearch.helper.client.RemoteInvoker;

import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.InetSocketAddress;
import java.net.URL;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.Map;
import java.util.concurrent.Executors;

public class HttpInvoker extends AbstractClient implements RemoteInvoker {

    private final Map<String, HttpElasticsearchClient.ActionEntry> actionMap = new HashMap();

    private final Map<Channel, HttpInvocationContext> contexts;

    private ClientBootstrap bootstrap;

    private URL url;

    static class ActionEntry<Request extends ActionRequest, Response extends ActionResponse> {
        public final GenericAction<Request, Response> action;
        public final HttpAction<Request, Response> httpAction;

        ActionEntry(GenericAction<Request, Response> action, HttpAction<Request, Response> httpAction) {
            this.action = action;
            this.httpAction = httpAction;
        }
    }

    public HttpInvoker(Settings settings, ThreadPool threadPool, Headers headers, URL url) {
        super(settings, threadPool, headers);
        this.contexts = new HashMap<>();
        this.bootstrap = new ClientBootstrap(new NioClientSocketChannelFactory(
                Executors.newCachedThreadPool(),
                Executors.newCachedThreadPool()));
        bootstrap.setPipelineFactory(new HttpInvoker.HttpClientPipelineFactory());
        bootstrap.setOption("tcpNoDelay", true);

        registerAction(BulkAction.INSTANCE, HttpBulkAction.class);
        registerAction(CreateIndexAction.INSTANCE, HttpCreateIndexAction.class);
        registerAction(RefreshAction.INSTANCE, HttpRefreshIndexAction.class);
        registerAction(ClusterUpdateSettingsAction.INSTANCE, HttpClusterUpdateSettingsAction.class);
        registerAction(UpdateSettingsAction.INSTANCE, HttpUpdateSettingsAction.class);
        registerAction(SearchAction.INSTANCE, HttpSearchAction.class);

        this.url = url;
    }

    @Override
    public <T> Future<T> invoke(Settings settings, Method method, Object... args) throws Exception {
        return null;
    }
    @Override
    public void close() {
        bootstrap.releaseExternalResources();
    }

    @SuppressWarnings("unchecked")
    @Override
    public <Request extends ActionRequest, Response extends ActionResponse, RequestBuilder extends ActionRequestBuilder<Request, Response, RequestBuilder>>
            void doExecute(Action<Request, Response, RequestBuilder> action, Request request, ActionListener<Response> listener) {
        HttpElasticsearchClient.ActionEntry entry = actionMap.get(action.name());
        if (entry == null) {
            throw new IllegalStateException("no action entry for " + action.name());
        }
        HttpAction<Request, Response> httpAction = entry.httpAction;
        if (httpAction == null) {
            throw new IllegalStateException("failed to find action [" + action + "] to execute");
        }
        HttpInvocationContext<Request, Response> httpInvocationContext = new HttpInvocationContext(httpAction, listener, new LinkedList<>(), request);
        try {
            httpInvocationContext.httpRequest = httpAction.createHttpRequest(this.url, request);
        } catch (IOException e) {
            logger.error(e.getMessage(), e);
        }
        ChannelFuture future = bootstrap.connect(new InetSocketAddress(url.getHost(), url.getPort()));
        future.awaitUninterruptibly();
        if (!future.isSuccess()) {
            bootstrap.releaseExternalResources();
            logger.error("can't connect to {}", url);
        } else {
            Channel channel = future.getChannel();
            httpInvocationContext.setChannel(channel);
            contexts.put(channel, httpInvocationContext);
            channel.getConfig().setConnectTimeoutMillis(settings.getAsInt("http.client.timeout", 5000));
            httpAction.execute(httpInvocationContext, listener);
        }
    }

    @SuppressWarnings("unchecked")
    public <Request extends ActionRequest, Response extends ActionResponse> void registerAction(GenericAction<Request, Response> action, Class<? extends HttpAction<Request, Response>> httpAction) {
        try {
            HttpAction<Request, Response> instance = httpAction.getDeclaredConstructor(Settings.class).newInstance(settings);
            actionMap.put(action.name(), new HttpElasticsearchClient.ActionEntry<>(action, instance));
        } catch (NoSuchMethodException | InvocationTargetException | InstantiationException | IllegalAccessException e ) {
            logger.error(e.getMessage(), e);
        }
    }

    private class HttpClientPipelineFactory implements ChannelPipelineFactory {

        public ChannelPipeline getPipeline() throws Exception {
            ChannelPipeline pipeline = Channels.pipeline();
            pipeline.addLast("codec", new HttpClientCodec());
            pipeline.addLast("aggregator", new HttpChunkAggregator(settings.getAsInt("http.client.maxchunksize", 10 * 1024 * 1024)));
            pipeline.addLast("inflater", new HttpContentDecompressor());
            pipeline.addLast("handler", new HttpInvoker.HttpResponseHandler());
            return pipeline;
        }
    }

    private class HttpResponseHandler<Request extends ActionRequest, Response extends ActionResponse> extends SimpleChannelUpstreamHandler {

        @SuppressWarnings("unchecked")
        @Override
        public void messageReceived(ChannelHandlerContext ctx, MessageEvent e) throws Exception {
            HttpInvocationContext<Request, Response> httpInvocationContext = contexts.get(ctx.getChannel());
            if (httpInvocationContext == null) {
                throw new IllegalStateException("no context for channel?");
            }
            try {
                if (e.getMessage() instanceof HttpResponse) {
                    HttpResponse httpResponse = (HttpResponse) e.getMessage();
                    HttpAction<Request, Response> action = httpInvocationContext.getHttpAction();
                    ActionListener<Response> listener = httpInvocationContext.getListener();
                    httpInvocationContext.httpResponse = httpResponse;
                    if (httpResponse.getContent().readable() && listener != null && action != null) {
                        listener.onResponse(action.createResponse(httpInvocationContext));
                    }
                }
            } finally {
                ctx.getChannel().close();
                contexts.remove(ctx.getChannel());
            }
        }

        @SuppressWarnings("unchecked")
        public void exceptionCaught(ChannelHandlerContext ctx, ExceptionEvent e) throws Exception {
            HttpInvocationContext<Request, Response> httpInvocationContext = contexts.get(ctx.getChannel());
            try {
                if (httpInvocationContext != null && httpInvocationContext.getListener() != null) {
                    httpInvocationContext.getListener().onFailure(e.getCause());
                } else {
                    logger.error(e.getCause().getMessage(), e.getCause());
                }
            } finally {
                ctx.getChannel().close();
                contexts.remove(ctx.getChannel());
            }
        }
    }
}