package leigh.esp.sdk;

import leigh.eo.EO;
import leigh.eo.io.EOInputStream;
import leigh.eo.json.JsonInput;
import leigh.eo.json.JsonOutput;
import leigh.esp.sdk.link23.Link23Request;
import leigh.esp.sdk.link23.Link23Response;
import leigh.mecha.http.client.PersistentCookieStore;
import leigh.mecha.lang.FutureResult;
import leigh.mecha.log.MechaLogger;
import leigh.mecha.log.MechaLoggerFactory;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.ByteArrayEntity;
import org.apache.http.entity.mime.MultipartEntityBuilder;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.impl.client.LaxRedirectStrategy;
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Path;
import java.util.List;

/**
 * This class implements a network client for the Enhanced Services Platform (ESP).
 *
 * @author Alex Leigh
 */
public class ESPClient {
    private static final MechaLogger logger = MechaLoggerFactory.getLogger(ESPClient.class);
    private final CloseableHttpClient httpClient;
    private final String doorUrl;

    public ESPClient(String doorUrl) {
        this.doorUrl = doorUrl;
        PoolingHttpClientConnectionManager cm = new PoolingHttpClientConnectionManager();
        cm.setMaxTotal(200);
        cm.setDefaultMaxPerRoute(20);
        httpClient = HttpClientBuilder.create().
                setRedirectStrategy(new LaxRedirectStrategy()).setConnectionManager(cm).build();
    }

    public ESPClient(String doorUrl, Path cookieDb) throws IOException, ClassNotFoundException {
        this.doorUrl = doorUrl;
        PoolingHttpClientConnectionManager cm = new PoolingHttpClientConnectionManager();
        cm.setMaxTotal(200);
        cm.setDefaultMaxPerRoute(20);

        PersistentCookieStore cookieStore = PersistentCookieStore.loadOrNew(cookieDb);

        httpClient = HttpClientBuilder.create().setDefaultCookieStore(cookieStore).
                setRedirectStrategy(new LaxRedirectStrategy()).setConnectionManager(cm).build();
    }

    public Link23Response link23Request(String path, EO data) throws IOException {
        HttpPost post = new HttpPost(doorUrl + path);
        post.setHeader("Content-type", ContentTypes.CONTENT_TYPE_JSON);

        if (data != null) {
            ByteArrayOutputStream bos = new ByteArrayOutputStream();
            logger.debug("Sending request: {}", data);
            JsonOutput out = new JsonOutput(bos);
            out.writeEO(data);
            post.setEntity(new ByteArrayEntity(bos.toByteArray()));
        }

        CloseableHttpResponse response = httpClient.execute(post);
        if (response.getStatusLine().getStatusCode() != 200) {
            throw new IOException("ESP returned other than 200. Status: ["
                    + response.getStatusLine().getStatusCode() + "] [path: [" + path + "]");
        }

        return new Link23Response(response);
    }

    public Link23Response link23Request(String path, EOInputStream data) throws IOException {
        HttpPost post = new HttpPost(doorUrl + path);
        post.setHeader("Content-type", ContentTypes.CONTENT_TYPE_JSON);

        if (data != null) {
            ByteArrayOutputStream bos = new ByteArrayOutputStream();
            logger.debug("Sending request: {}", data);
            JsonOutput out = new JsonOutput(bos);
            while (true) {
                EO obj = data.readEo();
                if (obj == null) break;
                out.writeEO(obj);
            }
            post.setEntity(new ByteArrayEntity(bos.toByteArray()));
        }

        CloseableHttpResponse response = httpClient.execute(post);
        if (response.getStatusLine().getStatusCode() != 200) {
            throw new IOException("ESP returned other than 200. Status: ["
                    + response.getStatusLine().getStatusCode() + "] [path: [" + path + "]");
        }

        return new Link23Response(response);
    }

