package cz.moneta.test.harness.connectors.rest;

import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.tuple.Pair;

import jakarta.ws.rs.InternalServerErrorException;
import jakarta.ws.rs.ProcessingException;
import jakarta.ws.rs.client.Client;
import jakarta.ws.rs.client.Entity;
import jakarta.ws.rs.client.Invocation;
import jakarta.ws.rs.client.WebTarget;
import jakarta.ws.rs.core.CacheControl;
import jakarta.ws.rs.core.GenericType;
import jakarta.ws.rs.core.Response;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.function.Function;

import static cz.moneta.test.harness.constants.HarnessConfigConstants.DEFAULT_REST_READ_TIMEOUT;
import static cz.moneta.test.harness.support.rest.RestUtils.toMultiMap;

public class SimpleRestConnector extends BaseRestConnector implements RestConnector {

    protected WebTarget target;
    protected List<ResponseHandler<Pair<Invocation.Builder, Function<Invocation.Builder, Response>>, Response>> responseHandlers = new ArrayList<>();

    public SimpleRestConnector(String endpointUrl, String loggerName) {
        this(endpointUrl, loggerName, DEFAULT_REST_READ_TIMEOUT);
    }

    public SimpleRestConnector(String endpointUrl, String loggerName, long readTimeout) {
        try {
            Client client = createHttpClient(loggerName, readTimeout);
            target = client.target(endpointUrl);
        } catch (Exception e) {
            throw new RuntimeException("REST connector is not configured properly", e);
        }
    }

    @Override
    public <T> Pair<Integer, T> get(String path, Map<String, Object> properties, GenericType<T> responseType, Map<String, Object> headers) {
        try {
            Invocation.Builder invocation = properties.entrySet().stream()
                    .reduce(target, (t, e) -> t.queryParam(e.getKey(), e.getValue()), (t1, t2) -> t1)
                    .path(path)
                    .request()
                    .headers(toMultiMap(headers))
                    .cacheControl(CacheControl.valueOf("no-cache"));

            Response response = responseHandlers.stream()
                    .reduce(
                            invocation.get(),
                            (resp, h) -> h.handle(Pair.of(invocation, Invocation.Builder::get), resp),
                            (h1, h2) -> h1);

            return Pair.of(response.getStatus(), response.readEntity(responseType));
        } catch (InternalServerErrorException serverError) {
            return throwWithResponseText(serverError);
        } catch (ProcessingException e) {
            throw new RuntimeException("Error during HTTP connection " + e.getMessage(), e);
        }
    }

    @Override
    public <T> Pair<Integer, T> post(String path, Entity<?> request, GenericType<T> responseType, Map<String, Object> headers) {
        try {
            Response response = doPost(path, request, headers);

            return Pair.of(response.getStatus(), response.readEntity(responseType));
        } catch (InternalServerErrorException serverError) {
            return throwWithResponseText(serverError);
        } catch (ProcessingException e) {
            throw new RuntimeException("Error during HTTP connection " + e.getMessage(), e);
        }
    }

    protected Response doPost(String path, Entity<?> request, Map<String, Object> headers) {
        Invocation.Builder invocation = target.path(path)
                .request()
                .headers(toMultiMap(headers))
                .cacheControl(CacheControl.valueOf("no-cache"));

        return responseHandlers.stream()
                .reduce(
                        invocation.post(request),
                        (resp, h) -> h.handle(Pair.of(invocation, i -> i.post(request)), resp),
                        (h1, h2) -> h1);
    }

    @Override
    public <T> Pair<Integer, T> delete(String path, Map<String, Object> properties, GenericType<T> responseType, Map<String, Object> headers) {
        try {
            Invocation.Builder invocation = properties.entrySet().stream()
                    .reduce(target, (t, e) -> t.queryParam(e.getKey(), e.getValue()), (t1, t2) -> t1)
                    .path(path)
                    .request()
                    .headers(toMultiMap(headers))
                    .cacheControl(CacheControl.valueOf("no-cache"));

            Response response = responseHandlers.stream()
                    .reduce(
                            invocation.delete(),
                            (resp, h) -> h.handle(Pair.of(invocation, Invocation.Builder::delete), resp),
                            (h1, h2) -> h1);

            return Pair.of(response.getStatus(), response.readEntity(responseType));
        } catch (InternalServerErrorException serverError) {
            return throwWithResponseText(serverError);
        } catch (ProcessingException e) {
            throw new RuntimeException("Error during HTTP connection " + e.getMessage(), e);
        }
    }

    @Override
    public <T> Pair<Integer, T> patch(String path, Entity<?> request, GenericType<T> responseType, Map<String, Object> headers) {
        try {
           Invocation.Builder invocation = target.path(path)
                    .request()
                    .headers(toMultiMap(headers))
                    .cacheControl(CacheControl.valueOf("no-cache"));

            Response response = responseHandlers.stream()
                    .reduce(
                            invocation.method("PATCH", request),
                            (resp, h) -> h.handle(Pair.of(invocation, i -> i.method("PATCH", request)), resp),
                            (h1, h2) -> h1);

            return Pair.of(response.getStatus(), response.readEntity(responseType));
        } catch (InternalServerErrorException serverError) {
            return throwWithResponseText(serverError);
        } catch (ProcessingException e) {
            throw new RuntimeException("Error during HTTP connection " + e.getMessage(), e);
        }
    }

    @Override
    public RestConnector registerResponseHandler(ResponseHandler<Pair<Invocation.Builder,
            Function<Invocation.Builder, Response>>, Response> responseHandler) {
        responseHandlers.add(responseHandler);
        return this;
    }

    protected <T> T throwWithResponseText(InternalServerErrorException serverError) {
        String response = "N/A";
        try {
            response = IOUtils.toString((InputStream) serverError.getResponse().getEntity(), Charset.defaultCharset());
        } catch (IOException ignore) {
        }
        throw new AssertionError(String.format("Request failed. HTTP status: %d, response: %s", serverError.getResponse().getStatus(), response));
    }
}
