package cn.jiguang.common.connection;

import cn.jiguang.common.ClientConfig;
import cn.jiguang.common.resp.APIConnectionException;
import cn.jiguang.common.resp.APIRequestException;
import cn.jiguang.common.resp.ResponseWrapper;
import cn.jiguang.common.utils.StringUtils;
import io.netty.bootstrap.Bootstrap;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.Channel;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelOption;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.http.*;
import io.netty.handler.ssl.SslContext;
import io.netty.handler.ssl.SslContextBuilder;
import io.netty.handler.ssl.util.InsecureTrustManagerFactory;
import io.netty.util.CharsetUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.net.ssl.SSLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.text.MessageFormat;
import java.util.concurrent.CountDownLatch;

import static io.netty.handler.codec.http.HttpVersion.HTTP_1_1;


public class NettyHttpClient implements IHttpClient {

    private static Logger LOG = LoggerFactory.getLogger(NettyHttpClient.class);

    private String _authCode;
    private int _maxRetryTimes;
    private int _readTimeout;
    private Channel _channel;
    private Bootstrap b;
    private EventLoopGroup _workerGroup;
    private SslContext _sslCtx;

    private final String _encryptType;


    public NettyHttpClient(String authCode, HttpProxy proxy, ClientConfig config) {
        _maxRetryTimes = config.getMaxRetryTimes();
        _readTimeout = config.getReadTimeout();
        String message = MessageFormat.format("Created instance with "
                        + "connectionTimeout {0}, readTimeout {1}, maxRetryTimes {2}, SSL Version {3}",
                config.getConnectionTimeout(), _readTimeout, _maxRetryTimes, config.getSSLVersion());
        LOG.debug(message);
        _authCode = authCode;
        _encryptType = config.getEncryptType();
        try {
            _sslCtx = SslContextBuilder.forClient().trustManager(InsecureTrustManagerFactory.INSTANCE).build();
            _workerGroup = new NioEventLoopGroup();
            b = new Bootstrap(); // (1)
            b.group(_workerGroup); // (2)
            b.channel(NioSocketChannel.class); // (3)
            b.option(ChannelOption.SO_KEEPALIVE, true); // (4)
        } catch (SSLException e) {
            e.printStackTrace();
        }
    }

    public void sendRequest(HttpMethod method, String content, URI uri, BaseCallback callback) {
        FullHttpRequest request;
        b = new Bootstrap();
        if (b.group() == null) {
            b.group(_workerGroup);
        }
        b.channel(NioSocketChannel.class);
        b.option(ChannelOption.SO_KEEPALIVE, true);
        b.handler(new NettyClientInitializer(_sslCtx, callback, null));
        String scheme = uri.getScheme() == null ? "http" : uri.getScheme();
        int port = uri.getPort();
        if (port == -1) {
            if ("http".equalsIgnoreCase(scheme)) {
                port = 80;
            } else if ("https".equalsIgnoreCase(scheme)) {
                port = 443;
            }
        }
        _channel = b.connect(uri.getHost(), port).syncUninterruptibly().channel();
        if (null != content) {
            ByteBuf byteBuf = Unpooled.copiedBuffer(content.getBytes(CharsetUtil.UTF_8));
            request = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, method, uri.getRawPath(), byteBuf);
            request.headers().set(HttpHeaderNames.CONTENT_LENGTH, (long) byteBuf.readableBytes());
        } else {
            request = new DefaultFullHttpRequest(HTTP_1_1, method, uri.getRawPath());
        }
        if (!StringUtils.isEmpty(_encryptType)) {
            request.headers().set("X-Encrypt-Type", _encryptType);
        }
        request.headers().set(HttpHeaderNames.HOST, uri.getHost());
        request.headers().set(HttpHeaderNames.AUTHORIZATION, _authCode);
        request.headers().set("Content-Type", "application/json;charset=utf-8");

        LOG.info("Sending request. " + request);
        LOG.info("Send body: " + content);
        _channel.writeAndFlush(request);
        try {
            _channel.closeFuture().sync();
            _workerGroup.shutdownGracefully();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

    }

    @Override
    public ResponseWrapper sendGet(String url) throws APIConnectionException, APIRequestException {
        return sendGet(url, null);
    }

    public ResponseWrapper sendGet(String url, String content) throws APIConnectionException, APIRequestException {
        return sendHttpRequest(HttpMethod.GET, url, content);
    }

    @Override
    public ResponseWrapper sendPut(String url, String content) throws APIConnectionException, APIRequestException {
        return sendHttpRequest(HttpMethod.PUT, url, content);
    }

    @Override
    public ResponseWrapper sendPost(String url, String content) throws APIConnectionException, APIRequestException {
        return sendHttpRequest(HttpMethod.POST, url, content);
    }

    @Override
    public ResponseWrapper sendDelete(String url) throws APIConnectionException, APIRequestException {
        return sendDelete(url, null);
    }

    public ResponseWrapper sendDelete(String url, String content) throws APIConnectionException, APIRequestException {
        return sendHttpRequest(HttpMethod.DELETE, url, content);
    }