    /**
     * Perform a multipart request.
     */
    public Link23Response request(String path, EO data, List<ContentPart> parts) throws Exception {
        MultipartEntityBuilder mimeBuilder = MultipartEntityBuilder.create();
        HttpPost post = new HttpPost(doorUrl + path);


        // TODO We are leaking the bos, technically, but it should gc
        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        logger.debug("Sending request: {}", data);
        JsonOutput out = new JsonOutput(bos);
        out.writeEO(data);
        mimeBuilder.addTextBody(Link23Request.MULTIPART_REQUEST_KEY, bos.toString());


        for (ContentPart part : parts) {
            logger.info("Adding part to request: {}", part);
            part.addToBuilder(mimeBuilder);
        }

        post.setEntity(mimeBuilder.build());

        CloseableHttpResponse response = httpClient.execute(post);

        if (response.getStatusLine().getStatusCode() != 200) {
            throw new IOException("ESP returned other than 200. Status: ["
                    + response.getStatusLine().getStatusCode() + "] [path: [" + path + "]");
        }

        return new Link23Response(response);
    }

    /**
     * Perform an ESP API request, and return the result as EOs.
     * <p>
     * If the response fails to contain any data, response.handle() will be called with
     * <code>null</code> once. This indicates that the request has completed processing and no data
     * has been returned.
     */
    public FutureResult<Link23Response> apiRequest(String path, EO data, List<ContentPart> parts) {
        final FutureResult<Link23Response> fr = new FutureResult<>();
        new Thread(() -> {
            try {
                fr.set(request(path, data, parts));
                // TODO: Set an error here instead
            } catch (Exception e) {
                fr.set(null);
                e.printStackTrace();
            }
        }).start();
        return fr;
    }

    public FutureResult<Link23Response> apiRequest(String path, EO data) {
        final FutureResult<Link23Response> fr = new FutureResult<>();
        new Thread(() -> {
            try {
                fr.set(link23Request(path, data));
            } catch (Exception e) {
                // TODO: Set an error here instead
                fr.set(null);
                e.printStackTrace();
            }
        }).start();
        return fr;
    }

    @Deprecated
    public void request(String path, EO data) throws Exception {
        request(path, data, (ResponseConsumer) null);
    }

    /**
     * Perform a singlepart request against the HTTP endpoint, and return an input stream for the response content.
     * <p>
     * This request form holds the encoded version of the entire request in RAM temporarily.
     */
    @Deprecated
    public void request(String path, EO data, ResponseConsumer consumer) throws Exception {
        HttpPost post = new HttpPost(doorUrl + path);
        post.setHeader("Content-type", ContentTypes.CONTENT_TYPE_JSON);

        if (data != null) {
            ByteArrayOutputStream bos = new ByteArrayOutputStream();
            logger.debug("Sending request: {}", data);
            JsonOutput out = new JsonOutput(bos);
            out.writeEO(data);
            post.setEntity(new ByteArrayEntity(bos.toByteArray()));
        }

        try (CloseableHttpResponse response = httpClient.execute(post)) {
            if (response.getStatusLine().getStatusCode() != 200) {
                throw new IOException("ESP returned other than 200. Status: ["
                        + response.getStatusLine().getStatusCode() + "] [path: [" + path + "]");
            }
            if (consumer != null) {
                consumer.drainResponse(response.getEntity().getContent());
            }
        }
    }

    /**
     * Perform an ESP API request, and return the result as EOs.
     * <p>
     * If the response fails to contain any data, response.handle() will be called with
     * <code>null</code> once. This indicates that the request has completed processing and no data
     * has been returned.
     */
    @Deprecated
    public void apiRequest(String path, EO data, EOResponseConsumer response) throws Exception {
        // TODO: If the response errored (vs 200 w/ no data), we should do something special to indicate it to the
        // caller, who may care.
        request(path, data, new ResponseConsumer() {
            @Override
            public void drainResponse(InputStream is) throws Exception {
                JsonInput in = new JsonInput(is);
                boolean hasWritten = false;
                while (true) {
                    EO obj = in.readEo();
                    if (obj != null) {
                        response.handle(obj);
                        hasWritten = true;
                    } else {
                        if (!hasWritten) response.handle(null);
                        return;
                    }
                }
            }
        });
    }
}
