package kg.apc.jmeter.http;

import kg.apc.jmeter.notifier.StatusNotifierCallback;
import net.sf.json.JSON;
import net.sf.json.JSONNull;
import net.sf.json.JSONObject;
import net.sf.json.JSONSerializer;
import net.sf.json.JsonConfig;
import org.apache.commons.io.output.ByteArrayOutputStream;
import org.apache.http.HttpEntity;
import org.apache.http.HttpHost;
import org.apache.http.HttpResponse;
import org.apache.http.auth.AuthScope;
import org.apache.http.auth.NTCredentials;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPatch;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.methods.HttpRequestBase;
import org.apache.http.conn.params.ConnRoutePNames;
import org.apache.http.entity.ContentType;
import org.apache.http.entity.StringEntity;
import org.apache.http.entity.mime.FormBodyPart;
import org.apache.http.entity.mime.HttpMultipartMode;
import org.apache.http.entity.mime.MultipartEntity;
import org.apache.http.impl.client.AbstractHttpClient;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.params.CoreConnectionPNames;
import org.apache.http.params.HttpParams;
import org.apache.jmeter.JMeter;
import org.apache.jmeter.util.JMeterUtils;
import org.apache.jorphan.logging.LoggingManager;
import org.apache.log.Logger;

import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.net.InetAddress;
import java.util.LinkedList;

/**
 * Class for working with HTTP requests
 */
public class HttpUtils {

    protected static final Logger log = LoggingManager.getLoggerForClass();
    protected final static int TIMEOUT = 5;

    protected final AbstractHttpClient httpClient;
    protected final StatusNotifierCallback notifier;
    protected final String address;
    protected final String dataAddress;

    public HttpUtils(StatusNotifierCallback notifier, String address, String dataAddress) {
        this.notifier = notifier;
        this.address = address;
        this.dataAddress = dataAddress;
        this.httpClient = createHTTPClient();
    }

    /**
     * Create Get Request
     */
    public HttpGet createGet(String uri) {
        HttpGet httpGet = new HttpGet(uri);
        httpGet.setHeader("Content-Type", "application/json");
        return httpGet;
    }

    /**
     * Create Post Request with json body
     */
    public HttpPost createPost(String uri, String data) {
        HttpPost httpPost = new HttpPost(uri);
        httpPost.setHeader("Content-Type", "application/json");
        HttpEntity entity = new StringEntity(data, ContentType.APPLICATION_JSON);
        httpPost.setEntity(entity);
        return httpPost;
    }

    /**
     * Create Post Request with FormBodyPart body
     */
    public HttpPost createPost(String uri, LinkedList<FormBodyPart> partsList) {
        HttpPost postRequest = new HttpPost(uri);
        MultipartEntity multipartRequest = new MultipartEntity(HttpMultipartMode.BROWSER_COMPATIBLE);
        for (FormBodyPart part : partsList) {
            multipartRequest.addPart(part);
        }
        postRequest.setEntity(multipartRequest);
        return postRequest;
    }

    /**
     * Create Patch Request
     */
    public HttpPatch createPatch(String url, JSON data) {
        HttpPatch patch = new HttpPatch(url);
        patch.setHeader("Content-Type", "application/json");

        String string = data.toString(1);
        try {
            patch.setEntity(new StringEntity(string, "UTF-8"));
        } catch (UnsupportedEncodingException e) {
            throw new RuntimeException(e.getMessage(), e);
        }
        return patch;
    }

    /**
     * Execute Http request and verify response
     * @param request - HTTP Request
     * @param expectedCode - expected response code
     * @return - response in JSONObject
     */
    public JSONObject queryObject(HttpRequestBase request, int expectedCode) throws IOException {
        JSON res = query(request, expectedCode);
        if (!(res instanceof JSONObject)) {
            throw new IOException("Unexpected response: " + res);
        }
        return (JSONObject) res;
    }