    private ResponseWrapper sendHttpRequest(HttpMethod method, String url, String body) throws APIConnectionException,
            APIRequestException{
        CountDownLatch latch = new CountDownLatch(1);
        NettyClientInitializer initializer = new NettyClientInitializer(_sslCtx, null, latch);
        b.handler(initializer);
        ResponseWrapper wrapper = new ResponseWrapper();
        URI uri = null;
		try {
			uri = new URI(url);
		} catch (URISyntaxException e1) {
			// TODO Auto-generated catch block
			LOG.debug(IO_ERROR_MESSAGE, e1);
            throw new APIConnectionException(READ_TIMED_OUT_MESSAGE, e1, true);
		}
        String scheme = uri.getScheme() == null ? "http" : uri.getScheme();
        String host = uri.getHost() == null ? "127.0.0.1" : uri.getHost();
        int port = uri.getPort();
        if (port == -1) {
            if ("http".equalsIgnoreCase(scheme)) {
                port = 80;
            } else if ("https".equalsIgnoreCase(scheme)) {
                port = 443;
            }
        }

        try {
            ChannelFuture connect = b.connect(host, port);
            _channel = connect.sync().channel();
            FullHttpRequest request;
            if (null != body) {
                ByteBuf byteBuf = Unpooled.copiedBuffer(body.getBytes(CharsetUtil.UTF_8));
                request = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, method, uri.getRawPath(), byteBuf);
                request.headers().set(HttpHeaderNames.CONTENT_LENGTH, (long) byteBuf.readableBytes());
            } else {
                request = new DefaultFullHttpRequest(HTTP_1_1, method, uri.getRawPath());
            }
            if (!StringUtils.isEmpty(_encryptType)) {
                request.headers().set("X-Encrypt-Type", _encryptType);
            }
            request.headers().set(HttpHeaderNames.HOST, uri.getHost());
            request.headers().set(HttpHeaderNames.AUTHORIZATION, _authCode);
            request.headers().set("Content-Type", "application/json;charset=utf-8");
            connect.awaitUninterruptibly();
            LOG.info("Sending request. " + request);
            LOG.info("Send body: " + body);
            _channel.writeAndFlush(request);
            latch.await();
            wrapper = initializer.getResponse();
            int status = wrapper.responseCode;
            String responseContent = wrapper.responseContent;
            if (status >= 200 && status < 300) {
                LOG.debug("Succeed to get response OK - responseCode:" + status);
                LOG.debug("Response Content - " + responseContent);

            } else if (status >= 300 && status < 400) {
                LOG.warn("Normal response but unexpected - responseCode:" + status + ", responseContent:" + responseContent);

            } else {
                LOG.warn("Got error response - responseCode:" + status + ", responseContent:" + responseContent);

                switch (status) {
                    case 400:
                        LOG.error("Your request params is invalid. Please check them according to error message.");
                        wrapper.setErrorObject();
                        break;
                    case 401:
                        LOG.error("Authentication failed! Please check authentication params according to docs.");
                        wrapper.setErrorObject();
                        break;
                    case 403:
                        LOG.error("Request is forbidden! Maybe your appkey is listed in blacklist or your params is invalid.");
                        wrapper.setErrorObject();
                        break;
                    case 404:
                        LOG.error("Request page is not found! Maybe your params is invalid.");
                        wrapper.setErrorObject();
                        break;
                    case 410:
                        LOG.error("Request resource is no longer in service. Please according to notice on official website.");
                        wrapper.setErrorObject();
                    case 429:
                        LOG.error("Too many requests! Please review your appkey's request quota.");
                        wrapper.setErrorObject();
                        break;
                    case 500:
                    case 502:
                    case 503:
                    case 504:
                        LOG.error("Seems encountered server error. Maybe JPush is in maintenance? Please retry later.");
                        break;
                    default:
                        LOG.error("Unexpected response.");
                }
                throw new APIRequestException(wrapper);
            }
        } catch (InterruptedException e) {
        	LOG.debug(IO_ERROR_MESSAGE, e);
            throw new APIConnectionException(READ_TIMED_OUT_MESSAGE, e, true);
        }
        return wrapper;
    }

    public void send(ByteBuf body, HttpMethod method, URI uri) {
        String scheme = uri.getScheme() == null ? "http" : uri.getScheme();
        String host = uri.getHost() == null ? "127.0.0.1" : uri.getHost();
        int port = uri.getPort();
        if (port == -1) {
            if ("http".equalsIgnoreCase(scheme)) {
                port = 80;
            } else if ("https".equalsIgnoreCase(scheme)) {
                port = 443;
            }
        }
        _channel = b.connect(host, port).syncUninterruptibly().channel();
        HttpRequest request;
        if (null != body) {
            request = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, method, uri.getRawPath(), body);
            request.headers().set(HttpHeaderNames.CONTENT_LENGTH, (long) body.readableBytes());
        } else {
            request = new DefaultFullHttpRequest(HTTP_1_1, method, uri.getRawPath());
        }
        request.headers().set(HttpHeaderNames.HOST, uri.getHost());
        request.headers().set(HttpHeaderNames.AUTHORIZATION, _authCode);
        request.headers().set("Content-Type", "application/json;charset=utf-8");
        LOG.info("Sending request. " + request);
        LOG.info("Send body: " + body);
        _channel.writeAndFlush(request);
    }


    public void close() {
        if (null != _channel) {
            _channel.closeFuture().syncUninterruptibly();
            _workerGroup.shutdownGracefully();
            _channel = null;
            _workerGroup = null;
        }
        System.out.println("Finished request(s)");
    }

    public interface BaseCallback {
        public void onSucceed(ResponseWrapper wrapper);
    }
}