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

import cz.moneta.test.harness.context.StoreAccessor;
import cz.moneta.test.harness.endpoints.greenscreen.GreenScreenEndpoint;
import cz.moneta.test.harness.exception.HarnessException;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.tuple.Pair;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.Optional;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import java.util.stream.Stream;

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

    private static final Logger LOG = LogManager.getLogger(Builders.class);

    private Builders() {
    }

    public static <A extends GreenScreenFlow<A>> A newGreenScreenFlowBuilder(
            Class<A> flow, GreenScreenEndpoint endpoint, StoreAccessor harness) {

        A builder = (A) Proxy.newProxyInstance(
                Thread.currentThread().getContextClassLoader(),
                new Class[]{flow},
                getInvocationHandler(endpoint, harness));

        processClassAnnotations(builder, endpoint, flow, harness);

        return builder;
    }

    private static <A extends GreenScreenFlow<A>> void processClassAnnotations(
            A builder, GreenScreenEndpoint endpoint, Class<A> flow, StoreAccessor harness) {

        List<Class<?>> interfaces = getInterfaces(builder.getClass(), new ArrayList<>());
        doCheckScreenContains(endpoint, flow, interfaces.stream()
                .flatMap(i -> Arrays.stream(i.getAnnotationsByType(CheckScreenContains.class)))
                .toArray(CheckScreenContains[]::new));
        doStoreElementContent(endpoint, harness, interfaces.stream()
                .flatMap(i -> Arrays.stream(i.getAnnotationsByType(StoreContent.class)))
                .toArray(StoreContent[]::new));
        doLog(endpoint, harness, interfaces.stream()
                .flatMap(i -> Arrays.stream(i.getAnnotationsByType(Log.class)))
                .toArray(Log[]::new));
    }

    private static List<Class<?>> getInterfaces(Class<?> clazz, List<Class<?>> accumulator) {
        Arrays.stream(clazz.getInterfaces()).forEach(c -> {
            accumulator.add(c);
            getInterfaces(c, accumulator);
        });
        return accumulator;
    }

    private static InvocationHandler getInvocationHandler(GreenScreenEndpoint endpoint, StoreAccessor harness) {
        return (proxy, method, args) -> {
            if (StoreAccessor.class.equals(method.getDeclaringClass())) {
                return method.invoke(harness, args);
            }
            return builderOperations(endpoint, harness, proxy, method, args);
        };
    }

    private static Object handleReturnType(GreenScreenEndpoint endpoint, Object proxy, Method method, Object[] args,
                                           StoreAccessor harness) {

        return Stream.<Supplier<Optional<Object>>>of(
                () -> handleThen(proxy, method, args),
                () -> resolveScreen(endpoint, proxy, method, harness))
                .map(Supplier::get)
                .filter(Optional::isPresent)
                .map(Optional::get)
                .findFirst()
                .orElse(proxy);
    }

    private static Optional<Object> handleThen(Object proxy, Method method, Object[] args) {
        return Arrays.stream(Builder.class.getMethods())
                .filter(m -> "then".equals(m.getName()))
                .filter(method::equals)
                .findFirst()
                .map(m -> args[0])
                .filter(Function.class::isInstance)
                .map(Function.class::cast)
                .map(f -> f.apply(proxy));
    }

    private static Object builderOperations(
            GreenScreenEndpoint endpoint, StoreAccessor harness, Object proxy, Method method, Object[] args) {

        doCheckScreenContains(endpoint, method, args);
        doLog(endpoint, harness, method.getAnnotationsByType(Log.class));
        doStoreElementContent(endpoint, harness, method.getAnnotationsByType(StoreContent.class));
        doType(endpoint, method, args);
        doKeyPress(endpoint, method);
        return handleReturnType(endpoint, proxy, method, args, harness);
    }

    private static void doCheckScreenContains(GreenScreenEndpoint endpoint, Method method, Object[] args) {
        CheckScreenContains[] annotations = method.getAnnotationsByType(CheckScreenContains.class);
        zipArgsAndExplicitValues(annotations, CheckScreenContains::text, method, args).forEach(p -> {
            LOG.debug(() -> String.format("CheckScreenContains at row %s, column %s: '%s'",
                    p.getLeft().row(), p.getLeft().column(), p.getRight()));
            String actual = endpoint.getText(p.getLeft().row(), p.getLeft().column(), p.getRight().length());
            if (!p.getRight().equals(actual)) {
                throw new HarnessException(String.format(
                        "%s#%s: Found '%s' at row %s, column %s, but expected '%s'",
                        method.getDeclaringClass().getSimpleName(), method.getName(), actual, p.getLeft().row(),
                        p.getLeft().column(), p.getRight()));
            }
        });
    }

    private static void doCheckScreenContains(GreenScreenEndpoint endpoint, Class<?> flow, CheckScreenContains[] annotations) {
        if (Arrays.stream(annotations).anyMatch(a -> StringUtils.isBlank(a.text()))) {
            throw new IllegalStateException("Class-level CheckScreenContains annotations must have its text specified");
        }

        Arrays.stream(annotations)
                .forEach(a -> {
                    LOG.debug(() -> String.format("CheckScreenContains at row %s, column %s: '%s'",
                            a.row(), a.column(), a.text()));
                    String actual = endpoint.getText(a.row(), a.column(), a.text().length());
                    if (!a.text().equals(actual)) {
                        throw new HarnessException(String.format(
                                "%s: Found '%s' at row %s, column %s, but expected '%s'",
                                flow.getSimpleName(), actual, a.row(), a.column(), a.text()));
                    }
                });
    }

    private static void doLog(GreenScreenEndpoint endpoint, StoreAccessor harness, Log[] logs) {
        Arrays.stream(logs)
                .forEach(log -> Optional.ofNullable(endpoint.getText(log.row(), log.column(), log.length()))
                        .filter(StringUtils::isNotBlank)
                        .map(c -> {
                            harness.log(log.level(), log.template(), c);
                            return c;
                        })
                        .orElseThrow(() -> new HarnessException("Screen content could not be read")));
    }

    private static void doStoreElementContent(
            GreenScreenEndpoint endpoint, StoreAccessor harness, StoreContent[] secs) {

        Arrays.stream(secs)
                .forEach(sec -> Optional.ofNullable(endpoint.getText(sec.row(), sec.column(), sec.length()))
                        .filter(StringUtils::isNotBlank)
                        .map(c -> {
                            LOG.debug(() -> String.format("StoreContent: %s", c));
                            harness.store(sec.storeKey(), c);
                            return c;
                        })
                        .orElseThrow(() -> new HarnessException("Screen content could not be read")));
    }

    private static void doType(GreenScreenEndpoint endpoint, Method method, Object[] args) {
        Type[] annotations = method.getAnnotationsByType(Type.class);
        zipArgsAndExplicitValues(annotations, Type::text, method, args).forEach(p -> {
            LOG.debug(() -> String.format("Type at row %s, column %s: '%s'", p.getLeft().row(), p.getLeft()
                    .column(), p.getRight()));
            endpoint.type(p.getLeft().row(), p.getLeft().column(), p.getRight());
        });
    }

    private static <A> List<Pair<A, String>> zipArgsAndExplicitValues(
            A[] annotations, Function<A, String> extractor, Method method, Object[] args) {

        if (ArrayUtils.isEmpty(annotations)) {
            return Collections.emptyList();
        }

        LinkedList<Object> argsQueue = args == null
                ? new LinkedList<>()
                : new LinkedList<>(Arrays.asList(args));
        List<Pair<A, String>> pairs = Arrays.stream(annotations)
                .map(a -> Pair.of(a, Optional.of(extractor.apply(a))
                        .filter(StringUtils::isNotBlank)
                        .orElseGet(() -> String.valueOf(argsQueue.poll()))))
                .collect(Collectors.toList());

        if (!argsQueue.isEmpty()) {
            throw new IllegalStateException("The number of arguments passed to method '"
                    + method.getDeclaringClass().getSimpleName() + "#" + method.getName()
                    + "' plus explicit declarations (i.e. those via Annotation#text()) must match the annotation "
                    + "count!");
        }

        return pairs;
    }

    private static void doKeyPress(GreenScreenEndpoint endpoint, Method method) {
        Arrays.stream(method.getAnnotationsByType(KeyPress.class))
                .forEach(ks -> endpoint.pressKey(ks.value()));
    }

    private static <A extends GreenScreenFlow<A>> Optional<Object> resolveScreen(
            GreenScreenEndpoint endpoint, Object proxy, Method method, StoreAccessor harness) {

        return Stream.<Supplier<Optional<A>>>of(
                () -> handleScreenFlowReturnType(endpoint, proxy, method, harness),
                () -> Optional.of((A) proxy))
                .map(Supplier::get)
                .filter(Optional::isPresent)
                .map(Optional::get)
                .map(Object.class::cast)
                .findFirst();
    }

    private static <A extends GreenScreenFlow<A>> Optional<A> handleScreenFlowReturnType(
            GreenScreenEndpoint endpoint, Object proxy, Method method, StoreAccessor harness) {

        return Optional.of(method.getReturnType())
                .filter(rt -> !rt.isInstance(proxy))
                .filter(GreenScreenFlow.class::isAssignableFrom)
                .map(screen -> {
                    LOG.debug(() -> String.format("Screen (return): %s", screen.getSimpleName()));
                    return newGreenScreenFlowBuilder((Class<A>) screen, endpoint, harness);
                });
    }
}