    /**
     * Execute Http request and response code
     * @param request - HTTP Request
     * @param expectedCode - expected response code
     * @return - response in JSONObject
     */
    public JSON query(HttpRequestBase request, int expectedCode) throws IOException {
        log.info("Requesting: " + request);
        addRequiredHeader(request);

        HttpParams requestParams = request.getParams();
        requestParams.setIntParameter(CoreConnectionPNames.SO_TIMEOUT, TIMEOUT * 1000);
        requestParams.setIntParameter(CoreConnectionPNames.CONNECTION_TIMEOUT, TIMEOUT * 1000);

        synchronized (httpClient) {
            String response;
            try {
                HttpResponse result = httpClient.execute(request);

                int statusCode = result.getStatusLine().getStatusCode();

                response = getResponseEntity(result);

                if (statusCode != expectedCode) {

                    notifier.notifyAbout("Response with code " + statusCode + ": " + extractErrorMessage(response));
                    throw new IOException("API responded with wrong status code: " + statusCode);
                } else {
                    log.debug("Response: " + response);
                }
            } finally {
                request.abort();
            }

            if (response == null || response.isEmpty()) {
                return JSONNull.getInstance();
            } else {
                return JSONSerializer.toJSON(response, new JsonConfig());
            }
        }
    }

    protected String extractErrorMessage(String response) {
        return response;
    }

    protected void addRequiredHeader(HttpRequestBase httpRequestBase) {
        // NOOP
    }

    private String getResponseEntity(HttpResponse result) throws IOException {
        HttpEntity entity = result.getEntity();
        if (entity == null) {
            log.debug("Null response entity");
            return null;
        }

        try {
            ByteArrayOutputStream bos = new ByteArrayOutputStream();
            entity.writeTo(bos);
            byte[] bytes = bos.toByteArray();
            if (bytes == null) {
                bytes = "null".getBytes();
            }
            String response = new String(bytes);
            log.debug("Response with code " + result + ": " + response);
            return response;
        } finally {
            InputStream content = entity.getContent();
            if (content != null) {
                content.close();
            }
        }
    }

    private static AbstractHttpClient createHTTPClient() {
        AbstractHttpClient client = new DefaultHttpClient();
        String proxyHost = System.getProperty("https.proxyHost", "");
        if (!proxyHost.isEmpty()) {
            int proxyPort = Integer.parseInt(System.getProperty("https.proxyPort", "-1"));
            log.info("Using proxy " + proxyHost + ":" + proxyPort);
            HttpParams params = client.getParams();
            HttpHost proxy = new HttpHost(proxyHost, proxyPort);
            params.setParameter(ConnRoutePNames.DEFAULT_PROXY, proxy);

            String proxyUser = System.getProperty(JMeter.HTTP_PROXY_USER, JMeterUtils.getProperty(JMeter.HTTP_PROXY_USER));
            if (proxyUser != null) {
                log.info("Using authenticated proxy with username: " + proxyUser);
                String proxyPass = System.getProperty(JMeter.HTTP_PROXY_PASS, JMeterUtils.getProperty(JMeter.HTTP_PROXY_PASS));

                String localHost;
                try {
                    localHost = InetAddress.getLocalHost().getCanonicalHostName();
                } catch (Throwable e) {
                    log.error("Failed to get local host name, defaulting to 'localhost'", e);
                    localHost = "localhost";
                }

                AuthScope authscope = new AuthScope(proxyHost, proxyPort);
                String proxyDomain = JMeterUtils.getPropDefault("http.proxyDomain", "");
                NTCredentials credentials = new NTCredentials(proxyUser, proxyPass, localHost, proxyDomain);
                client.getCredentialsProvider().setCredentials(authscope, credentials);
            }
        }
        return client;
    }

    public AbstractHttpClient getHttpClient() {
        return httpClient;
    }

    public StatusNotifierCallback getNotifier() {
        return notifier;
    }

    public String getAddress() {
        return address;
    }

    public String getDataAddress() {
        return dataAddress;
    }
}