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

import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.datatype.jdk8.Jdk8Module;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import com.fasterxml.jackson.module.paramnames.ParameterNamesModule;
import cz.moneta.test.harness.context.StoreAccessor;
import cz.moneta.test.harness.endpoints.RestEndpoint;
import cz.moneta.test.harness.exception.HarnessException;
import cz.moneta.test.harness.support.reflect.ReflectionUtils;
import cz.moneta.test.harness.support.util.BaseRequestBuilder;
import cz.moneta.test.harness.support.util.RequestPoll;
import jakarta.ws.rs.client.Entity;
import org.apache.commons.exec.util.MapUtils;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.tuple.Pair;

import java.lang.reflect.*;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.stream.Stream;

@SuppressWarnings("unchecked")
public final class Builders {

    private static final ObjectMapper JACKSON = new ObjectMapper()
            .configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false)
            .registerModules(new ParameterNamesModule(), new Jdk8Module(), new JavaTimeModule());

    private Builders() {
    }

    public static <RESP, R extends RestRequest<R, ?, RESP>> R newRestBuilder(
            Class<R> request, Class<? extends RestEndpoint> endpoint, StoreAccessor harness) {
        return (R) Proxy.newProxyInstance(
                Thread.currentThread().getContextClassLoader(),
                new Class[]{request, RequestPoll.class},
                new RequestBuilderInvocationHandler<RESP>(harness.getEndpoint(endpoint), harness));
    }

    private static class RequestBuilderInvocationHandler<RESP> extends BaseRequestBuilder<RESP> implements InvocationHandler {

        private final RestEndpoint endpoint;
        private final Map<String, Object> params = new HashMap<>();
        private final Map<String, Object> pathParams = new HashMap<>();
        private final Map<String, Object> headers = new HashMap<>();

        private RequestBuilderInvocationHandler(RestEndpoint endpoint, StoreAccessor harness) {
            super(harness);
            this.endpoint = endpoint;
        }

        @Override
        public Object invoke(Object proxy, Method method, Object[] args) {
            return Stream.<Supplier<Optional<?>>>of(
                    () -> handleLog(proxy, method, args),
                    () -> handleThen(proxy, method, args),
                    () -> handlePollUntil(proxy, method, args),
                    () -> handlePollDelay(proxy, method, args),
                    () -> handlePollRetries(proxy, method, args),
                    () -> handlePollEvalFunction(proxy, method, args),
                    () -> handleSend(proxy, method),
                    () -> handleHeaders(proxy, method, args),
                    () -> handlePathParams(proxy, method, args),
                    () -> handleParams(proxy, method, args))
                    .map(Supplier::get)
                    .filter(Optional::isPresent)
                    .map(Optional::get)
                    .findFirst()
                    .orElseThrow(() -> new IllegalStateException("Could not handle method: " +
                            proxy.getClass().getInterfaces()[0].getSimpleName() + "#" + method.getName()));
        }

        private Optional<Object> handleParams(Object proxy, Method method, Object[] args) {
            return Optional.ofNullable(args)
                    .map(Arrays::stream)
                    .orElseGet(Stream::empty)
                    .findFirst()
                    .map(a -> {
                        params.put(method.getName(), a);
                        return a;
                    })
                    .map(a -> proxy);
        }

        private Optional<Object> handleHeaders(Object proxy, Method method, Object[] args) {
            return Arrays.stream(method.getAnnotationsByType(Header.class))
                    .filter(a -> ArrayUtils.isNotEmpty(args))
                    .findFirst()
                    .map(h -> {
                        headers.put(h.value(), args[0]);
                        return h;
                    })
                    .map(h -> proxy);
        }

        private Optional<Object> handlePathParams(Object proxy, Method method, Object[] args) {
            return Arrays.stream(method.getAnnotationsByType(PathParam.class))
                    .filter(a -> ArrayUtils.isNotEmpty(args))
                    .findFirst()
                    .map(p -> {
                        pathParams.put(p.value(), args[0]);
                        return p;
                    })
                    .map(p -> proxy);
        }

        private Optional<RESP> handleSend(Object proxy, Method method) {
            return Optional.of(method)
                    .filter(m -> "send".equals(m.getName()))
                    .flatMap(m -> doSend(proxy));
        }

        protected Optional<RESP> doSend(Object proxy) {
            Class<?> builder = proxy.getClass().getInterfaces()[0];
            return Stream.<Supplier<Optional<RESP>>>of(
                    () -> Optional.ofNullable(builder.getAnnotation(Post.class))
                            .map(p -> (RESP) endpoint.post(
                                    resolvePathParams(p.value()),
                                    toEntity(params, getRequestClass(proxy), p.callType()),
                                    getResponseClass(proxy),
                                    resolveHeaders(builder), p.rawResponse()).getRight()),
                    () -> Optional.ofNullable(builder.getAnnotation(Get.class))
                            .map(g -> (RESP) endpoint.get(
                                    resolvePathParams(g.value()),
                                    params,
                                    getResponseClass(proxy),
                                    resolveHeaders(builder)).getRight()))
                    .map(Supplier::get)
                    .filter(Optional::isPresent)
                    .map(Optional::get)
                    .findFirst()
                    .map(resp -> {
                        harness.store(resp.getClass().getName(), resp);
                        responseLoggers.forEach(l -> l.accept(resp));
                        return resp;
                    });
        }

        private String resolvePathParams(String path) {
            return pathParams.entrySet().stream()
                    .<Function<String, String>>map(e -> s -> s.replace("{" + e.getKey() + "}", String.valueOf(e.getValue())))
                    .reduce(Function.identity(), Function::andThen)
                    .apply(path);
        }

        private Map<String, Object> resolveHeaders(Object proxy) {
            return ReflectionUtils.<Map<String, Object>>invokeDefaultMethodByName(
                    "defaultHeaders",
                    proxy,
                    (String message) -> String.format("Error getting default headers. Error message: %s", message))
                    .map(h -> MapUtils.merge(h, headers))
                    .orElse(headers);
        }

        private static Class<?> getRequestClass(Object proxy) {
            Pair<? extends Class<?>, ? extends Class<?>> pair = getRestRequestTypes(proxy);
            return pair.getLeft();
        }

        private static <R> Class<R> getResponseClass(Object proxy) {
            return (Class<R>) getRestRequestTypes(proxy).getRight();
        }

        private static Pair<? extends Class<?>, ? extends Class<?>> getRestRequestTypes(Object proxy) {
            return ReflectionUtils.getInterfaces(proxy.getClass()).stream()
                    .flatMap(i -> Arrays.stream(i.getGenericInterfaces()))
                    .filter(gi -> gi instanceof ParameterizedType)
                    .map(ParameterizedType.class::cast)
                    //.filter(gi -> RestRequest.class.equals(gi.getRawType()))
                    .findFirst()
                    .map(rr -> {
                        Type[] arguments = rr.getActualTypeArguments();
                        return Pair.of(resolveType(arguments[1], proxy), resolveType(arguments[2], proxy)); //FIXME
                    })
                    .orElseThrow(() -> new IllegalStateException(proxy.getClass().getInterfaces()[0].getSimpleName() +
                            " builder interfaces must inherit from RestRequest<REQ, RESP>"));
        }

        private static Class<?> resolveType(Type argument, Object proxy) {
           // if (argument instanceof TypeVariableImpl) {
           //     return (Class<?>) ((TypeVariableImpl) argument).getGenericDeclaration();
           // } else {
                return (Class<?>) argument;
           // }
        }

        private static Entity toEntity(Map<String, Object> params, Class<?> requestClass, CallType callType) {
            switch (callType) {
                case JSON:
                    return Entity.json(JACKSON.convertValue(params, requestClass));
                case FORM:
                    return Entity.form(RestUtils.toMultiMap(params));
                default:
                    throw new HarnessException("Call type: " + callType + " is unknown.");
            }
        }
    }
}
