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

import cz.moneta.test.harness.constants.HarnessConfigConstants;
import cz.moneta.test.harness.context.StoreAccessor;
import cz.moneta.test.harness.endpoints.ilodsserver.IlodsServerEndpoint;
import cz.moneta.test.harness.support.util.BaseRequestBuilder;
import cz.moneta.test.harness.support.util.RequestPoll;
import org.apache.commons.lang3.ArrayUtils;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Proxy;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import java.util.stream.Stream;

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

    private Builders() {}

    public static <RESP, R extends MwfRequest<R, RESP>> R newMwfBuilder(
            Class<R> request, TifClient tifClient, StoreAccessor harness) {

        return (R) Proxy.newProxyInstance(
                Thread.currentThread().getContextClassLoader(),
                new Class[]{request, RequestPoll.class},
                new RequestBuilderInvocationHandler<RESP>(request.getSimpleName(), getApiVersion(request, harness), tifClient, harness.getEndpoint(IlodsServerEndpoint.class), harness));
    }

    /**
     * Choosing MWF version based on current environment. Used in combination with annotation ApiVersion, ex. @ApiVersion(versionTST1 = "100", versionPPE = "200")
     * If annotation ApiVersion is not used it defaults to version 001.
     */
    private static <RESP, R extends MwfRequest<R, RESP>> String getApiVersion(Class<R> request, StoreAccessor harness) {
        String version = "001";
        ApiVersion apiVersion = request.getAnnotation(ApiVersion.class);
        if (apiVersion != null) {
            switch (harness.getConfig(HarnessConfigConstants.ENVIRONMENT_TYPE).toUpperCase()) {
                case "TST3":
                    version = apiVersion.versionTST3();
                    break;
                case "TST1":
                    version = apiVersion.versionTST1();
                    break;
                case "PPE":
                    version = apiVersion.versionPPE();
                    break;
                case "EDU":
                    version = apiVersion.versionEDU();
                    break;
            }
        }
        return version;
    }

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

        private final String name;
        private final String apiVersion;
        private TifClient tifClient;
        private IlodsServerEndpoint endpoint;
        private final Map<String, Object> params = new HashMap<>();

        private RequestBuilderInvocationHandler(String name, String apiVersion, TifClient tifClient, IlodsServerEndpoint endpoint, StoreAccessor harness) {
            super(harness);
            this.name = name;
            this.apiVersion = apiVersion;
            this.tifClient = tifClient;
            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),
                    () -> 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) {
            Optional.ofNullable(method.getAnnotation(Parameter.class))
                    .filter(p -> ArrayUtils.isNotEmpty(args))
                    .ifPresent(p -> {
                        if (args[0] instanceof Collection) {
                            Object collection = ((Collection) args[0]).stream()
                                    .filter(MwfData.class::isInstance)
                                    .map(s -> ((MwfData) s).getParams())
                                    .collect(Collectors.toList());
                            params.put(p.name(), collection);
                        } else {
                            params.put(p.name(), args[0]);
                        }
                    });
            return Optional.of(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<RESP> responseClass = getResponseClass(proxy);
            Set<String> outputParamNames = Arrays.stream(responseClass.getMethods())
                    .map(m -> m.getAnnotation(Parameter.class))
                    .filter(Objects::nonNull)
                    .map(Parameter::name)
                    .collect(Collectors.toSet());

            return Optional.ofNullable(endpoint.callMWF(name, apiVersion, tifClient, params, outputParamNames))
                    .map(response -> {
                        RESP responseProxy = toResponse(responseClass, response);
                        harness.store(responseClass.getSimpleName(), response);
                        responseLoggers.forEach(l -> l.accept(responseProxy));
                        return responseProxy;
                    });
        }

        private RESP toResponse(Class<RESP> responseClass, Map<String, Object> outputParameters) {
            return (RESP) Proxy.newProxyInstance(
                    Thread.currentThread().getContextClassLoader(),
                    new Class[]{responseClass},
                    new ResponseInvocationHandler(outputParameters));
        }

        private static <R> Class<R> getResponseClass(Object proxy) {
            return (Class<R>) ((ParameterizedType) ((Class<?>) proxy.getClass()
                    .getGenericInterfaces()[0])
                    .getGenericInterfaces()[0])
                    .getActualTypeArguments()[1];
        }

        private static class ResponseInvocationHandler implements InvocationHandler {

            private final Map<String, Object> outputParameters;

            public ResponseInvocationHandler(Map<String, Object> outputParameters) {
                this.outputParameters = outputParameters;
            }

            @Override
            public Object invoke(Object proxy, Method method, Object[] args) {
                return Optional.ofNullable(method.getAnnotation(Parameter.class))
                        .map(Parameter::name)
                        .map(outputParameters::get)
                        .flatMap(p -> Stream.<Supplier<Optional<?>>>of(
                                () -> extractCollection(method, p),
                                () -> extractObject(method, p),
                                () -> Optional.of(p))
                                .map(Supplier::get)
                                .filter(Optional::isPresent)
                                .map(Optional::get)
                                .findFirst()
                        )
                        .orElse(null);
            }

            private Optional<?> extractObject(Method method, Object param) {
                return Arrays.stream(method.getReturnType().getConstructors())
                        .filter(c -> param instanceof Map)
                        .filter(c -> c.getParameterTypes().length == 1 && Map.class.equals(c.getParameterTypes()[0]))
                        .findFirst()
                        .map(c -> {
                            try {
                                return c.newInstance(param);
                            } catch (InstantiationException | IllegalAccessException | InvocationTargetException e) {
                                throw new IllegalArgumentException("Failed to map parameter", e);
                            }
                        });
            }

            private Optional<?> extractCollection(Method method, Object param) {
                return Optional.of(param)
                        .filter(p -> List.class.equals(method.getReturnType()))
                        .filter(Collection.class::isInstance)
                        .map(Collection.class::cast)
                        .map(c -> {
                            Class<?> dataType = (Class<?>) ((ParameterizedType) method.getGenericReturnType())
                                    .getActualTypeArguments()[0];
                            return c.stream()
                                    .map(i -> {
                                        try {
                                            return dataType.getConstructor(Map.class).newInstance((Map) i);
                                        } catch (InstantiationException | NoSuchMethodException |
                                                InvocationTargetException | IllegalAccessException e) {
                                            throw new IllegalArgumentException("Failed to map parameter", e);
                                        }
                                    })
                                    .collect(Collectors.toList());
                        });
            }
        }
    }

}
