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

import cz.moneta.test.harness.context.StoreAccessor;
import cz.moneta.test.harness.exception.HarnessException;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.tuple.Pair;
import org.apache.commons.lang3.tuple.Triple;

import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.stream.Stream;

@SuppressWarnings("unchecked")
public abstract class BaseRequestBuilder<RESP> {

    protected final StoreAccessor harness;
    private Function<RESP, Object> pollElementExtractor;
    private int pollDelay = 500;
    private int pollRetries = 10;
    protected List<Consumer<RESP>> responseLoggers = new ArrayList<>();

    public BaseRequestBuilder(StoreAccessor harness) {
        this.harness = harness;
    }

    protected Optional<?> handleLog(Object proxy, Method method, Object[] args) {
        return Optional.of(method)
                .filter(m -> "log".equals(m.getName()))
                .map(m -> {
                    if (Level.class.equals(m.getParameterTypes()[0])) {
                        return Triple.of((Level) args[0], (String) args[1], (Function<RESP, Object>[]) args[2]);
                    } else {
                        return Triple.of(Level.INFO, (String) args[0], (Function<RESP, Object>[]) args[1]);
                    }
                })
                .map(t -> {
                    responseLoggers.add((RESP r) -> harness.log(t.getLeft(), t.getMiddle(), Arrays
                            .stream(t.getRight()).map(a -> a.apply(r)).toArray()));
                    return proxy;
                });
    }

    protected Optional<Object> handleThen(Object proxy, Method method, Object[] args) {
        return Optional.of(method)
                .filter(m -> "then".equals(m.getName()))
                .filter(m -> ArrayUtils.isNotEmpty(args))
                .map(m -> args[0])
                .filter(Function.class::isInstance)
                .map(Function.class::cast)
                .map(f -> f.apply(proxy));
    }

    protected Optional<Object> handlePollUntil(Object proxy, Method method, Object[] args) {
        return Optional.of(method)
                .filter(m -> "pollUntil".equals(m.getName()))
                .map(m -> {
                    pollElementExtractor = (Function<RESP, Object>) args[0];
                    return proxy;
                });
    }

    protected Optional<Object> handlePollDelay(Object proxy, Method method, Object[] args) {
        return Optional.of(method)
                .filter(m -> "retryDelayMillis".equals(m.getName()))
                .map(m -> {
                    pollDelay = (int) args[0];
                    return proxy;
                });
    }

    protected Optional<Object> handlePollRetries(Object proxy, Method method, Object[] args) {
        return Optional.of(method)
                .filter(m -> "maxRetries".equals(m.getName()))
                .map(m -> {
                    pollRetries = (int) args[0];
                    return proxy;
                });
    }

    protected Optional<RESP> handlePollEvalFunction(Object proxy, Method method, Object[] args) {
        return Stream.<Pair<String, Supplier<Function<Object, Boolean>>>>of(
                Pair.of("isNull", () -> Objects::isNull),
                Pair.of("isNotNull", () -> Objects::nonNull),
                Pair.of("isEqual", () -> o -> Objects.equals(args[0], o)))
                .filter(p -> method.getName().equals(p.getLeft()))
                .findFirst()
                .map(Pair::getRight)
                .map(Supplier::get)
                .map(f -> doPoll(proxy, f));
    }

    private RESP doPoll(Object proxy, Function<Object, Boolean> evalFunction) {
        return Stream.iterate(0, this::delay)
                .limit(pollRetries)
                .map(i -> doSend(proxy))
                .filter(Optional::isPresent)
                .map(Optional::get)
                .filter(res -> {
                    Boolean result = pollElementExtractor.andThen(evalFunction).apply(res);
                    if (!result) {
                        harness.log(Level.INFO, "Poll unsuccessful (response was: {})", res.toString());
                    }
                    return result;
                })
                .findFirst()
                .orElseThrow(() -> new HarnessException(String.format("Poll failed. Tried %d time(s) with delay %d", pollRetries, pollDelay)));
    }

    protected abstract Optional<RESP> doSend(Object proxy);

    private int delay(int i) {
        try {
            TimeUnit.MILLISECONDS.sleep(pollDelay);
        } catch (InterruptedException e) {
            //
        }
        return i;
    }

}
