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

import cz.moneta.test.harness.context.StoreAccessor;
import cz.moneta.test.harness.support.reflect.ReflectionUtils;
import org.apache.commons.beanutils.ConvertUtils;
import org.apache.commons.lang3.StringUtils;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.time.LocalDate;
import java.time.ZoneId;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import java.util.function.Supplier;
import java.util.stream.Stream;

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

    private Builders() {
    }

    public static <B extends DataProvider<B>> B newDataBuilder(Class<B> builder, StoreAccessor harness) {
        return (B) Proxy.newProxyInstance(
                Thread.currentThread().getContextClassLoader(),
                new Class[]{builder},
                new DataProviderInvocationHandler(harness));
    }

    private static class DataProviderInvocationHandler implements InvocationHandler {

        private final Map<String, Object> values = new HashMap<>();
        private final StoreAccessor harness;
        private String persistingMethod;

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

        @Override
        public Object invoke(Object proxy, Method method, Object[] args) throws InvocationTargetException, IllegalAccessException {
            if (StoreAccessor.class.equals(method.getDeclaringClass())) {
                return method.invoke(harness, args);
            }
            return Stream.<Supplier<Optional<?>>>of(
                    () -> handlePersists(method),
                    () -> handleWith(proxy, method, args),
                    () -> handleGet(method),
                    () -> handleDefault(proxy, method, args),
                    () -> handleValue(method),
                    () -> handleDate(method),
                    () -> handleGenerate(method, args))
                    .map(Supplier::get)
                    .filter(Optional::isPresent)
                    .map(Optional::get)
                    .findFirst()
                    .map(v -> {
                        values.put(method.getName(), v);
                        return v;
                    })
                    .orElseThrow(() -> new IllegalStateException("Could not handle method: " +
                            proxy.getClass().getInterfaces()[0].getSimpleName() + "#" + method.getName()));
        }

        private Optional<?> handlePersists(Method method) {
            Optional.ofNullable(method.getAnnotation(Persists.class))
                    .ifPresent(a -> persistingMethod = method.getName());
            //a bit hacky, but we need this method always pass-through
            return Optional.empty();
        }

        private Optional<?> handleWith(Object proxy, Method method, Object[] args) {
            return Optional.of(method)
                    .filter(m -> m.getName().startsWith("with"))
                    .map(m -> {
                        if (persistingMethod != null) {
                            throw new IllegalStateException(String.format(
                                    "Method NonClient#%s attempted to modify the entity after it had been persisted in " +
                                            "NonClient#%s. Please do all modifications before persisting.",
                                    method.getName(), persistingMethod));
                        }
                        String paramName = StringUtils.uncapitalize(m.getName().replaceFirst("with", ""));
                        values.put(paramName, args[0]);
                        return proxy;
                    });
        }

        private Optional<?> handleGet(Method method) {
            return Optional.of(method.getName()).map(values::get);
        }

        private Optional<?> handleDefault(Object proxy, Method method, Object[] args) {
            return Optional.of(method)
                    .filter(Method::isDefault)
                    .map(m -> ReflectionUtils
                            .invokeDefaultMethod(proxy, m, (String message) -> String.format("Error invoking method %s. Error message: %s", m.getName(), message), args));
        }

        private Optional<?> handleValue(Method method) {
            return Optional.ofNullable(method.getAnnotation(Value.class))
                    .map(v -> ConvertUtils.convert(v.value(), method.getReturnType()))
                    .filter(v -> String.class.isAssignableFrom(method.getReturnType()) || !String.class.isInstance(v));
        }

        private Optional<?> handleDate(Method method) {
            return Optional.ofNullable(method.getAnnotation(Date.class))
                    .map(d -> java.util.Date.from(Optional.of(d.year())
                            .filter(y -> y == 0)
                            .map(y -> LocalDate.now())
                            .orElseGet(() -> LocalDate.of(d.year(), d.month(), d.dayOfMonth()))
                            .plusDays(d.plusDays())
                            .minusDays(d.minusDays())
                            .atStartOfDay()
                            .atZone(ZoneId.systemDefault())
                            .toInstant()));
        }

        private Optional<?> handleGenerate(Method method, Object[] args) {
            return Optional.ofNullable(method.getAnnotation(Generate.class))
                    .map(g -> harness.generate(g.value(), args));
        }
    }
}
